热修复中可能会涉及到资源文件的替换,有两个问题:
一、到底能不能替换?这里是将主APP中的资源替换成Patch apk中的资源,可以实现么?
二、怎么替换,会不会有资源id冲突的问题?
本文会来研究这两个问题,并给出Demo来展现如何替换资源。
加载patch apk时,和加载插件类似,可以参考我之前的两篇文章:
http://blog.csdn.net/dingjikerbo/article/details/47757511
http://blog.csdn.net/dingjikerbo/article/details/47783411
如下
DexClassLoader dexClassLoader = createDexClassLoader(pluginId, packageId, packagePath);
AssetManager assetManager = createAssetManager(packagePath);
Resources resources = createResources(assetManager);
private AssetManager createAssetManager(String dexPath) {
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod(
"addAssetPath", String.class);
addAssetPath.invoke(assetManager, dexPath);
return assetManager;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
private Resources createResources(AssetManager assetManager) {
Resources superRes = mContext.getResources();
Resources resources = new Resources(assetManager,
superRes.getDisplayMetrics(), superRes.getConfiguration());
return resources;
}
可见这里专门为Patch apk创建了独立的AssetManager,并将这个apk的路径添加到AssetManager的AssetPath路径集合中。
这里有两个问题:
1. 主APP和Patch加载资源用的是不同的AssetManager和Resources,如果主APP和Patch Apk中的资源id有冲突,那么加载资源时会不会串了?
2. 同一个AssetManager下可以添加多个资源包,假如这些资源包之间存在相同的资源id,加载资源时会不会串呢?
要研究这两个问题,我们需要先了解一下AssetManager的实现以及资源加载机制。
每个AssetManager下可以添加多个资源包,而且为了查找资源时更快,一定会先解析这些资源包,然后生成索引便于之后的资源加载。这个索引的key也就是资源的id应该包含资源的package id,资源type id以及资源的entry id。为保证key的唯一性,这三者不能同时冲突。除了资源type id通常是固定的之外,资源的package id 和entry id应该是打包apk的时候生成的,所以我们重点要确认一下打包apk时是如何生成资源的package id和entry id的。不过我们可以设想一下,假如两个资源包中的资源非常多,多到可以填满所有的资源entry id,那么就一定会有entry id的冲突,那么为保证key的唯一性的最后一道关卡就只剩下资源的package id了。然而实践中发现,编译时生成的R.java中所有的资源都是以0x7F开头,这个就是package id了,可见打包时默认的package id都是0x7F,除非修改aapt为每个包指定不同的package id,否则很有可能资源冲突。
当然如果采用的是不同的AssetManager去加载资源包里的资源就不会有这种问题了,因为索引表都不一样,即便key一样也没有关系。
接下来,我们将过一下AssetManager的代码,这里只是为了了解资源加载的大致流程,所以会略去一些细节,如果想更深入了解,可以参考如下几篇文章:
Android应用程序资源的编译和打包过程分析
Android应用程序资源管理器(Asset Manager)的创建过程分析
Android应用程序资源的查找过程分析
public AssetManager() {
init();
}
private native final void init();
可见,直接调用了native的init函数进行初始化
static void android_content_AssetManager_init(JNIEnv* env, jobject clazz)
{
AssetManager* am = new AssetManager();
am->addDefaultAssets();
env->SetIntField(clazz, gAssetManagerOffsets.mObject, (jint)am);
}
这个init主要做了三件事,首先在Native层New了一个AssetManager,然后向其中addDefaultAssets,最后将这个AssetManager的指针设置到Java中的AssetManager类的mObject。我们先来看看AssetManager 的构造函数。
class AssetManager : public AAssetManager {
public:
..........
Vector<asset_path> mAssetPaths;
mutable ResTable* mResources;
ResTable_config* mConfig;
CacheMode mCacheMode; // is the cache enabled?
SortedVector<AssetDir::FileInfo> mCache;
AssetManager(CacheMode cacheMode = CACHE_OFF);
..........
}
这个AssetManager中比较重要的是mAssetPaths和mResources。mAssetPaths中保存的是这个AssetManager维护的所有资源apk的路径。mResources就是一个Resource Table,相当于资源的索引表。再来看addDefaultAssets:
bool AssetManager::addDefaultAssets()
{
const char* root = getenv("ANDROID_ROOT");
String8 path(root);
path.appendPath(kSystemAssets);
return addAssetPath(path, NULL);
}
static const char* kSystemAssets = "framework/framework-res.apk";
可见这里所谓的defaultAssets就是${ANDROID_ROOT}/framework/framework-res.apk,是系统资源路径。
接下来重点讲解这个addAssetPath函数,在分析这个函数代码之前,我们先来看看那些地方调用了它:
一、addDefaultAssets时调用了addAssetPath(path, NULL)
二、Java层的AssetManager调用addAssetPath时调到了native层的android_content_AssetManager_addAssetPath如下
static jint android_content_AssetManager_addAssetPath(JNIEnv* env, jobject clazz,
jstring path)
{
ScopedUtfChars path8(env, path);
AssetManager* am = assetManagerForJavaObject(env, clazz);
void* cookie;
bool res = am->addAssetPath(String8(path8.c_str()), &cookie);
return (res) ? (jint)cookie : 0;
}
这两个地方调用方式略有不同,前者第二个参数传NULL,后者添加用户自定义的path时传入了&cookie,是个void *,看来要返回一个指针,至于这个指针是做什么的,我们只能在看了addAssetPath代码后才能知道。
bool AssetManager::addAssetPath(const String8& path, void** cookie) {
for (size_t i=0;