关于Android如何动态加载res
Posted
最近抽时间看了一下android插件化的东西。
也稍微研究了一下。总结一下权当记录。
同时也把研究过程中的testbed上传到github了,造福后人吧。
####为什么需要插件化?
最简单的原因大概就是为了避免过大的体积(rom size),以加速下载和安装。
更多的原因例如插件化后可以单独升级避免每次一个小改动就app体量的升级,为了针对不同区域不同用户做不同的适配,为了市场更高的单app评分(插件的差评不算主app中)等等。
####哪些可以插件化?
大部分的功能性代码/资源及非自定义的布局
####可用插件化方案: 这里仅讨论:以apk(在插件化的前提下,相比较jar,apk要灵活的多)的形式插件化的方案。
这里不讨论:自行定义数据格式,并自行实现解析器(parser或LayoutInflater)的方案。(因为造这种轮子总让我想起曾经一个叫XXUI的垃圾框架。。。)
-
已安装插件apk的情况下:
-
读取静态资源(如png/jpg等) 可以通过AIDL的方式直接获取,这里不赘述。
-
读取并执行功能函数: 同上。参数不可序列化的情况,可参考下面“不安装插件apk的情况”
-
读取布局 简单布局同上,自定义布局不确定是否可行(不过已经安装了,这就不重要了)。
-
直接启动activity 因为已安装,所以直接intent去start acitivty即可。
-
-
未安装插件apk的情况下:
-
读取静态资源(如png/jpg等)
例如从包名
com.example.plugin
里,读取名为test
的drawable
资源String libPath = Environment.getExternalStorageDirectory() + "/Plugin.apk"; try { AssetManager assetManager = AssetManager.class.newInstance(); Method addAssetPath = assetManager.getClass().getMethod( "addAssetPath", String.class); addAssetPath.invoke(assetManager, libPath); mAssetManager = assetManager; } catch (Exception e) { e.printStackTrace(); } Resources superRes = super.getResources(); mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(), superRes.getConfiguration()); Drawable temp = mResources.getDrawable(mResources.getIdentifier("test", "drawable", "com.example.plugin"));
-
读取并执行功能函数:
例如从卡上的
Plugin.apk
中,读取com.example.plugin.MainActivity
中的一个String md5(String path)
的函数并执行:DexClassLoader mClassLoader = null; String libPath = Environment.getExternalStorageDirectory() + "/Plugin.apk"; File tmpDir = getDir("dex", 0); mClassLoader = new DexClassLoader(libPath, tmpDir.getAbsolutePath(), null, getClassLoader()); Class classToLoad = mClassLoader .loadClass("com.example.plugin.MainActivity"); Object myInstance = classToLoad.newInstance(); Method[] ms = classToLoad.getDeclaredMethods(); Method md5 = classToLoad.getMethod("md5", String.class); String xxx = (String) md5.invoke(myInstance, libPath);
-
读取布局
例如从卡上的
Plugin.apk
中,读取com.example.plugin
中的一个test_layout
的xml并inflate成view:这里需要自己generate一个假的Context,代码较多,可以直接参考android_load_res_dynamically里TestApk里的TargetContext(为了这个还建一个工程也是醉了。。。),有几个关键的函数需要重载掉:
- getSystemService
- getResources
- getAssets
- getClassLoader
- getTheme
- getPackageName
具体的参考TargetContext就好了。然后类似读取资源的时候,只不过context换成了target context:
LayoutInflater inflater = (LayoutInflater) targetContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); return inflater.inflate(mResource.getLayout(mResources.getIdentifier("test_layout", "id", "com.example.plugin")), null); ``` 这里也尝试了一下加载自定义的view(如继承自ImageView的),但是失败了(理论上Context提供的classloader正确就应该可行,但是会报错。没有继续深入。) * 直接启动activity 可以参考https://github.com/singwhatiwanna/dynamic-load-apk。
-
综上,要加载插件化的资源/函数/activity等,对于已安装的apk可以用aidl,intent等各种系统提供的“普遍”方法;未安装的apk就是用反射去先获取到Resource和Asset等Context,再加载。
个人认为,不管apk是安装还是不安装,都不建议单独插件化布局(毕竟涉及界面交互(如点击/回调等),设置个listener都很麻烦)。
而对于涉及界面交互的插件(其实算功能),建议使用“直接启动activity”的方式。
Start activity虽然会造成界面跳转之间的闪烁和卡顿,但是相比较生硬的反射,不管是从实现复杂度还是从代码可维护性上来说,都是利大于弊的;更不用说用了反射(依赖name),代码混淆就比较尴尬了。 (BTW,Android设计activity的初衷,就是作为一个自治单元)
另外目前也有一些成型的插件化框架可以直接使用,如
https://github.com/singwhatiwanna/dynamic-load-apk
另外注意一点,上面的code必须要有卡读写权限(其实文档建议是最好别放外部存储上):
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
差不多就这些了~ 有遗漏和更新再补充。
本文参考了各位前人的经验(排名不分先后):
@Siddharth Naik
Load resource (layout) from another apk dynamically
@singwhatiwanna
@tabolt
@jefferyyangkai