10.源码阅读(插件式换肤-安卓Resources加载资源的过程-android api 26)
我们知道,每一个View的子类都可以设置backgroud,那么这个背景是如何加载出来的呢?
找到View的构造方法
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { ...... case com.android.internal.R.styleable.View_background: background = a.getDrawable(attr); break; ...... }
@Nullable public Drawable getDrawable(@StyleableRes int index) { return getDrawableForDensity(index, 0); } @Nullable public Drawable getDrawableForDensity(@StyleableRes int index, int density) { ...... return mResources.loadDrawable(value, value.resourceId, density, mTheme); ...... }
看到这一行
return mResources.loadDrawable(value, value.resourceId, density, mTheme);
进入Resources中
@NonNull Drawable loadDrawable(@NonNull TypedValue value, int id, int density, @Nullable Theme theme) throws NotFoundException { return mResourcesImpl.loadDrawable(this, value, id, density, theme); }
可以看到,背景最终是被Resources中的ResourcesImpl加载得到Drawable的,ResourcesImpl在Resources构造中创建出来
@Deprecated public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) { this(null); mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments()); } private Resources() { ...... mResourcesImpl = new ResourcesImpl(AssetManager.getSystem(), metrics, config, new DisplayAdjustments()); }
我们再来看Resources是如何创建的,平常我们获取一些资源文件的时候,会这样获取Resources
context.getResources()
我们知道Context是抽象类,所以直接到Context的子类ContextImpl中去找
@Override public Resources getResources() { return mResources; }
那么这个mResources是什么时候创建的,找到这个方法
void setResources(Resources r) { if (r instanceof CompatResources) { ((CompatResources) r).setContext(this); } mResources = r; }
然后看到很多地方调用了这个方法
c.setResources(createResources(mActivityToken, pi, null, displayId, null, getDisplayAdjustments(displayId).getCompatibilityInfo())); c.setResources(createResources(mActivityToken, pi, null, displayId, null, getDisplayAdjustments(displayId).getCompatibilityInfo())); context.setResources(packageInfo.getResources()); context.setResources(ResourcesManager.getInstance().getResources( mActivityToken, mPackageInfo.getResDir(), paths, mPackageInfo.getOverlayDirs(), mPackageInfo.getApplicationInfo().sharedLibraryFiles, displayId, null, mPackageInfo.getCompatibilityInfo(), classLoader)); context.setResources(resourcesManager.createBaseActivityResources(activityToken, packageInfo.getResDir(), splitDirs, packageInfo.getOverlayDirs(), packageInfo.getApplicationInfo().sharedLibraryFiles, displayId, overrideConfiguration, compatInfo, classLoader));
这样的方法有好几个,而且我们找不到其他地方给mResources赋值的地方,初步可以判断,Resources就是这样创建的,顺着这几个方法去看,你会发现,尽管setResources方法有好几种形式,但最后都会进入到ResourcesManger这个类中,这个方法的注释可以看看,Resources是会被缓存的,一个Resources的生命周期和这个Activity同等,当classloader改变,Resources也会改变
/** * Gets or creates a new Resources object associated with the IBinder token. References returned * by this method live as long as the Activity, meaning they can be cached and used by the * Activity even after a configuration change. If any other parameter is changed * (resDir, splitResDirs, overrideConfig) for a given Activity, the same Resources object * is updated and handed back to the caller. However, changing the class loader will result in a * new Resources object. * <p/> * If activityToken is null, a cached Resources object will be returned if it matches the * input parameters. Otherwise a new Resources object that satisfies these parameters is * returned. * * @param activityToken Represents an Activity. If null, global resources are assumed. * @param resDir The base resource path. Can be null (only framework resources will be loaded). * @param splitResDirs An array of split resource paths. Can be null. * @param overlayDirs An array of overlay paths. Can be null. * @param libDirs An array of resource library paths. Can be null. * @param displayId The ID of the display for which to create the resources. * @param overrideConfig The configuration to apply on top of the base configuration. Can be * null. Mostly used with Activities that are in multi-window which may override width and * height properties from the base config. * @param compatInfo The compatibility settings to use. Cannot be null. A default to use is * {@link CompatibilityInfo#DEFAULT_COMPATIBILITY_INFO}. * @param classLoader The class loader to use when inflating Resources. If null, the * {@link ClassLoader#getSystemClassLoader()} is used. * @return a Resources object from which to access resources. */ public @Nullable Resources getResources(@Nullable IBinder activityToken, @Nullable String resDir, @Nullable String[] splitResDirs, @Nullable String[] overlayDirs, @Nullable String[] libDirs, int displayId, @Nullable Configuration overrideConfig, @NonNull CompatibilityInfo compatInfo, @Nullable ClassLoader classLoader) { try { Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ResourcesManager#getResources"); final ResourcesKey key = new ResourcesKey( resDir, splitResDirs, overlayDirs, libDirs, displayId, overrideConfig != null ? new Configuration(overrideConfig) : null, // Copy compatInfo); classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader(); return getOrCreateResources(activityToken, key, classLoader); } finally { Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); } }
/** * Gets an existing Resources object set with a ResourcesImpl object matching the given key, * or creates one if it doesn't exist. * * @param activityToken The Activity this Resources object should be associated with. * @param key The key describing the parameters of the ResourcesImpl object. * @param classLoader The classloader to use for the Resources object. * If null, {@link ClassLoader#getSystemClassLoader()} is used. * @return A Resources object that gets updated when * {@link #applyConfigurationToResourcesLocked(Configuration, CompatibilityInfo)} * is called. */ private @Nullable Resources getOrCreateResources(@Nullable IBinder activityToken, @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) { synchronized (this) { ...... //下边是分两种情况,当activityToken(IBinder)是否为null的情况下根据key获取ResourcesImpl, //只要这个ResourcesImpl存在,就会直接得到Resources缓存返回或者新创建一个Resources返回 if (activityToken != null) { ...... //根据key获取与之对应的ResourcesImpl缓存 ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key); if (resourcesImpl != null) { ...... // 只要根据key获取到的ResourcesImpl不为null,就根据这个ResourcesImpl去获取缓存的 //Resources,如果又这个Resources缓存,就返回,没有就创建,具体看这个方法 return getOrCreateResourcesForActivityLocked(activityToken, classLoader, resourcesImpl, key.mCompatInfo); } // We will create the ResourcesImpl object outside of holding this lock. } else { ...... ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key); if (resourcesImpl != null) { ...... return getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo); } } //当程序走到这里的时候,说明ResourcesImpl没有找到,Resources也就没有得到,那么这里就是根据 //key创建出一个ResourcesImpl来,程序第一次运行的时候肯定会首先走到这里,所以,上边的代码可以 //不用太重点的去看,接下来我们看看ResourcesImpl是如何被创建出来的,见方法createResourcesImpl // If we're here, we didn't find a suitable ResourcesImpl to use, so create one now. ResourcesImpl resourcesImpl = createResourcesImpl(key); if (resourcesImpl == null) { return null; } synchronized (this) { ResourcesImpl existingResourcesImpl = findResourcesImplForKeyLocked(key); if (existingResourcesImpl != null) { //从缓存中获取 ...... resourcesImpl.getAssets().close(); resourcesImpl = existingResourcesImpl; } else { // 将创建的ResourcesImpl缓存起来 mResourceImpls.put(key, new WeakReference<>(resourcesImpl)); } final Resources resources; //在此针对activityToken是否为null分别处理,在getOrCreateResourcesForActivityLocked和getOrCreateResourcesLocked //这两个方法中,我们重点关注,Resources不存在缓存的情况,所以,最终会看到Resourses的创建, //见下边的方法 if (activityToken != null) { resources = getOrCreateResourcesForActivityLocked(activityToken, classLoader, resourcesImpl, key.mCompatInfo); } else { resources = getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo); } return resources; } //Resources的创建,这里看到根据条件的不同有两种方式获取,一个是new CompatResources,一个是 //new Resources,进入到CompatResources类中,我们看到这个构造最终也会调用Resources的一个构造 //方法public Resources(@Nullable ClassLoader classLoader) 返回Resources,可见这个Resources是new //出来的 Resources resources = compatInfo.needsCompatResources() ? new CompatResources(classLoader) : new Resources(classLoader); //给Resources设置ResourcesImpl resources.setImpl(impl); //加入缓存 mResourceReferences.add(new WeakReference<>(resources));
getOrCreateResourcesForActivityLocked
/** * Gets an existing Resources object tied to this Activity, or creates one if it doesn't exist * or the class loader is different. */ private @NonNull Resources getOrCreateResourcesForActivityLocked(@NonNull IBinder activityToken, @NonNull ClassLoader classLoader, @NonNull ResourcesImpl impl, @NonNull CompatibilityInfo compatInfo) { ...... //有缓存获取缓存 Resources resources = weakResourceRef.get(); ...... return resources; ...... //没有缓存创建出来然后加入缓存 Resources resources = compatInfo.needsCompatResources() ? new CompatResources(classLoader) : new Resources(classLoader); resources.setImpl(impl); activityResources.activityResources.add(new WeakReference<>(resources)); ...... return resources; }
createResourcesImpl,可以看到ResourcesImpl的创建依赖于这几个对象AssetManager,DisplayMetrics,Configuration,DisplayAdjustments
private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) { final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration); daj.setCompatibilityInfo(key.mCompatInfo); final AssetManager assets = createAssetManager(key); if (assets == null) { return null; } final DisplayMetrics dm = getDisplayMetrics(key.mDisplayId, daj); final Configuration config = generateConfig(key, dm); final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj); ...... return impl; }
着重看AssetManager的创建,这个类很关键
/** * Creates an AssetManager from the paths within the ResourcesKey. * * This can be overridden in tests so as to avoid creating a real AssetManager with * real APK paths. * @param key The key containing the resource paths to add to the AssetManager. * @return a new AssetManager. */ @VisibleForTesting protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) { AssetManager assets = new AssetManager(); // resDir can be null if the 'android' package is creating a new Resources object. // This is fine, since each AssetManager automatically loads the 'android' package // already. if (key.mResDir != null) { // 将app中的资源路径都加入到AssetManager对象中,下边的方法都可以不看,我们重点 //关注这个方法,可以说,应用之所以能加载资源,就是通过AssetManager以及调用addAssetPath对他设置的 //资源路径 if (assets.addAssetPath(key.mResDir) == 0) { Log.e(TAG, "failed to add asset path " + key.mResDir); return null; } } ..... return assets; }
getDisplayMetrics,也只是new了一个DisplayMetrics
@VisibleForTesting protected @NonNull DisplayMetrics getDisplayMetrics(int displayId, DisplayAdjustments da) { DisplayMetrics dm = new DisplayMetrics(); final Display display = getAdjustedDisplay(displayId, da); if (display != null) { display.getMetrics(dm); } else { dm.setToDefaults(); } return dm; }
generateConfig,Configuration也是new出来的
private Configuration generateConfig(@NonNull ResourcesKey key, @NonNull DisplayMetrics dm) { Configuration config; .... config = new Configuration(getConfiguration()); .... return config; }
Resources的创建中有一个个关键的类,就是ResourcesImpl,这个类的创建需要几个重要的信息,其中之一就是AssetManager,通过直接实例话一个AssetManager对象并给这个对象设置资源路径,这是#Resources可以获取到文件资源的基础,DisplayMetrics或者Configuration则相当于一些固定设置,AssetManager中设置的这个路径,其实就是我们将要设置的apk的路径,从这个apk中获取资源
如此一来,我们获取到了Resources,就可以自由的去获取资源文件了
那么我们再回到最初,看看Resources是如何loadDrawable的
Resources中
@NonNull Drawable loadDrawable(@NonNull TypedValue value, int id, int density, @Nullable Theme theme) throws NotFoundException { return mResourcesImpl.loadDrawable(this, value, id, density, theme); }
ResourcesImpl中
@Nullable Drawable loadDrawable(@NonNull Resources wrapper, @NonNull TypedValue value, int id, int density, @Nullable Resources.Theme theme) throws NotFoundException { ...... Drawable dr; boolean needsNewDrawableAfterCache = false; if (cs != null) { dr = cs.newDrawable(wrapper); } else if (isColorDrawable) { dr = new ColorDrawable(value.data); } else { dr = loadDrawableForCookie(wrapper, value, id, density, null); } ...... return dr; ......
/** * Loads a drawable from XML or resources stream. */ private Drawable loadDrawableForCookie(@NonNull Resources wrapper, @NonNull TypedValue value, int id, int density, @Nullable Resources.Theme theme) { ...... final Drawable dr; Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file); try { //如果资源是xml文件 if (file.endsWith(".xml")) { final XmlResourceParser rp = loadXmlResourceParser( file, id, value.assetCookie, "drawable"); dr = Drawable.createFromXmlForDensity(wrapper, rp, density, theme); rp.close(); } else { //如果资源是图片资源,打开它得到流,然后解析得到drawable final InputStream is = mAssets.openNonAsset( value.assetCookie, file, AssetManager.ACCESS_STREAMING); dr = Drawable.createFromResourceStream(wrapper, value, is, file, null); is.close(); } ...... Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); return dr; }
经过上面的分析,皮肤切换的思路已经有了,下载apk文件,这个文件中包含有另一个皮肤的各种资源文件,通过Resources去加载这个apk中的资源,达到换肤的效果,关键代码如下
//点击从手机中一个apk中获取图片资源并且设置给ImageView显示 //获取系统的两个参数 Resources superResources = getResources(); //创建assetManger(无法直接new因为被hide了,所以用反射) AssetManager assetManager = AssetManager.class.newInstance(); //添加资源目录(addAssetPath也是一样被hide无法直接调用) Method method = AssetManager.class.getDeclaredMethod("addAssetPath", String.class); method.setAccessible(true);//如果是私有的,添上防止万一某一天他变成了私有的 method.invoke(assetManager,Environment.getExternalStorageDirectory().getAbsolutePath()+File.separator+"red.skin");//注意你资源的名字要一致 //DisplayMetrics和Configuration的对象可以直接new出来,这里使用的是从getResources得到的Resources中获取,其实也是new出来的 Resources resources = new Resources(assetManager,superResources.getDisplayMetrics(),superResources.getConfiguration()); //用创建好的Resources获取资源(注意着三个参数,第一个是要获取资源的名字,我们设置的是girl,不要忘了,第二个参数代表这个资源在哪个文件夹中,第三个参数表示要获取资源的apk的包名,缺一不可) int identifier = resources.getIdentifier("girl", "drawable", "com.example.myapplication"); if (identifier != 0){ Drawable drawable = resources.getDrawable(identifier); mImage.setImageDrawable(drawable); }
下面是native端AssetManager初始化的过程,为什么我们的app可以调用系统提供好的资源,以及这些资源是如何加载的,可以在这里得到答案
AssetManager的init()
public AssetManager() { synchronized (this) { if (DEBUG_REFS) { mNumRefs = 0; incRefsLocked(this.hashCode()); } init(false); if (localLOGV) Log.v(TAG, "New asset manager: " + this); ensureSystemAssets(); } } //android_util_AssetManager.cpp //AssetManager.cpp private native final void init(boolean isSystem);
static void android_content_AssetManager_init(JNIEnv* env, jobject clazz, jboolean isSystem) { if (isSystem) { verifySystemIdmaps(); } // AssetManager.cpp AssetManager* am = new AssetManager(); if (am == NULL) { jniThrowException(env, "java/lang/OutOfMemoryError", ""); return; } am->addDefaultAssets(); ALOGV("Created AssetManager %p for Java object %p\n", am, clazz); env->SetLongField(clazz, gAssetManagerOffsets.mObject,reinterpret_cast<jlong>(am)); } bool AssetManager::addDefaultAssets() { const char* root = getenv("ANDROID_ROOT"); LOG_ALWAYS_FATAL_IF(root == NULL, "ANDROID_ROOT not set"); String8 path(root); // 初始化的时候加载系统的framework-res.apk path.appendPath(kSystemAssets); return addAssetPath(path, NULL); }
AssetManager的addAssetPath(String path)方法
bool AssetManager::addAssetPath(const String8& path, int32_t* cookie) { AutoMutex _l(mLock); asset_path ap; String8 realPath(path); if (kAppZipName) { realPath.appendPath(kAppZipName); } ap.type = ::getFileType(realPath.string()); if (ap.type == kFileTypeRegular) { ap.path = realPath; } else { ap.path = path; ap.type = ::getFileType(path.string()); if (ap.type != kFileTypeDirectory && ap.type != kFileTypeRegular) { ALOGW("Asset path %s is neither a directory nor file (type=%d).", path.string(), (int)ap.type); return false; } } // Skip if we have it already. for (size_t i=0; i<mAssetPaths.size(); i++) { if (mAssetPaths[i].path == ap.path) { if (cookie) { *cookie = static_cast<int32_t>(i+1); } return true; } } ALOGV("In %p Asset %s path: %s", this, ap.type == kFileTypeDirectory ? "dir" : "zip", ap.path.string()); // Check that the path has an AndroidManifest.xml Asset* manifestAsset = const_cast<AssetManager*>(this)->openNonAssetInPathLocked( kAndroidManifest, Asset::ACCESS_BUFFER, ap); if (manifestAsset == NULL) { // This asset path does not contain any resources. delete manifestAsset; return false; } delete manifestAsset; mAssetPaths.add(ap); // new paths are always added at the end if (cookie) { *cookie = static_cast<int32_t>(mAssetPaths.size()); } #ifdef HAVE_ANDROID_OS // Load overlays, if any asset_path oap; for (size_t idx = 0; mZipSet.getOverlay(ap.path, idx, &oap); idx++) { mAssetPaths.add(oap); } #endif if (mResources != NULL) { appendPathToResTable(ap); } return true; } bool AssetManager::appendPathToResTable(const asset_path& ap) const { // skip those ap's that correspond to system overlays if (ap.isSystemOverlay) { return true; } Asset* ass = NULL; ResTable* sharedRes = NULL; bool shared = true; bool onlyEmptyResources = true; MY_TRACE_BEGIN(ap.path.string()); Asset* idmap = openIdmapLocked(ap); size_t nextEntryIdx = mResources->getTableCount(); ALOGV("Looking for resource asset in '%s'\n", ap.path.string()); if (ap.type != kFileTypeDirectory) { if (nextEntryIdx == 0) { // The first item is typically the framework resources, // which we want to avoid parsing every time. sharedRes = const_cast<AssetManager*>(this)-> mZipSet.getZipResourceTable(ap.path); if (sharedRes != NULL) { // skip ahead the number of system overlay packages preloaded nextEntryIdx = sharedRes->getTableCount(); } } if (sharedRes == NULL) { ass = const_cast<AssetManager*>(this)-> mZipSet.getZipResourceTableAsset(ap.path); if (ass == NULL) { ALOGV("loading resource table %s\n", ap.path.string()); ass = const_cast<AssetManager*>(this)-> openNonAssetInPathLocked("resources.arsc", Asset::ACCESS_BUFFER, ap); if (ass != NULL && ass != kExcludedAsset) { ass = const_cast<AssetManager*>(this)-> mZipSet.setZipResourceTableAsset(ap.path, ass); } } if (nextEntryIdx == 0 && ass != NULL) { // If this is the first resource table in the asset // manager, then we are going to cache it so that we // can quickly copy it out for others. ALOGV("Creating shared resources for %s", ap.path.string()); sharedRes = new ResTable(); sharedRes->add(ass, idmap, nextEntryIdx + 1, false); #ifdef HAVE_ANDROID_OS const char* data = getenv("ANDROID_DATA"); LOG_ALWAYS_FATAL_IF(data == NULL, "ANDROID_DATA not set"); String8 overlaysListPath(data); overlaysListPath.appendPath(kResourceCache); overlaysListPath.appendPath("overlays.list"); addSystemOverlays(overlaysListPath.string(), ap.path, sharedRes, nextEntryIdx); #endif sharedRes = const_cast<AssetManager*>(this)-> mZipSet.setZipResourceTable(ap.path, sharedRes); } } } else { ALOGV("loading resource table %s\n", ap.path.string()); ass = const_cast<AssetManager*>(this)-> openNonAssetInPathLocked("resources.arsc", Asset::ACCESS_BUFFER, ap); shared = false; } if ((ass != NULL || sharedRes != NULL) && ass != kExcludedAsset) { ALOGV("Installing resource asset %p in to table %p\n", ass, mResources); if (sharedRes != NULL) { ALOGV("Copying existing resources for %s", ap.path.string()); mResources->add(sharedRes); } else { ALOGV("Parsing resources for %s", ap.path.string()); mResources->add(ass, idmap, nextEntryIdx + 1, !shared); } onlyEmptyResources = false; if (!shared) { delete ass; } } else { ALOGV("Installing empty resources in to table %p\n", mResources); mResources->addEmpty(nextEntryIdx + 1); } if (idmap != NULL) { delete idmap; } MY_TRACE_END(); return onlyEmptyResources; }
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Android 四种引用比较与源码分析
目录介绍 0.关于四种引用 0.1 引用说明 0.2 关于Java下ref包和Android下ref包 1.强引用 1.0 关于强引用引用的场景 1.1 强引用介绍 1.2 强引用的特点 1.3 注意相互引用情况 2.软引用 2.0 关于SoftReference软引用 2.1 软引用应用场景 2.2 软引用的简单使用 2.3 软引用的特点 2.4 实际应用案例 2.5 注意避免软引用获取对象为null 3.弱引用 3.0 关于WeakReference弱引用 3.1 WeakReference:防止内存泄漏,要保证内存被虚拟机回收 3.1.1 先看一个handler小案例 3.1.2 为什么这样会造成内存泄漏 3.1.3 根本原因 3.2 弱引用解决办法 3.3 弱引用实际应用案例 4.虚引用 4.0 关于PhantomReference类虚引用 4.1 Android实际开发中没有用到过 5.四种引用其他介绍 5.1 弱引用和软引用区别 5.2 使用软引用或者弱引用防止内存泄漏 5.3 到底什么时候使用软引用,什么时候使用弱引用呢? 5.4 四种引用用一张表总结[摘自网络] 6.源...
- 下一篇
Android 实现无网络传输文件(3)
系列文章:Android 实现无网络传输文件(1)Android 实现无网络传输文件(2) 在第一篇文章里,我介绍了如何通过 Wifi Direct 实现 Android 设备之间隔空传输文件的功能,在第二篇文章里,我将传输方法改为通过Wifi热点的方式来传输文件 这里,我修复了第二种方法中的一些bug,且提供的传输信息更为丰富,下图展示的就是文章发送端的传输信息 提供的信息有:传输总耗时、瞬时传输速率、平均传输速率、预估完成时间、传输进度等,具体的开发思路还是和上一篇文章一样,这里不再赘述 项目主页:WifiFileTransfer 我的GitHub主页:leavesC
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- Eclipse初始化配置,告别卡顿、闪退、编译时间过长
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- CentOS8编译安装MySQL8.0.19
- Linux系统CentOS6、CentOS7手动修改IP地址
- CentOS7安装Docker,走上虚拟化容器引擎之路
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题