前言

VirtualAPK是didi出品的插件化框架,最近读了读源码,简单做些总结,先针对资源的处理这一块,dodola的这篇文章对原理阐述的很详尽,建议大家先看一下,我这边对文章中几个点再阐述下以及对gradle插件所做的事做一些总结。所在分支是dev。

资源的处理

一、先看下ResourcesManager.javacreateResources方法片段:

	if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
	    assetManager = AssetManager.class.newInstance();
	    ReflectUtil.invoke(AssetManager.class, assetManager, "addAssetPath", hostContext.getApplicationInfo().sourceDir);
	} else {
	    assetManager = hostResources.getAssets();
	}
	ReflectUtil.invoke(AssetManager.class, assetManager, "addAssetPath", apk);

这边针对资源的加载做了兼容处理,差异主要是在AssetManager.cppaddAssetPath方法,可以比较下4.4Android O的代码,会发现Android O中会将插件apk的resources.arsc所代表的Asset对象add到现有的索引表ResTable中,而4.4中只是将该apk的path加入到path列表里,所以它这边选择了new一个AssetManager。

文章中还提到了淘宝发布的《深入探索 Android 热修复技术原理》中的一种少兼容少hook的方案,简单理解就是重构native端的AssetManager,添加所有插件的path,接着AssetManager::getResTable时去生成新的索引表ResTable,而Java层的Resource对象地址是不变的,也就省去了兼容和hook。

二、Activity 启动过程中对资源的处理

VAInstrumentation.javanewActivity方法片段:

	Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent);
	activity.setIntent(intent);
	try {
		// for 4.1+
		ReflectUtil.setField(ContextThemeWrapper.class, activity, "mResources", plugin.getResources());
	} catch (Exception ignored) {
		// ignored.
	}

文章中提到此处需要重设mResources的原因在ActivityThrea的performLaunchActivity方法(以Android O版本为例)

	private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
		...
		ContextImpl appContext = createBaseContextForActivity(r);
		activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
		...
		int theme = r.activityInfo.getThemeResource();
		if (theme != 0) {
			activity.setTheme(theme);
		}
		...
		if (r.isPersistable()) {
			mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
		} else {
			mInstrumentation.callActivityOnCreate(activity, r.state);
		}
	}

在 createBaseContextForActivity 方法中创建出来的 ContextImpl appContext 使用的是宿主的Resources,如果不进行处理紧接着Activity会走入onCreate的生命周期中,此时插件加载资源的时候还是使用的宿主的资源,而不是我们特意为插件所创建出来的Resources对象,则会发生找不到资源的问题

这里说的有点问题,在走到onCreate之前,框架是hook了Instrumentation.callActivityOnCreate并且重设了mResources等一系列值,其实是可以找到插件的资源的,真正的问题出在上面的几句设置theme的代码,我们知道框架hook了mInstrumentation.newActivity此时返回的已经是插件的Activity实例,而此时给这个插件ActivitysetTheme这个theme是哪个Activity的theme,如果不做任何操作,肯定是占位Activity也就是宿主Application中的默认theme,而框架把这个theme已经换掉了,代码在VAInstrumentationhandleMessage方法中:

	public boolean handleMessage(Message msg) {
		if (msg.what == LAUNCH_ACTIVITY) {
			// ActivityClientRecord r
			Object r = msg.obj;
			try {
				Intent intent = (Intent) ReflectUtil.getField(r.getClass(), r, "intent");
				intent.setExtrasClassLoader(VAInstrumentation.class.getClassLoader());
				ActivityInfo activityInfo = (ActivityInfo) ReflectUtil.getField(r.getClass(), r, "activityInfo");
				if (PluginUtil.isIntentFromPlugin(intent)) {
					int theme = PluginUtil.getTheme(mPluginManager.getHostContext(), intent);
					if (theme != 0) {
						Log.i(TAG, "resolve theme, current theme:" + activityInfo.theme + "  after :0x" + Integer.toHexString(theme));
						activityInfo.theme = theme;
					}
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		return false;
	}

所以如果此时上面的newActivityhook中不去重设mResourcessetTheme方法就会报错。

文章中还提到:

另:如果采用上述所说的AssetManager销毁的方法,则无需在创建Activity后设置Resources对象,因为此处全局都是宿主+插件的资源。

这里简单解释下,虽然每次new一个新的Activity的时候都会产生一个新的ContextImpl,但是这个ContextImpl去生成Resource实例的时候首先会有个取缓存的过程,这个缓存的key一般情况下都是相同的,所以底层也就共用一个AssetManager。可以跟踪下ResourcesManager.javacreateBaseActivityResources方法看下。

gradle插件中对资源的处理

一、VAHostPlugin

宿主的这个plugin是为插件plugin服务的,作用是生成一些文件。与资源处理相关的是备份了宿主的R.txt和生成宿主的依赖列表versions.txt,放在宿主module/build/VAHost

二、VAPlugin

插件的这个plugin针对资源处理相关的操作主要是hook了app编译过程中几个Task,如Merge[Variant]AssetsProcess[Variant]Resources等以达到剔除与宿主共享资源,重设插件中资源id,裁剪resources.arsc等目的,下面我们具体看下这些hook。

  • PrepareDependenciesHooker

hook了pre[Variant]Buildtask,主要目的是过滤插件依赖(aar or jar),将其与宿主共享的依赖和其独有的依赖分开放到若干集合中,用到了上面说到的宿主依赖列表versions.txt。

  • MergeAssetsHooker

hook了Merge[Variant]Assetstask,这个task是用于merge插件依赖的各个aar中的assets,然后会交由AAPT处理,上一步过滤出了插件与宿主共享的aar,那么这些aar中的assets是不需要插件AAPT再处理了,所以需要此hook剔除这些assets。

  • ProcessResourcesHooker

hook了Process[Variant]Resourcestask,这个task最终是调用AAPT去编译资源生成R文件和resources-[Variant].ap_,这个.ap_文件位置在build/intermediates/res下,可以解压出来包含了resources.arsc,编译过的AndroidManifest.xml和资源。这个hook的主要作用就是去修改.ap_文件和R文件。

我们看下ProcessResourcesHooker.groovy中的repackage方法里的一些关键点:

	void repackage(ProcessAndroidResources par, File apFile) {
		...
		resourceCollector = new ResourceCollector(project, par)
		resourceCollector.collect() // ①
		...
		def aapt = new Aapt(resourcesDir, rSymbolFile, androidConfig.buildToolsRevision)
		//Delete host resources, must do it before filterPackage
		aapt.filterResources(retainedTypes, filteredResources)  // ②
		//Modify the arsc file, and replace ids of related xml files
		aapt.filterPackage(retainedTypes, retainedStylealbes, virtualApk.packageId, resIdMap, libRefTable, updatedResources) // ③
		...
		/*
		 * Delete filtered entries and then add updated resources into resources-${variant.name}.ap_
		 */
		com.didi.virtualapk.utils.ZipUtil.with(apFile).deleteAll(filteredResources + updatedResources) // ④
		project.exec {
		    executable par.buildTools.getPath(BuildToolInfo.PathId.AAPT)
		    workingDir resourcesDir
		    args 'add', apFile.path
		    args updatedResources
		    standardOutput = System.out
		    errorOutput = System.err
		} // ⑤
		updateRJava(aapt, par.sourceOutputDir) // ⑥
		...
	}

① 解析宿主和插件的R.txt按照ResType收集资源,并从收集的插件资源集合中过滤出与宿主同名的资源(资源类型和名称相同,参见ResourceEntry.groovyequals())。补充下,R.txt也是AAPT的产物,除了自身的资源也包括了其依赖的aar中的资源,输出位置一般在build/intermediates/symbols下。接着重设资源ID,与宿主共享部分的资源,其ID需要设置成宿主中的值;插件独有的资源需要重新设置,0XPPTTEEEE,PP段设置成我们在插件gradle文件中设置的值,TT和EEEE段按顺序重新赋值。这一步主要生成几个集合供下面的几步使用。

② 遍历从.ap_文件中解压出来的所有资源,将与宿主共享的资源全部删除。

③ 开始修改与裁剪resources.arsc各个chunk段的值,只留下插件独有的资源索引。关于arsc的格式,可以去参照老罗的博客,下面图片来自ArscEditor.groovy中:

arsc

接着将从.ap_文件中解压的已经编译过的xml文件中的引用的资源ID替换成上面新生成的ID值。编译的xml文件格式也可以参考老罗的博客,所有修改的文件(包括arsc,各种xml)都会放到一个集合中。下面便是打包新的.ap_文件。

④ 删除.ap_文件中所有与宿主共享的和所有修改过的资源的原始版本

⑤ 通过aapt add命令将所有修改的资源重新打包到.ap_文件,.ap_文件的修改全部结束。

⑥ 下面便是更新插件的R.java包括其依赖的aar的R.java,将其中的所有ID值更新,这样便能正确索引arsc文件了。这一步还做了一个操作,将插件独有的资源写到一个单独的R.java中,作用会在下面说。到此为止,AAPT的产物都已经修改裁剪完毕,通过重设资源ID的PP段以达到避免ID冲突的问题,这种方式相比修改AAPT的源码兼容性相对好些,VirtualAPK的这种方式参照了Small的实现

  • DxTaskHooker

hook了dex这个Transform,作用是在dex操作之前去覆盖插件app的R.class(不包括其依赖的aar的R.class),只留下插件独有的资源ID,能这么操作是因为插件app编译后的class文件中对R中资源ID的引用都已经编译成对一个常量的引用,如下图(而aar中则是编译成对一个变量的引用,因为aar中ID值在编译时会和主module融合导致其值变化):

app_R

而为了兼容宿主反射调用插件中的独有资源,则要留下插件独有的资源ID。这个hooker我觉得是可以去掉的,不去覆盖R.class也不会有什么影响。

TL;DR

大体VirtualAPK针对资源的处理都梳理了,简单总结下,编译阶段,针对插件apk重设资源ID,运行阶段,宿主加载插件,将插件和宿主的apk添加到同一个AssetManager中,接着构建一个新的Resources去替换原来的Resources,这样通过Context去取资源的时候就能在一个完整的resources.arsc里索引。

No More

Android DataBinding实践

Published on January 27, 2016