Android资源的热修复,就是在app不重新安装的情况下,利用下发的补丁包直接更新本app中的资源。
我们在开发阿里云移动热修复(Sophix)的过程中,对Android资源的加载原理做了深入的探究,最终在资源修复方法上取得了突破性进展!新的资源修复方法不论是在使用便捷性、补丁包大小以及运行时效率方面,相比其他实现都有巨大的优势。
目前市面上的很多资源热修复方案基本上都是参考了Instant Run的实现。
首先,我们简单来看一下Instant Run是怎么做到资源热修复的。
Instant Run资源热修复的核心代码就是这个monkeyPatchExistingResources方法:
@com/android/tools/fd/runtime/MonkeyPatcher.java public static void monkeyPatchExistingResources(@Nullable Context context, @Nullable String externalResourceFile, @Nullable Collection<Activity> activities) { if (externalResourceFile == null) { return; } try { // %% Part 1. 创建一个新的AssetManager,并通过反射调用addAssetPath添加/sdcard上的新资源包. // 这样就构造出了一个带新资源的AssetManager // Create a new AssetManager instance and point it to the resources installed under // /sdcard AssetManager newAssetManager = AssetManager.class.getConstructor().newInstance(); Method mAddAssetPath = AssetManager.class.getDeclaredMethod("addAssetPath", String.class); mAddAssetPath.setAccessible(true); if (((Integer) mAddAssetPath.invoke(newAssetManager, externalResourceFile)) == 0) { throw new IllegalStateException("Could not create new AssetManager"); } // Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm // in L, so we do it unconditionally. Method mEnsureStringBlocks = AssetManager.class.getDeclaredMethod("ensureStringBlocks"); mEnsureStringBlocks.setAccessible(true); mEnsureStringBlocks.invoke(newAssetManager); // %% Part 2. 反射得到Activity中AssetManager的引用处,全部换成刚才新构建的newAssetManager if (activities != null) { for (Activity activity : activities) { Resources resources = activity.getResources(); try { Field mAssets = Resources.class.getDeclaredField("mAssets"); mAssets.setAccessible(true); mAssets.set(resources, newAssetManager); } catch (Throwable ignore) { Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl"); mResourcesImpl.setAccessible(true); Object resourceImpl = mResourcesImpl.get(resources); Field implAssets = resourceImpl.getClass().getDeclaredField("mAssets"); implAssets.setAccessible(true); implAssets.set(resourceImpl, newAssetManager); } ... ... pruneResourceCaches(resources); } } // %% Part 3. 得到Resources的弱引用集合,把他们的AssetManager成员替换成newAssetManager // Iterate over all known Resources objects Collection<WeakReference<Resources>> references; if (SDK_INT >= KITKAT) { // Find the singleton instance of ResourcesManager Class<?> resourcesManagerClass = Class.forName("android.app.ResourcesManager"); Method mGetInstance = resourcesManagerClass.getDeclaredMethod("getInstance"); mGetInstance.setAccessible(true); Object resourcesManager = mGetInstance.invoke(null); try { Field fMActiveResources = resourcesManagerClass.getDeclaredField("mActiveResources"); fMActiveResources.setAccessible(true); @SuppressWarnings("unchecked") ArrayMap<?, WeakReference<Resources>> arrayMap = (ArrayMap<?, WeakReference<Resources>>) fMActiveResources.get(resourcesManager); references = arrayMap.values(); } catch (NoSuchFieldException ignore) { Field mResourceReferences = resourcesManagerClass.getDeclaredField("mResourceReferences"); mResourceReferences.setAccessible(true); //noinspection unchecked references = (Collection<WeakReference<Resources>>) mResourceReferences.get(resourcesManager); } } else { Class<?> activityThread = Class.forName("android.app.ActivityThread"); Field fMActiveResources = activityThread.getDeclaredField("mActiveResources"); fMActiveResources.setAccessible(true); Object thread = getActivityThread(context, activityThread); @SuppressWarnings("unchecked") HashMap<?, WeakReference<Resources>> map = (HashMap<?, WeakReference<Resources>>) fMActiveResources.get(thread); references = map.values(); } for (WeakReference<Resources> wr : references) { Resources resources = wr.get(); if (resources != null) { // Set the AssetManager of the Resources instance to our brand new one try { Field mAssets = Resources.class.getDeclaredField("mAssets"); mAssets.setAccessible(true); mAssets.set(resources, newAssetManager); } catch (Throwable ignore) { Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl"); mResourcesImpl.setAccessible(true); Object resourceImpl = mResourcesImpl.get(resources); Field implAssets = resourceImpl.getClass().getDeclaredField("mAssets"); implAssets.setAccessible(true); implAssets.set(resourceImpl, newAssetManager); } resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics()); } } } catch (Throwable e) { throw new IllegalStateException(e); }}简要说来,Instant Run中的资源热修复分为两步,
1、构造一个新的AssetManager,并通过反射调用addAssetPath,把这个完整的新资源包加入到AssetManager中。这样就得到了一个含有所有新资源的AssetManager。
2、找到所有之前引用到原有AssetManager的地方,通过反射,把引用处替换为AssetManager。
其实仔细看可以发现,大量代码都是在处理兼容性问题和找到所有AssetManager的引用处。真正的实现逻辑其实很简单。
这其中的重点,自然是addAssetPath这个函数。现在我们来看一下它的底层实现逻辑。
以Android6.0为例,addAssetPath最终调用到了native方法。
@frameworks/base/core/java/android/content/res/AssetManager.java /** * Add an additional set of assets to the asset manager. This can be * either a directory or ZIP file. Not for use by applications. Returns * the cookie of the added asset, or 0 on failure. * {@hide} */ public final int addAssetPath(String path) { synchronized (this) { int res = addAssetPathNative(path); makeStringBlocks(mStringBlocks); return res; } } ... ... private native final int addAssetPathNative(String path);Java层的AssetManager只是个包装,真正关于资源处理的所有逻辑,其实都位于native层由C++实现的AssetManager。
执行addAssetPath就是解析这个格式,然后构造出底层数据结构的过程。整个解析资源的调用链是:
public final int addAssetPath(String path)
=jni=> android_content_AssetManager_addAssetPath
=> AssetManager::addAssetPath => AssetManager::appendPathToResTable => ResTable::add => ResTable::addInternal => ResTable::parsePackage
解析的细节比较繁琐,就不细细说明了,有兴趣的可以一层层追下去。
大致过程就是,通过传入的资源包路径,先得到其中的resources.arsc,然后解析它的格式,存放在底层的AssetManager的mResources成员中。
@frameworks/base/include/androidfw/AssetManager.h class AssetManager : public AAssetManager { ... ... mutable ResTable* mResources; ... ...AssetManager的mResources成员是一个ResTable结构体:
class ResTable { mutable Mutex mLock; // 互斥锁,用于多进程间互斥操作。 status_t mError; ResTable_config mParams; // Array of all resource tables. Vector<Header*> mHeaders; // 表示所有resources.arsc原始数据,这就等同于所有通过addAssetPath加载进来的路径的资源id信息。 // Array of packages in all resource tables. Vector<PackageGroup*> mPackageGroups; // 资源包的实体,包含所有加载进来的package id所对应的资源。 // Mapping from resource package IDs to indices into the internal // package array. uint8_t mPackageMap[256]; // 索引表,表示0~255的package id,每个元组分别存放 该id所属PackageGroup 在mPackageGroups中的index uint8_t mNextPackageId; };一个Android进程只包含一个ResTable,ResTable的成员变量mPackageGroups就是所有解析过的资源包的集合。任何一个资源包中都含有resources.arsc,它记录了所有资源的id分配情况以及资源中的所有字符串。这些信息是以二进制方式存储的。底层的AssetManager做的事就是解析这个文件,然后把相关信息存储到mPackageGroups里面。
整个resources.arsc文件,实际上是由一个个ResChunk(以下简称chunk)拼接起来的。从文件头开始,每个chunk的头部都是一个ResChunk_header结构,它指示了这个chunk的大小和数据类型。
/** * Header that appears at the front of every data chunk in a resource. */ struct ResChunk_header { // Type identifier for this chunk. The meaning of this value depends // on the containing chunk. uint16_t type; // Size of the chunk header (in bytes). Adding this value to // the address of the chunk allows you to find its associated data // (if any). uint16_t headerSize; // Total size of this chunk (in bytes). This is the chunkSize plus // the size of any data associated with the chunk. Adding this value // to the chunk allows you to completely skip its contents (including // any child chunks). If this value is the same as chunkSize, there is // no data associated with the chunk. uint32_t size; };通过ResChunk_header中的type成员,可以知道这个chunk是什么类型,从而就可以知道应该如何解析这个chunk。
解析完一个chunk后,从这个chunk + size的位置开始,就可以得到下一个chunk起始位置,这样就可以依次读取完整个文件的数据内容。
一般来说,一个resources.arsc里面包含若干个package,不过默认情况下,由打包工具aapt打出来的包只有一个package。这个package里包含了app中的所有资源信息。
资源信息主要是指每个资源的名称以及它对应的编号。我们知道,Android中的每个资源,都有它唯一的编号。
编号是一个32位数字,用十六进制来表示就是0xPPTTEEEE。PP为package id,TT为type id,EEEE为entry id。
它们代表什么?在resources.arsc里是以怎样的方式记录的呢?
对于package id,每个package对应的是类型为RES_TABLE_PACKAGE_TYPE的ResTable_package结构体,ResTable_package结构体的id成员变量就表示它的package id。对于type id,每个type对应的是类型为RES_TABLE_TYPE_SPEC_TYPE的ResTable_typeSpec结构体。它的id成员变量就是type id。但是,该type id具体对应什么类型,是需要到package chunk里的Type String Pool中去解析得到的。比如Type String Pool中依次有attr、drawable、mipmap、layout字符串。就表示attr类型的type id为1, drawable类型的type id为2,mipmap类型的type id为3,layout类型的type id为4。所以,每个type id对应了Type String Pool里的字符顺序所指定的类型。对于entry id,每个entry表示一个资源项,资源项是按照排列的先后顺序自动被标机编号的。也就是说,一个type里按位置出现的第一个资源项,其entry id为0x0000,第二个为0x0001,以此类推。因此我们是无法直接指定entry id的,只能够根据排布顺序决定。资源项之间是紧密排布的,没有空隙,但是可以指定资源项为ResTable_type::NO_ENTRY来填入一个空资源。举个例子,我们随便找个带资源的apk,用aapt解析一下,看到其中的一行是:
$ aapt d resources app-debug.apk ... ... spec resource 0x7f040019 com.taobao.patch.demo:layout/activity_main: flags=0x00000000 ... ...这就表示,activity_main.xml这个资源的编号是0x7f040019。它的package id是0x7f,资源类型的id为0x04,Type String Pool里的第四个字符串正是layout类型,而0x04类型的第0x0019个资源项就是activity_main这个资源。
默认由Android SDK编出来的apk,是由aapt工具进行打包的,其资源包的package id就是0x7f。
系统的资源包,也就是framework-res.jar,package id为0x01。
在走到app的第一行代码之前,系统就已经帮我们构造好一个已经添加了安装包资源的AssetManager了。
@frameworks/base/core/java/android/app/ResourcesManager.java Resources getTopLevelResources(String resDir, String[] splitResDirs, String[] overlayDirs, String[] libDirs, int displayId, Configuration overrideConfiguration, CompatibilityInfo compatInfo) { ... ... AssetManager assets = new AssetManager(); // resDir就是安装包apk if (resDir != null) { if (assets.addAssetPath(resDir) == 0) { return null; } } ... ...因此,这个AssetManager里就已经包含了系统资源包以及app的安装包,就是package id为0x01的framework-res.jar中的资源和package id为0x7f的app安装包资源。
如果此时直接在原有AssetManager上继续addAssetPath的完整补丁包的话,由于补丁包里面的package id也是0x7f,就会使得同一个package id的包被加载两次。这会有怎样的问题呢?
在Android L之后,这是没问题的,他会默默地把后来的包添加到之前的包的同一个PackageGroup下面。
而在解析的时候,会与之前的包比较同一个type id所对应的类型,如果该类型下的资源项数目和之前添加过的不一致,会打出一条warning log,但是仍旧加入到该类型的TypeList中。
status_t ResTable::parsePackage(const ResTable_package* const pkg, const Header* const header) ... ... TypeList& typeList = group->types.editItemAt(typeIndex); if (!typeList.isEmpty()) { const Type* existingType = typeList[0]; if (existingType->entryCount != newEntryCount && idmapIndex < 0) { ALOGW("ResTable_typeSpec entry count inconsistent: given %d, previously %d", (int) newEntryCount, (int) existingType->entryCount); // We should normally abort here, but some legacy apps declare // resources in the 'android' package (old bug in AAPT). } } Type* t = new Type(header, package, newEntryCount); t->typeSpec = typeSpec; t->typeSpecFlags = (const uint32_t*)( ((const uint8_t*)typeSpec) + dtohs(typeSpec->header.headerSize)); if (idmapIndex >= 0) { t->idmapEntries = idmapEntries[idmapIndex]; } typeList.add(t); ... ...但是在get这个资源的时候呢?
status_t ResTable::getEntry( const PackageGroup* packageGroup, int typeIndex, int entryIndex, const ResTable_config* config, Entry* outEntry) const { const TypeList& typeList = packageGroup->types[typeIndex]; ... ... // %% 从第一个type开始遍历,也就是说会先取得安装包的资源,然后才是补丁包的。 // Iterate over the Types of each package. const size_t typeCount = typeList.size(); for (size_t i = 0; i < typeCount; i++) { const Type* const typeSpec = typeList[i]; int realEntryIndex = entryIndex; int realTypeIndex = typeIndex; bool currentTypeIsOverlay = false; if (static_cast<size_t>(realEntryIndex) >= typeSpec->entryCount) { ALOGW("For resource 0xx, entry index(%d) is beyond type entryCount(%d)", Res_MAKEID(packageGroup->id - 1, typeIndex, entryIndex), entryIndex, static_cast<int>(typeSpec->entryCount)); // We should normally abort here, but some legacy apps declare // resources in the 'android' package (old bug in AAPT). continue; } const size_t numConfigs = typeSpec->configs.size(); for (size_t c = 0; c < numConfigs; c++) { ... ... if (bestType != NULL) { // Check if this one is less specific than the last found. If so, // we will skip it. We check starting with things we most care // about to those we least care about. if (!thisConfig.isBetterThan(bestConfig, config)) { if (!currentTypeIsOverlay || thisConfig.compare(bestConfig) != 0) { continue; } } } bestType = thisType; bestOffset = thisOffset; bestConfig = thisConfig; bestPackage = typeSpec->package; actualTypeIndex = realTypeIndex; // If no config was specified, any type will do, so skip if (config == NULL) { break; } } } }在获取某个Type的资源时,会从前往后遍历,也就是说先得到原有安装包里的资源,除非后面的资源的config比前面的更详细才会发生覆盖。而对于同一个config而言,补丁中的资源就永远无法生效了。所以在Android L以上的版本,在原有AssetManager上加入补丁包,是没有任何作用的,补丁中的资源无法生效。
而在Android KK及以下版本,addAssetPath只是把补丁包的路径添加到了mAssetPath中,而真正解析的资源包的逻辑是在app第一次执行AssetManager::getResTable的时候。
@android-4.4.4_r2/frameworks/base/libs/androidfw/AssetManager.cpp const ResTable* AssetManager::getResTable(bool required) const { // %% mResources已存在,直接返回,不再往下走。 ResTable* rt = mResources; if (rt) { return rt; } // Iterate through all asset packages, collecting resources from each. AutoMutex _l(mLock); if (mResources != NULL) { return mResources; } if (required) { LOG_FATAL_IF(mAssetPaths.size() == 0, "No assets added to AssetManager"); } if (mCacheMode != CACHE_OFF && !mCacheValid) const_cast<AssetManager*>(this)->loadFileNameCacheLocked(); const size_t N = mAssetPaths.size(); for (size_t i=0; i<N; i++) { // ... %% 真正解析package的地方 ... } if (required && !rt) ALOGW("Unable to find resources file resources.arsc"); if (!rt) { mResources = rt = new ResTable(); } return rt; }而在执行到加载补丁代码的时候,getResTable已经执行过了无数次了。这是因为就算我们之前没做过任何资源相关操作,Android framework里的代码也会多次调用到那里。所以,以后即使是addAssetPath,也只是添加到了mAssetPath,并不会发生解析。所以补丁包里面的资源是完全不生效的!
所以,像Instant Run这种方案,一定需要一个全新的AssetManager时,然后再加入完整的新资源包,替换掉原有的AssetManager。
而一个好的资源热修复方案是怎样的呢?
首先,补丁包要足够小,像直接下发完整的补丁包肯定是不行的,很占用空间。
而像有些方案,是先进行bsdiff,对资源包做差量,然后下发差量包,在运行时合成完整包再加载。这样确实减小了包的体积,但是却在运行时多了合成的操作,耗费了运行时间和内存。合成后的包也是完整的包,仍旧会占用磁盘空间。
而如果不采用类似Instant Run的方案,市面上许多实现,是自己修改aapt,在打包时将补丁包资源进行重新编号。这样就会涉及到修改Android SDK工具包,即不利于集成也无法很好地对将来的aapt版本进行升级。
针对以上几个问题,一个好的资源热修复方案,既要保证补丁包足够小,不在运行时占用很多资源,又要不侵入打包流程。我们提出了一个目前市面上未曾实现的方案。
简单来说,我们构造了一个package id为0x66的资源包,这个包里只包含改变了的资源项,然后直接在原有AssetManager中addAssetPath这个包。然后,就可以了。
真的这么简单?
没错!由于补丁包的package id为0x66,不与目前已经加载的0x7f冲突,因此直接加入到已有的AssetManager中就可以直接使用了。补丁包里面的资源,只包含原有包里面没有而新的包里面有的新增资源,以及原有内容发生了改变的资源。
而资源的改变包含增加、减少、修改这三种情况,我们分别是如何处理的呢?
对于新增资源,直接加入补丁包,然后新代码里直接引用就可以了,没什么好说的。对于减少资源,我们只要不使用它就行了,因此不用考虑这种情况,它也不影响补丁包。对于修改资源,比如替换了一张图片之类的情况。我们把它视为新增资源,在打入补丁的时候,代码在引用处也会做相应修改,也就是直接把原来使用旧资源id的地方变为新id。用一张图来说明补丁包的情况,是这样的:
图中绿线表示新增资源。红线表示内容发生修改的资源。黑线表示内容没有变化,但是id发生改变的资源。×表示删除了的资源。
可以看到,新的资源包与旧资源包相比,新增了holo_grey和dropdn_item2资源,新增的资源被加入到patch中。并分配了0x66开头的资源id。
而新增的两个资源导致了在它们所属的type中跟在它们之后的资源id发生了位移。比如holo_light,id由0x7f020002变为0x7f020003,而abc_dialog由0x7f030004变为0x7f030003。新资源插入的位置是随机的,这与每次aapt打包时解析xml的顺序有关。发生位移的资源不会加入patch,但是在patch的代码中会调整id的引用处。
比如说在代码里,我们是这么写的
imageView.setImageResource(R.drawable.holo_light);这个R.drawable.holo_light是一个int值,它的值是aapt指定的,对于开发者透明,即使点进去,也会直接跳到对应res/drawable/holo_light.png,无法查看。不过可以用反编译工具,看到它的真实值是0x7f020002。所以这行代码其实等价于:
imageView.setImageResource(0x7f020002);而当打出了一个新包后,对开发者而言,holo_light的图片内容没变,代码引用处也没变。但是新包里面,同样是这句话,由于新资源的插入导致的id改变,对于R.drawable.holo_light的引用已经变成了:
imageView.setImageResource(0x7f020003);但实际上这种情况并不属于资源改变,更不属于代码的改变,所以我们在对比新旧代码之前,会把新包里面的这行代码修正回原来的id。
imageView.setImageResource(0x7f020002);然后再进行后续代码的对比。这样后续代码对比时就不会检测到发生了改变。
而对于内容发生改变的资源(类型为layout的activity_main,这可能是我们修改了activity_main.xml的文件内容。还有类型为string的no,可能是我们修改了这个字符串的值),它们都会被加入到patch中,并重新编号为新id。
而相应的代码,也会发生改变,比如,
setContentView(R.layout.activity_main);实际上也就是
setContentView(0x7f030000);在生成对比新旧代码之前,我们会把新包里面的这行代码变为
setContentView(0x66020000);这样,在进行代码对比时,会使得这行代码所在函数被检测到发生了改变。于是相应的代码修复会在运行时发生,这样就引用到了正确的新内容资源。
对于删除的资源,不会影响补丁包。
这很好理解,既然资源被删除了,就说明新的代码中也不会用到它,那资源放在那里没人用,就相当于不存在了。
可以看到,由于type0x01的所有资源项都没有变化,所以整个type0x01资源都没有加入到patch中。这也使得后面的type的id都往前移了一位。因此Type String Pool中的字符串也要进行修正,这样才能使得0x01的type指向drawable,而不是原来的attr。
所以我们可以看到,所谓简单,指的是运行时应用patch变的简单了。
而真正复杂的地方在于构造patch。我们需要把新旧两个资源包解开,分别解析其中的resources.arsc文件,对比新旧的不同,并将它们重新打成带有新package id的新资源包。这里补丁包指定的package id只要不是0x7f和0x01就行,可以是任意0x7f以下的数字,我们默认把它指定为0x66。
构造这样的补丁资源包,需要对整个resources.arsc的结构十分了解,要对二进制形式的一个一个chunk进行解析分类,然后再把补丁信息一个一个重新组装成二进制的chunk。这里面很多工作与aapt做的类似,实际上开发打包工具的时候也是参考了很多aapt和系统加载资源的代码。
对于Android L以后的版本,直接在原有AssetManager上应用patch就行了。并且由于用的是原来的AssetManager,所以原先大量的反射修改替换操作就完全不需要了,大大提高了加载补丁的效率。
但之前提到过,在Android KK和以下版本,addAssetPath是不会加载资源的,必须重新构造一个新的AssetManager并加入patch,再换掉原来的。那么我们不就又要和Instant Run一样,做一大堆兼容版本和反射替换的工作了吗?
对于这种情况,我们也找到了更优雅的方式,不需要再如此地大费周章。
在AssetManager的源码里面,有一个有趣的东西。
@frameworks/base/core/java/android/content/res/AssetManager.java public final class AssetManager { ... ... private native final void destroy(); ... ...明显,这个是用来销毁AssetManager并释放资源的函数,我们来看看它具体做了什么吧。
static void android_content_AssetManager_destroy(JNIEnv* env, jobject clazz) { AssetManager* am = (AssetManager*) (env->GetIntField(clazz, gAssetManagerOffsets.mObject)); ALOGV("Destroying AssetManager %p for Java object %p\n", am, clazz); if (am != NULL) { delete am; env->SetIntField(clazz, gAssetManagerOffsets.mObject, 0); } }可以看到,首先,它析构了native层的AssetManager,然后把java层的AssetManager对native层的AssetManager的引用设为空。
AssetManager::~AssetManager(void) { int count = android_atomic_dec(&gCount); //ALOGI("Destroying AssetManager in %p #%d\n", this, count); delete mConfig; delete mResources; // don't have a String class yet, so make sure we clean up delete[] mLocale; delete[] mVendor; }native层的AssetManager析构函数会析构它的所有成员,这样就会释放之前加载了的资源。
而现在,java层的AssetManager已经成为了空壳。我们就可以调用它的init方法,对它重新进行初始化了!
@frameworks/base/core/java/android/content/res/AssetManager.java public final class AssetManager { ... ... private native final void init(); ... ...这同样是个native方法,
static void android_content_AssetManager_init(JNIEnv* env, jobject clazz) { 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->SetIntField(clazz, gAssetManagerOffsets.mObject, (jint)am); }这样,在执行init的时候,会在native层创建一个没有添加过资源,并且mResources没有初始化的的AssetManager。然后我们再对它进行addAssetPath,之后由于mResource没有初始化过,就可以正常走到解析mResources的逻辑,加载所有此时add进去的资源了!
@android-4.4.4_r2/frameworks/base/libs/androidfw/AssetManager.cpp const ResTable* AssetManager::getResTable(bool required) const { ResTable* rt = mResources; // %% mResources没有初始化过,为空,因此不会return。 if (rt) { return rt; } ... ... // %% 这时就会走到这里,进行所有add进去的path的加载。 const size_t N = mAssetPaths.size(); for (size_t i=0; i<N; i++) { // ... 解析package ... } ... ... return rt; }这个方案的实现代码如下:
... ... Method initMeth = assetManagerMethod("init"); Method destroyMeth = assetManagerMethod("destroy"); Method addAssetPathMeth = assetManagerMethod("addAssetPath", String.class); // %% 析构AssetManager destroyMeth.invoke(am); // %% 重新构造AssetManager initMeth.invoke(am); // %% 置空mStringBlocks assetManagerField("mStringBlocks").set(am, null); // %% 重新添加原有AssetManager中加载过的资源路径 for (String path : loadedPaths) { LogTool.d(TAG, "pexyResources" + path); addAssetPathMeth.invoke(am, path); } // %% 添加patch资源路径 addAssetPathMeth.invoke(am, patchPath); // %% 重新对mStringBlocks赋值 assetManagerMethod("ensureStringBlocks").invoke(am); } private Method assetManagerMethod(String name, Class<?>... parameterTypes) { try { Method meth = Class.forName("android.content.res.AssetManager") .getDeclaredMethod(name, parameterTypes); meth.setAccessible(true); return meth; } catch (Exception e) { LogTool.e(TAG, "assetManagerMethod", e); return null; } } private Field assetManagerField(String name) { try { Field field = mAssetManagerClass.getDeclaredField(name); field.setAccessible(true); return field; } catch (Exception e) { LogTool.e(TAG, "assetManagerField", e); return null; } }这里需要注意的地方是mStringBlocks。它记录了之前加载过的所有资源包的String Pool,因此很多时候访问字符串是通过它来找到的。如果不进行重新构造,在后面使用到它时就会导致崩溃。
由于我们是直接对原有的AssetManager进行析构和重构,所有原先对AssetManager对象的引用是没有发生改变的,这样,就不需要像Instant Run那样进行繁琐的修改了。
顺带一提,类似Instant Run的完整替换资源的方案,在替换AssetManager这一步,也可以采用我们这种方式进行替换,省时省力又省心。
总结一下,相比于目前市面上的资源修复方式,我们提出的资源修复的优势在于:
不侵入打包,直接对比新旧资源即可产生补丁资源包。(对比修改aapt方式的实现)不必下发完整包,补丁包中只包含有变动的资源。(对比Instanat Run、Amigo等方式的实现)不需要在运行时合成完整包。不占用运行时计算和内存资源。(对比Tinker的实现)唯一有个需要注意的地方就是,因为对新的资源的引用是在新代码中,所有资源修复是需要代码修复的支持的。也因此所有资源修复方案必然是附带代码修复的。而之前提到过,本方案在进行代码修复前,会对资源引用处进行修正。而修正就是需要找到旧的资源id,换成新的id。查找旧id时是直接对int值进行替换,所以会找到0x7f??????这样的需要替换id。但是,如果有开发者使用到了0x7f??????这样的数字,而它并非资源id,可是却和需要替换的id数值相同,这就会导致这个数字被错误地替换。
但这种情况是极为罕见的,因为很少会有人用到这样特殊的数字,并且还需要碰巧这数字和资源id相等才行。即使出现,开发者也可以用拼接的方式绕过这类数字的产生。所以基本可以不用担心这种情况,只是需要注意它的存在。
这套资源修复方案目前已经完全集成进阿里云移动热修复(Sophix),值得一提的是,结合Sophix提供的代码热替换机制,资源也可以做到补丁下发即时生效,无需重启APP!如果对代码热替换的技术的实现细节有兴趣,可以看这篇文章,其中实现了兼容性极好的Java方法的Native热替换。
另外,不同于阿里Hotfix1.X版本笨拙的命令行操作,新的补丁工具实现了图形界面,使用起来更加方便快捷。
最后,展示一下这个工具的界面。轻松一键,即可完美生成补丁。
原创文章,转载请注明出处。手淘公众号文章链接:https://mp.weixin.qq.com/s/7f81xxRjqHu3Nu9xDrqShw
相关资源:敏捷开发V1.0.pptx