安卓插件化方案调研一:任玉刚dynamic_load_apk方案

动态加载

常规情况下,android应用apk必须安装到手机(/system/app或/data/app/目录)才能加载使用。但是通过动态加载技术,可以将未安装的apk(如/sdcard/目录)加载使用。

意义

一是解决Android系统的方法书限制:一个dex中最多只能有65535个方法数。针对该限制,可以通过hack方法扩大一些LinearAllocHdr分配空间(5M提升到8M),但该方法治标不治本。还有一种方案就是动态加载方案,通过调整架构,拆分dex,可在程序运行中动态加载插件。
二是提升编译速度。工程被拆分成了数个子工程,Android Studio编译速度可提升。
三是启动速度的提升。Google提供的MultiDex方案,会在主线程中执行所有dex的解压、dexopt、加载操作,这是一个非常漫长的过程,用户会明显的看到长久的黑屏,更容易造成主线程的ANR,导致首次启动初始化失败。
四是减少apk的size,可以根据需要,用户在使用过程中按需下载所需模块。
五是可以为Native客户端提供热部署能力

适用范围

用于改进大型App的架构,实现多团队协作开发,实现app的瘦身。但引入的缺点就是增加产品复杂性,降低代码复用率,不适合小团队开发。

方案

目前的开源技术方案,主要是以下4个:

还有一个商业收费的方案APKPlug

滴滴也公布了自己的插件方案,源码地址 https://github.com/didi/VirtualAPK

技术演进图

技术演进.png

任玉刚DL开源框架

DynamicLoadApk 原理的核心思想可以总结为两个字:代理。通过在宿主 Manifest 中注册代理组件来实现插件组件的启动。当宿主启动插件组件时,通过优先启动的代理组件来构建、启动真正的插件组件。

核心概念

  • 宿主:主App,也称为Host。可以动态加载插件扩充能力。
  • 插件:插件 App,也称 Plugin,提供独立的能力。可以被宿主加载,也可以跟普通 App 一样独立运行。
  • 组件:指 Android 中的Activity、Service、BroadcastReceiver、ContentProvider四大组件,目前 DL仅 支持Activity、Service以及动态的BroadcastReceiver。
  • 插件组件:插件中的android组件。
  • 代理组件:在宿主的 Manifest 中注册,启动插件组件时首先被启动的组件。目前包括 DLProxyActivity(代理 Activity)、DLProxyFragmentActivity(代理 FragmentActivity)、DLProxyService(代理 Service)。
  • Base 组件:插件组件的基类,目前包括 DLBasePluginActivity(插件 Activity 的基类)、DLBasePluginFragmentActivity(插件 FragmentActivity 的基类)、DLBasePluginService(插件 Service 的基类)。
设计方案
image.png

上面是 DynamicLoadApk 的总体设计图,DynamicLoadApk 主要分为四大模块:
(1)DLPluginManager
插件管理模块,负责插件的加载、管理以及启动插件组件。内部对象DLPluginPackage包含了插件的DexClassLoader,AssetManager、Resource、PackageInfo等信息
(2) Proxy
代理组件模块,目前包括 DLProxyActivity(代理 Activity)、DLProxyFragmentActivity(代理 FragmentActivity)、DLProxyService(代理 Service)。
(3)Proxy Impl
代理组件公用逻辑模块,与(2)中的 Proxy 不同的是,这部分并不是一个组件,而是负责构建、加载插件组件的管理器。这些 Proxy Impl 通过反射得到插件组件,然后将插件组件与 Proxy 组件建立关联,最后调用插件组件的 onCreate 函数进行启动。
(4)Base Plugin
插件组件的基类模块,目前包括 DLBasePluginActivity(插件 Activity 的基类)、DLBasePluginFragmentActivity(插件 FragmentActivity 的基类)、DLBasePluginService(插件 Service 的基类)。
通过使用that(当插件apk单独安装使用时指向当前Activity的this对象;当插件apk通过host动态加载时指向proxyActivity的this对象)来保证获取context对象,以及插件apk的独立运行。

流程图
image.png

上面是调用插件 Activity 的流程图,其他组件调用流程类似。启动的关键包括如下三步:
1)通过 DLPluginManager 的 loadApk 函数初始化插件加载环境,这步每个插件只需调用一次。
2)通过 DLPluginManager 的 startPluginActivity 函数启动代理 Activity。
3)代理 Activity 启动过程中构建、启动、并attach插件 Activity,运行中传播生命周期等事件到插件Activity。

LoadApk初始化插件加载环境流程
image.png

注意事项:
1)将插件的DexClassLoader,AssetManager,Resources等信息存入DLPluginPackage对象,便于后续运行时加载插件,替换插件资源
2)将已初始化的插件加载环境对象DLPluginPackage放入HashMap类型的管理器mPackagesHolder,以保证不重复进行初始化操作。
3)So对象只能放到/data/data/目录下加载。

startPluginActivity启动插件组件流程
image.png

注意事项:
1)只支持显示Intent启动,将待启动的插件packageName,组件名作为参数来启动。
2)startPluginActivityForResult的流程是一样的。
3)通过Class.forName来动态加载插件类

DLProxyActivity的初始化流程

DLProxyActivity拥有两个核心对象:1)DLPlugin类型的mRemoteActivity对象,标志Attach到当前代理的插件Activity。2)DLProxyImpl类型的impl对象,来获取插件的资源及classloader。

其中,DLPlugin是一个接口,定义了Activity运行中的各种生命周期、触摸事件、菜单等函数。所有的插件Activity都实现了DLPlugin接口,避免了通过反射了操作插件Activity对象。DLProxyImpl封装了插件Activity的内在逻辑,如初始化插件Activity,Attach到代理Activity,运行中获取插件Activity的资源等。DLProxyImpl关键的onCreate初始化操作流程如下:

image.png

注意事项:
1)代理Activity,是真正在宿主Manifest中注册的组件。也是启动插件Activity时,被Android系统启动的Activity,拥有完整的生命周期。
2)通过代理Activity避免了插件Activity的注册,隔离了变化。
3)主题资源部分的修改,在兼容性方面要尤其关注测试。如三星系列的机型

关键代码
createClassLoader

使用DexClassLoader来加载插件类。

DexClassLoader loader = new DexClassLoader(dexPath, dexOutputPath, mNativeLibDir, mContext.getClassLoader());
createAssetManager

在正常情况下,通过R只能获取代理组件内部的资源,因为运行中的是代理组件的Context。为了获取插件资源,结合AssetManager的源码,最终做了如下改动:

protected void loadResources() {  
    try {  
        AssetManager assetManager = AssetManager.class.newInstance();  
        Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);  
        addAssetPath.invoke(assetManager, mDexPath);  
        mAssetManager = assetManager;  
    } catch (Exception e) {  
        e.printStackTrace();  
    }  
    Resources superRes = super.getResources();  
    mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(), 
            superRes.getConfiguration());  
    mTheme = mResources.newTheme();  
    mTheme.setTo(super.getTheme());  
}  

通过反射调用AssetManager中的addAssetPath方法,可以将插件apk中的资源加载到Resources中。而在代理组件中通过重写getAssets和getResources方法来保证调用插件资源。

@Override  
public AssetManager getAssets() {  
    return mAssetManager == null ? super.getAssets() : mAssetManager;  
}  
  
@Override  
public Resources getResources() {  
    return mResources == null ? super.getResources() : mResources;  
}  
插件Activity生命周期的管理

通过将Activity的生命周期方法提取成一个接口DLPlugin,插件Activity实现该接口。并在插件Activity创建时类型转化为DLPlugin对象,然后在代理Activity的生命周期方法中调用插件Activity实现的生命周期方法,避免采用反射来管理插件Activity的生命周期。

public interface DLPlugin {  
  
    public void onStart();  
    public void onRestart();  
    public void onActivityResult(int requestCode, int resultCode, Intent data);  
    public void onResume();  
    public void onPause();  
    public void onStop();  
    public void onDestroy();  
    public void onCreate(Bundle savedInstanceState);  
    public void setProxy(Activity proxyActivity, String dexPath);  
    public void onSaveInstanceState(Bundle outState);  
    public void onNewIntent(Intent intent);  
    public void onRestoreInstanceState(Bundle savedInstanceState);  
    public boolean onTouchEvent(MotionEvent event);  
    public boolean onKeyUp(int keyCode, KeyEvent event);  
    public void onWindowAttributesChanged(LayoutParams params);  
    public void onWindowFocusChanged(boolean hasFocus);  
    public void onBackPressed();  
}  

在代理类DLProxyActivity中的调用:其中mRemoteActivity就是DLPlugin的实现,真正的插件Activity对象。

1....  
2.    @Override  
3.    protected void onStart() {  
4.        mRemoteActivity.onStart();  
5.        super.onStart();  
6.    }  
7.  
8.    @Override  
9.    protected void onRestart() {  
10.        mRemoteActivity.onRestart();  
11.        super.onRestart();  
12.    }  
13.  
14.    @Override  
15.    protected void onResume() {  
16.        mRemoteActivity.onResume();  
17.        super.onResume();  
18.    }  
19.  
20.    @Override  
21.    protected void onPause() {  
22.        mRemoteActivity.onPause();  
23.        super.onPause();  
24.    }  
25.  
26.    @Override  
27.    protected void onStop() {  
28.        mRemoteActivity.onStop();  
29.        super.onStop();  
30.    }  
31....  
插件与宿主的通信

目前DL作者指出,DL框架支持以下三种通信方式:

image.png

其中,模式1:这是DL推荐的模式,对应的工程目录为main。在这种模式下,宿主和插件不需要通信,两者是独立开发的,宿主引用DL的jar包(dl-lib.jar),插件也需要引用DL的jar包,但是不能放入到插件工程的libs目录下面。该模式实际测试OK
模式2:插件部分依赖宿主的模式,或者说插件依赖宿主提供的接口,适合能够拿到宿主的接口的情况。在这种模式下,宿主放出一些接口并实现一些接口,然后给插件调用,这样插件就可以访问宿主的一些服务等。该模式实际测试OK。
模式3:插件完全依赖宿主的模式,适合于能够能到宿主的源代码的情况。这种模式一般多用在公司内部,插件可以访问宿主的所有代码,但是,这样插件和宿主的耦合比较高,宿主一动,插件就必须动,比较麻烦。目前该模式我还未调通。
具体采用哪种方式,需要结合实际情况来选择,一般来说,如果是宿主和插件不是同一个公司开发,建议选择模式1和模式2;如果宿主和插件都在同一个公司开发,那么选择哪个都可以。从DL的实现出发,我们推荐采用模式1,真的需要通信的话采用模式2,尽量不要采用模式3.

DL对插件独立运行的支持

为了便于调试,采用DL所开发的插件都可以独立运行,当然,这要分情况来说:
对于模式1,如果插件想独立运行,只需要把external-jars下的jar包拷贝一份到插件的libs
目录下即可。
对于模式2,只需要提供一个宿主接口的默认实现即可。
对于模式3,只需要apk打包时把所引用的宿主代码打包进去即可,具体方式可以参看
sample/depend_on_host目录。未测试通过

在开发过程中,应该先开启插件的独立运行功能以便于调试,等功能开发完毕后再将其插件化。

总结

方案优点:1)插件apk不需安装即可被宿主运行;2)通过proxy实现插件Activity的生命周期管理;3)支持直接通过R来访问插件资源;4)插件可独立运行,单独开发调试。
方案缺点:1)不支持静态注册的广播;2)插件不能使用this,而是自定义的that;3)插件资源id和宿主资源不能重复;4)不支持自定义主题,透明主题;5)不支持ContentProvider;6)对插件so的处理有异常(需测试)。

参考文章:

1):DynamicLoadApk源码解析[http://a.codekk.com/detail/Android/FFish/DynamicLoadApk%20%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90](http://a.codekk.com/detail/Android/FFish/DynamicLoadApk æº�ç �解æ��)
2)DL动态加载框架技术文档:http://blog.csdn.net/singwhatiwanna/article/details/40283117
3)Android中的动态加载机制:http://blog.csdn.net/jiangwei0910410003/article/details/17679823

推荐阅读更多精彩内容

  • 最近几年移动开发业界兴起了「 插件化技术 」的旋风,各个大厂都推出了自己的插件化框架,各种开源框架都评价自身功能优...
    斜杠Allen阅读 3,166评论 1 36
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 157,630评论 24 688
  • 引言 先简单介绍一下Android插件化。很早之前已经有公司在研究这项技术,淘宝做得比较早,但淘宝的这项技术一直是...
    流水潺湲阅读 9,337评论 8 148
  • 本文转自:Android博客周刊专题之#插件化开发# 原文作者:陆镇生_Jomeslu 本人最近研究插件化, 偶然...
    Aegis阅读 33,543评论 25 406
  • 我似傻瓜如浮云, 任凭风吹雨打滚, 天下富贵各有主, 唯我独自去沉沦。
    他说这不是诗阅读 67评论 0 0