这篇文章主要记录了在使用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版本中的关系如下图所示。
0x03 Small框架插件资源加载方案 Small框架的资源加载流程在ApkBundleLauncher中完成,setup
流程获取到所有插件so的信息,在postSetUp
中获取所有插件包的资源路径,通过反射调用AssetManager的addAssetPaths
方法,构造一个包含宿主包资源、系统资源和插件包资源的AssetManger。最后还是通过反射,使用包含所有资源的AssetManager替换掉ResourcesManager中Resources的AssetManger,最终达到加载插件中资源的目的。
Small框架资源加载流程:
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)); } } }
这里有两个问题:
SDK<24时,在原生的Android系统中并不是每启动一个Activity都会创建一个新的Resources实例,ResourcesManager会使用缓存的Resources实例,所以只需要Hook一次资源加载。但是一旦创建多个Resources实例时,是不是意味着新创建的Resources并会包含插件的资源路径。个人理解是这样的。这应该也能解释为啥Small框架会在某些手机的分屏模式 和某些横竖屏切换 的时候会发生Crash,详情请查看#356 和#548
SDK>24时,Small会执行ensureCacheResources
希望将新创建的ResourcesImpl的AssetsManger替换掉。但是看到源码中的实现方式是,通过反射为ActivityThread的mH
Handler注入一个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 。🙏🙏🙏