@JeromeLiee
2020-02-06T23:00:38.000000Z
字数 7418
阅读 889
可以动态扩展功能,避免频繁地发布版本来更新迭代,随之带来的另外一个好处就是减少了apk包的体积。
/**
* DexClassLoader的构造方法
*
* @param dexPath APK的文件目录
* @param optimizedDirectory 内部存储路径,用来缓存系统创建的Dex文件
* @param librarySearchPath 库文件的存储路径
* @param parent 父ClassLoader
*/
public void DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
}
双亲委派机制
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 首先,检查这个类是否已经加载过
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// c==null,说明没有加载过,那么让父加载器去加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// 如果找到最顶层的ClassLoader都没有加载过,那么就由自己加载
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
...
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
双亲委派机制整个流程:先检查自己有没有加载过该类,如果没有则交由父加载器去加载,如果一直递归到最顶层加载器也没有加载过,则最终由调用自己的findClass()方法加载,这种机制避免了类的重复加载。
// BaseDexClassLoader.java
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException(
"Didn't find class \"" + name + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
// DexPathList.java
public Class<?> findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
Class<?> clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
通过给插件创建一个DexClassLoader便可在宿主中调用插件中的类,有两种处理方式:只创建一个DexClassLoader和为每个插件都单独创建一个DexClassLoader
但宿主调用插件的类,有两种方式:
为每个插件都创建一个对应的DexClassLoader,当加载插件的类时,需要通过对应的DexClassLoader。这样不同的插件的类是隔离的,如果两个插件依赖了同一个第三方库的不同版本,是不会有冲突的。
将插件DexClassLoader中的DexPathList合并到宿主DexClassLoader的DexPathList,好处是在不同的插件中都可以和宿主通过类名互相调用类和方法,并且可以将不同插件的公共模块抽出来放在一个common插件中直接供其他插件使用。Small采用的是这种方式。
Android中资源是通过Resources类加载的,而Resources类是由AssetManager创建的,具体如下:
// ResourcesManager.java #getTopLevelResources()
Resources getTopLevelResources(String resDir, String[] splitResDirs,
String[] overlayDirs, String[] libDirs, int displayId,
Configuration overrideConfiguration, CompatibilityInfo compatInfo) {
Resources r;
...
AssetManager assets = new AssetManager()
// 将apk路径添加到AssetManager中,路径可以是一个目录或zip压缩文件,而apk正好是一个压缩文件
// 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 (resDir != null) {
if (assets.addAssetPath(resDir) == 0) {
return null;
}
}
...
r = new Resources(assets, dm, config, compatInfo);
synchronized (this) {
...
return r;
}
}
因此,只要将插件apk的路径传入AssetManager的addAssetPath()方法中,便可以加载插件的资源文件。
但由于AssetManager的addAssetPath()方法是hide类型不对外开放,所以需要使用反射来调用。
要加载插件中的资源,需传入插件的路径,最终生成Resources对象,同类的处理一样,资源的处理也有两种方式:
合并式由于添加了宿主和所有插件的资源路径,因此生成的Resources在宿主和插件中都能访问所有的资源文件。但由于宿主和插件是分别编译的,所以资源id可能会产生冲突。
独立式由于各自生成了宿主和插件的Resources,所以资源是隔离的不会有冲突,但想要互相访问彼此的资源文件,需要持有彼此的Resources对象。
我们平时访问资源的时候,都是通过Context获取Resources对象,那么就需要对Context里的mResources进行替换,这里以VirtualApk为例:
第一步:创建Resources对象
if (Constants.COMBINE_RESOURCES) {
// 创建插件的Resources并与宿主的Resources合并,生成一个新的Resources对象
Resources resources = ResourcesManager.createResources(context, apk.getAbsolutePath());
// 插件和主工程资源合并时需要hook住主工程的资源
ResourcesManager.hookResources(context, resources);
return resources;
} else {
// 插件资源独立,该resource只能访问插件自己的资源
Resources hostResources = context.getResources();
AssetManager assetManager = createAssetManager(context, apk);
return new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());
}
VirtualApk支持合并式和独立式资源路径处理。
第二步:替换Context中的成员变量mResources
public static void hookResources(Context base, Resources resources) {
try {
// 替换Context中的mResources
ReflectUtil.setField(base.getClass(), base, "mResources", resources);
Object loadedApk = ReflectUtil.getPackageInfo(base);
// 替换LoadedApk中的mResources
ReflectUtil.setField(loadedApk.getClass(), loadedApk, "mResources", resources);
Object activityThread = ReflectUtil.getActivityThread(base);
Object resManager = ReflectUtil.getField(activityThread.getClass(), activityThread, "mResourcesManager");
if (Build.VERSION.SDK_INT < 24) {
Map<Object, WeakReference<Resources>> map = (Map<Object, WeakReference<Resources>>) ReflectUtil.getField(resManager.getClass(), resManager, "mActiveResources");
Object key = map.keySet().iterator().next();
map.put(key, new WeakReference<>(resources));
} else {
// still hook Android N Resources, even though it's unnecessary, then nobody will be strange.
Map map = (Map) ReflectUtil.getFieldNoException(resManager.getClass(), resManager, "mResourceImpls");
Object key = map.keySet().iterator().next();
Object resourcesImpl = ReflectUtil.getFieldNoException(Resources.class, resources, "mResourcesImpl");
map.put(key, new WeakReference<>(resourcesImpl));
}
} catch (Exception e) {
e.printStackTrace();
}
}
上述代码hook了三个点:
第三步:关联Activity和Resources
Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent); activity.setIntent(intent);
// 设置Activity的mResources属性,Activity中访问资源时都通过mResources
ReflectUtil.setField(ContextThemeWrapper.class, activity, "mResources", plugin.getResources());
在Activity生成后替换其父类ContextThemeWrapper的成员变量mResources,这样在插件Activity中直接可以通过getResources获取Resources对象。
接下来就可以在插件的Activity中使用setContentView、inflate等方法来加载布局了。
在合并式的资源处理方式中,很大概率会产生资源冲突,因为不同的apk编译打包过程中,资源id名称可能会相同。那么解决冲突就是让宿主和插件的资源id保持不同。
资源id由8位16进制表示,格式为0xPPTTNNNN,其中PP段用来区分包空间,TT段用来区分资源类型,NNNN段是在apk里从0000开始递增。
那么要保证宿主和插件的资源id不一致,我们一般修改PP段的值即可,有如下两种方式:
由于第一种修改AAPT工具侵入性比较大,所以推荐使用第二种,可参考 插件化-解决插件资源ID与宿主资源ID冲突的问题
经过上述步骤后,便实现了插件Activity的启动,并且该插件Activity中并不需要什么额外的处理,和常规的Activity一样。那问题来了,之后的onResume,onStop等生命周期怎么办呢?答案是所有和Activity相关的生命周期函数,系统都会调用插件中的Activity。原因在于AMS在处理Activity时,通过一个token表示具体Activity对象,而这个token正是和启动Activity时创建的对象对应的,而这个Activity被我们替换成了插件中的Activity,所以之后AMS的所有调用都会传给插件中的Activity。
四大组件中Activity的支持是最复杂的,其他组件的实现原理要简单很多,简要概括如下:
Service:Service和Activity的差别在于,Activity的生命周期是由用户交互决定的,而Service的生命周期是我们通过代码主动调用的,且Service实例和manifest中注册的是一一对应的。实现Service插件化的思路是通过在manifest中预埋StubService,hook系统startService等调用替换启动的Service,之后在StubService中创建插件Service,并手动管理其生命周期。
BroadCastReceiver:解析插件的manifest,将静态注册的广播转为动态注册。
ContentProvider:类似于Service的方式,对插件ContentProvider的所有调用都会通过一个在manifest中占坑的ContentProvider分发。