Android插件化之资源加载机制

这篇文章主要记录了在使用Small插件化框架中遇到的资源加载问题及相应解决方案,并梳理出Android的资源加载流程和插件化框架的资源加载原理。在前两篇插件化技术介绍的基础上会关注更多技术细节,希望能有所收获!

关于Small插件化的其他文档:

《Android插件化之Small框架实践总结》
《Android插件化之Small框架原理》
《Android插件化之从入门到放弃》

0x01 Small框架的资源加载异常

最近收到一个客户反馈,在他们的中兴V0840手机上打开我们的app会持续崩溃。第一时间在百度移动质量平台上短时租用了该机型,抓取了log。发现是资源查找失败异常。并在Small github issues中搜索android.content.res.Resources$NotFoundException 可以发现很多类似的问题,详细日志可查看下图。

Github issus链接:#555 Small Sample项目打包后在ZTE上闪退

项目崩溃日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
03-06 17:48:31.685 E/AndroidRuntime( 8189): FATAL EXCEPTION: main
03-06 17:48:31.685 E/AndroidRuntime( 8189): Process: com.shandiangou.kaguanjia, PID: 8189
03-06 17:48:31.685 E/AndroidRuntime( 8189): java.lang.RuntimeException: Unable to start activity ComponentInfo{com.shandiangou.kaguanjia/com.shandiangou.kaguanjia.app.main.activity.GuideActivity}: android.content.res.Resources$NotFoundException: Resource ID #0x2a030010
03-06 17:48:31.685 E/AndroidRuntime( 8189): at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2669)
03-06 17:48:31.685 E/AndroidRuntime( 8189): at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2730)
03-06 17:48:31.685 E/AndroidRuntime( 8189): at android.app.ActivityThread.-wrap12(ActivityThread.java)
03-06 17:48:31.685 E/AndroidRuntime( 8189): at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1481)
03-06 17:48:31.685 E/AndroidRuntime( 8189): at android.os.Handler.dispatchMessage(Handler.java:102)
03-06 17:48:31.685 E/AndroidRuntime( 8189): at android.os.Looper.loop(Looper.java:154)
03-06 17:48:31.685 E/AndroidRuntime( 8189): at android.app.ActivityThread.main(ActivityThread.java:6144)
03-06 17:48:31.685 E/AndroidRuntime( 8189): at java.lang.reflect.Method.invoke(Native Method)
03-06 17:48:31.685 E/AndroidRuntime( 8189): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
03-06 17:48:31.685 E/AndroidRuntime( 8189): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)
03-06 17:48:31.685 E/AndroidRuntime( 8189): Caused by: android.content.res.Resources$NotFoundException: Resource ID #0x2a030010
03-06 17:48:31.685 E/AndroidRuntime( 8189): at android.content.res.ResourcesImpl.getValue(ResourcesImpl.java:196)
03-06 17:48:31.685 E/AndroidRuntime( 8189): at android.content.res.Resources.loadXmlResourceParser(Resources.java:2101)
03-06 17:48:31.685 E/AndroidRuntime( 8189): at android.content.res.Resources.getLayout(Resources.java:1115)
03-06 17:48:31.685 E/AndroidRuntime( 8189): at android.view.LayoutInflater.inflate(LayoutInflater.java:424)
03-06 17:48:31.685 E/AndroidRuntime( 8189): at android.view.LayoutInflater.inflate(LayoutInflater.java:377)
03-06 17:48:31.685 E/AndroidRuntime( 8189): at com.shandiangou.kaguanjia.common.base.CustomProgressDialog.init(CustomProgressDialog.java:38)
03-06 17:48:31.685 E/AndroidRuntime( 8189): at com.shandiangou.kaguanjia.common.base.CustomProgressDialog.<init>(CustomProgressDialog.java:26)
03-06 17:48:31.685 E/AndroidRuntime( 8189): at com.shandiangou.kaguanjia.common.base.BaseActivity.initProgressDialog(BaseActivity.java:27)
03-06 17:48:31.685 E/AndroidRuntime( 8189): at com.shandiangou.kaguanjia.common.base.BaseActivity.onCreate(BaseActivity.java:22)
03-06 17:48:31.685 E/AndroidRuntime( 8189): at com.shandiangou.kaguanjia.app.main.activity.GuideActivity.onCreate(GuideActivity.java:45)
03-06 17:48:31.685 E/AndroidRuntime( 8189): at android.app.Activity.performCreate(Activity.java:6722)
03-06 17:48:31.685 E/AndroidRuntime( 8189): at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1119)
03-06 17:48:31.685 E/AndroidRuntime( 8189): at net.wequick.small.ApkBundleLauncher$InstrumentationWrapper.callActivityOnCreate(ApkBundleLauncher.java:334)
03-06 17:48:31.685 E/AndroidRuntime( 8189): at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2622)
03-06 17:48:31.685 E/AndroidRuntime( 8189): ... 9 more
03-06 17:48:31.688 W/ActivityManager( 1384): Force finishing activity com.shandiangou.kaguanjia/net.wequick.small.A

Small框架官方Sample崩溃日志

0x02 Android资源加载流程

Android源码Resources创建流程图:

ActivityThread在接收到LAUNCH_ACTIVITY消息以后,在 performLaunchActivity 方法中,使用Instrumentation通过反射的方式创建Activity实例,再创建Activity的Base Context, 并在创建Context过程中实例化AssetManger和Resources。
ActivityThread在LAUNCH_ACTIVITY消息中,完成了Activity生命周期中的三个回调,分别是onCreate onStart onRestoreInstanceState

Android中资源管理类在不同sdk版本中的关系如下图所示。

Android源码资源类图

0x03 Small框架插件资源加载方案

Small框架的资源加载流程在ApkBundleLauncher中完成,setup流程获取到所有插件so的信息,在postSetUp中获取所有插件包的资源路径,通过反射调用AssetManager的addAssetPaths方法,构造一个包含宿主包资源、系统资源和插件包资源的AssetManger。最后还是通过反射,使用包含所有资源的AssetManager替换掉ResourcesManager中Resources的AssetManger,最终达到加载插件中资源的目的。

Small框架资源加载流程:

Android源码资源类图

0x04 bug修复方案

看完Small插件资源加载流程,你是否有疑问?Small只在框架加载时对ResourcesManager进行了hook,好像在创建新的Resources并没有进行hook操作?那么当系统新创建Resources实例时,新的Resources中包含的资源路径并没有插件资源,这好像说不通吧。其实关注Small的源码中ReflectAccelerator.ensureCacheResources,这个方法想要的达到的作用是当每次启动Activity时遍历系统缓存的ResourceImpl,将它的AssetManager替换成包含插件资源的AssetManager。当然这个机制只在SDK>=24时生效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void ensureCacheResources() {
if (Build.VERSION.SDK_INT < 24) return;
if (sResourceImpls == null || sMergedResourcesImpl == null) return;

Set<?> resourceKeys = sResourceImpls.keySet();
for (Object resourceKey : resourceKeys) {
WeakReference resourceImpl = (WeakReference)sResourceImpls.get(resourceKey);
if (resourceImpl != null && resourceImpl.get() != sMergedResourcesImpl) {
// Sometimes? the weak reference for the key was released by what
// we can not find the cache resources we had merged before.
// And the system will recreate a new one which only build with host resources.
// So we needs to restore the cache. Fix #429.
// FIXME: we'd better to find the way to KEEP the weak reference.
sResourceImpls.put(resourceKey, new WeakReference<Object>(sMergedResourcesImpl));
}
}
}

这里有两个问题:

  1. SDK<24时,在原生的Android系统中并不是每启动一个Activity都会创建一个新的Resources实例,ResourcesManager会使用缓存的Resources实例,所以只需要Hook一次资源加载。但是一旦创建多个Resources实例时,是不是意味着新创建的Resources并会包含插件的资源路径。个人理解是这样的。这应该也能解释为啥Small框架会在某些手机的分屏模式和某些横竖屏切换的时候会发生Crash,详情请查看#356#548
  2. SDK>24时,Small会执行ensureCacheResources希望将新创建的ResourcesImpl的AssetsManger替换掉。但是看到源码中的实现方式是,通过反射为ActivityThread的mHHandler注入一个Handler.Callback。当HandlerCallback handleMessage LAUNCH_ACTIVITY消息时,执行ensureCacheResources方法。查看Handler的dispatchMessage 发现mCallback.handleMessage是先于mHandler.handleMessage的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Handle system messages here.
*/
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}

查看 0x02 Android资源加载流程 的资源流程,你会发现Resources对象的实例化并将ResourcesImpl添加到ResourcesManger的缓存列表中是在Handler.handleMessage之后的。所以ensureCacheResources并不能保证启动Activity时新创建的ResourcesImpl实例能够被正常hook的!!

综上所述,这就是文章开头中兴手机Android7.1系统的手机上使用Small框架会发生Crash问题的原因,因为中兴系统每次打开新的Activity都会创建一个新的Resources和ResourcesImpl实例,而这些都是没有被hook的,不包含插件资源路径,自然就会发生资源查找失败的异常。解决方法也比较简单,因为是SDK>24的机器,只需要在Small框架的InstrumentationWrapper.callActivityOnCreate方法中执行ReflectAccelerator.ensureCacheResources()就可以解决上面的问题了。

同时你需要注意另一个问题,查看ActivityThread的源码,在启动Activity流程的performLaunchActivity方法中,在mInstrumentation.callActivityOnCreate之前系统会为Activity设置主题。如果你选择在mInstrumentation.callActivityOnCreate中执行Resources的hook,并且此时需要的主题资源恰好在插件中,那依然会发生Crash。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
....
....省略其他代码
int theme = r.activityInfo.getThemeResource();
if (theme != 0) {
activity.setTheme(theme);
}

activity.mCalled = false;
if (r.isPersistable()) {
mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
} else {
mInstrumentation.callActivityOnCreate(activity, r.state);
}
....省略其他代码
....
}

这里我的建议是把你项目中所有的主题定义都放在宿主中,并且修改Small的框架代码在InstrumentationWrapper.callActivityOnCreate方法中执行ReflectAccelerator.ensureCacheResources方法,这样就可以解决Small框架在某些场景下发生Resources$NotFoundException异常的问题。

0x05 完

Small插件化框架是我在项目中使用的框架,他的设计和实现思路上都非常优雅,是首选的轻量级插件化框架。以上分析只是对Android源码和Small框架的个人理解,如有理解有误的地方还望指出,个人微信号:tykYang,邮箱:yangfan3687@163.com。🙏🙏🙏

转载请标明出处病已blog https://ivonhoe.github.io/