/ Android

关于Android如何动态加载res

最近抽时间看了一下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里,读取名为testdrawable资源

      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的初衷,就是作为一个自治单元)

另外目前也有一些成型的插件化框架可以直接使用,如

http://www.apkplug.com/

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

DL : Apk动态加载框架@Github

APK动态加载框架(DL)解析

Android apk动态加载机制的研究

@tabolt

获取未安装资源apk种的资源文件

@jefferyyangkai

qq游戏大厅中解析不安装apk的研究