VirtualAPK:滴滴 Android 插件化的实践之路
2016-12-25 22:54 阅读(315)

作者简介: 任玉刚,滴滴出行 Android 技术专家,《Android 开发艺术探索》作者,插件化框架 dynamic-load-apk 的发起者,CSDN 移动开发博客专家,曾当选 CSDN 2014、2015年度十大博客之星。热爱技术,热爱开源,凡事喜欢刨根问底,长期活跃在 CSDN 和 GitHub。目前就职于滴滴出行 App 架构组,从事热修复和插件化相关的开发工作。 
博客地址:http://blog.csdn.net/singwhatiwanna; 
GitHub:https://github.com/singwhatiwanna。 
本文为 CSDN 首发,欢迎技术投稿、约稿,给文章纠错,请发送邮件至mobile@csdn.net

一、前言

在 Android 插件化技术日新月异的今天,开发并落地一款插件化框架到底是简单还是困难,这个问题不同人会有不同的答案。但是我相信,完成一个插件化框架的 Demo 并不是多难的事儿,然而要开发一款完善的插件化框架却并非易事,尤其在国内,各大 ROM 厂商都对 Android 系统做了一定程度的定制,这更进一步加剧了 Android 本身的碎片化问题。

滴滴出行在插件化上的探索起步较晚,由于业务发展较快,迭代占据了大量的时间,这使得我们在2016年才开始研究这方面的技术。经过半年的开发、测试、适配和线上验证,今天,我们正式推出一款较为完善的插件化框架——VirtualAPK。之所以现在推出,是因为 VirtualAPK 在内部已经得到了很好的验证,我们在迭代过程中不断地做机型适配和细节特性的支持,目前已达到一个非常稳定的状况,足以支撑滴滴部分乃至全部业务的动态发版需求。目前滴滴出行最新版本(v5.0.4)上,小巴和接送机业务均为插件,大家可以去体验。

二、插件化的现状

到目前为止,业界已经有很多优秀的开源项目,比如早期的基于静态代理思想的 DynamicLoadApk,随后的基于占坑思想的 DynamicApk、Small,还有360手机助手的 DroidPlugin。它们都是优秀的开源项目,很大程度上促进了国内插件化技术的发展。

尽管有如此多的优秀框架存在,但是兼容性问题仍然是制约插件化发展的一大难题。一款插件化框架,也许可以在一款手机上完美运行,但是在数以千万的设备上却总是容易存在这样那样的兼容性问题。我相信上线过插件化的工程师应该深有体会。滴滴为什么还要自研一款新的插件化框架?因为我们需要一款功能完备、兼容性优秀、适用于滴滴业务的插件化框架,目前市面上的开源不能满足我们的需求,所以必须重新造轮子,于是 VirtualAPK 诞生了。

三、VirtualAPK 的诞生

VirtualAPK 是滴滴出行自研的一款优秀的插件化框架,主要有如下几个特性。

1. 功能完备

2. 优秀的兼容性

3. 入侵性极低

四、VirtualAPK 的工作过程

VirtualAPK 对插件没有额外的约束,原生的 apk 即可作为插件。插件工程编译生成 apk 后,即可通过宿主 App 加载,每个插件 apk 被加载后,都会在宿主中创建一个单独的 LoadedPlugin 对象。如下图所示,通过这些 LoadedPlugin 对象,VirtualAPK 就可以管理插件并赋予插件新的意义,使其可以像手机中安装过的App一样运行。


VirtualAPK


1. VirtualAPK 的运行形态

我们计划赋予 VirtualAPK 两种工作形态,耦合形态和独立形态。目前 VirtualAPK 对耦合形态已经有了很好的支持,接下来将计划支持独立形态。

2. 如何使用

第一步: 初始化插件引擎

@Override
protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    PluginManager.getInstance(base).init();
}

第二步:加载插件

public class PluginManager {
    public void loadPlugin(final File apk);
    public void loadPlugin(final String moduleCode);
}

我们对上述加载过程进行了一些封装,通过如下方式即可异步地去加载一个插件。

// 示例:启动插件中的Activity
DownloadManager downloadManager = DownloadManager.getInstance(this);
downloadManager.loadModule("com.ryg.test", true, this, new ILoadListener() {
    @Override
    public void onLoadEnd(int resultCode) {
        if (resultCode == ILoadListener.LOAD_SUCCESS) {
            Intent intent = new Intent();
            intent.setClassName("com.ryg.test", "com.ryg.test.MainActivity");
            startActivity(intent);
        } else {
            // todo load plugin failed
        }
    }
});

当插件入口被调用后,插件的后续逻辑均不需要宿主干预,均走原生的 Android 流程。比如,在插件内部,如下代码将正确执行:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_book_manager);
    LinearLayout holder = (LinearLayout)findViewById(R.id.holder);
    TextView imei = (TextView)findViewById(R.id.imei);
    imei.setText(IDUtil.getUUID(this));

    // bind service in plugin
    Intent service = new Intent(this, BookManagerService.class);
    bindService(service, mConnection, Context.BIND_AUTO_CREATE);

    // start activity in plugin
    Intent intent = new Intent(this, TCPClientActivity.class);
    startActivity(intent);
}

五、探究原理

1. 基本原理

2. 四大组件的实现原理


VirtualAPK


以下是 VirtualAPK 的整体结构图。


VirtualAPK


六、填坑之路

在实践中我们遇到了很多很多的问题,比如机型适配、API 版本适配、Binder Hook 的稳定性保证等问题,这里拿一个典型的资源适配问题来说明。

其实这是一个很无奈的问题,由于国内各大 ROM 厂商喜欢深度定制 Android 系统,所以就出现了这种适配问题。

正常情况下我们通过如下代码去创建插件的 Resources 对象:

Resources newResources = new Resources(assetManager,
 hostResources.getDisplayMetrics(), hostResources.getConfiguration());

然后在 Vivo 手机上,竟然出现了如下的类型转换错误,看起来是 Vivo 自己派生了 Resources 的子类。

java.lang.RuntimeException: Unable to start activity ComponentInfo{com.sdu.didi.psnger/com.didi.virtualapk.core.A$1}: java.lang.ClassCastException: android.content.res.Resources cannot be cast to android.content.res.VivoResources
    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2196)
    at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2245)
    at android.app.ActivityThread.access$800(ActivityThread.java:140)
    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1202)
    at android.os.Handler.dispatchMessage(Handler.java:102)
    at android.os.Looper.loop(Looper.java:136)
    at android.app.ActivityThread.main(ActivityThread.java:5143)
    at java.lang.reflect.Method.invokeNative(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:515)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:786)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:602)
    at dalvik.system.NativeStart.main(Native Method)
Caused by: java.lang.ClassCastException: android.content.res.Resources cannot be cast to android.content.res.VivoResources
    at android.app.ResourcesManager.getTopLevelResources(ResourcesManager.java:236)
    at android.app.ContextImpl.<init>(ContextImpl.java:2057)
    at android.app.ContextImpl.createActivityContext(ContextImpl.java:2008)
    at android.app.ActivityThread.createBaseContextForActivity(ActivityThread.java:2207)
    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2140)
    ... 11 more

于是反编译了下 Vivo 的 Framework 代码,果不其然,在如下代码中进行了类型转换,所以在加载插件资源的时候就报错了。

@VivoHook(hookType = VivoHookType.NEW_METHOD)
    public Resources getTopLevelResources(String pkgName, String resDir, int displayId, Configuration overrideConfiguration, CompatibilityInfo compatInfo, IBinder token) {
        Resources resources = getTopLevelResources(resDir, displayId, overrideConfiguration, compatInfo, token);
        if (resources != null) {
            ((VivoResources) resources).init(pkgName);
        }
        return resources;
    }

为了解决这个问题,我们分析了 VivoResources 的代码实现,然后在创建插件资源的时候,采用了如下的代码。

private static final class VivoResourcesCompat {
    private static Resources createResources(Resources hostResources, AssetManager assetManager) throws Exception {
        Class resourcesClazz = Class.forName("android.content.res.VivoResources");
        Resources newResources = (Resources)ReflectUtil.invokeConstructor(resourcesClazz,
                new Class[]{AssetManager.class, DisplayMetrics.class, Configuration.class},
                new Object[]{assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration()});
        return newResources;
    }
}

除了 Vivo 以外,有类似问题的还有 MIUI、Nubia 以及其他不知名的机型。而且在 Vivo 手机上,除了类型转换错误的问题,还有其他很坑的问题。

事实上我们还处理了很多其他的坑,这里无法一一说明,所以说如何保证插件化的稳定性是一件很有技术挑战的事情。

七、一些暂时不支持的特性

由于种种原因,VirtualAPK 目前未能支持所有的 Android 的特性,已知的有如下几点:

八、开源计划

我们的目标是打造一款功能完备的插件化框架,使得各个业务线都能以插件的形式集成,从而实现 Android App 的热更新能力。

目前 VirtualAPK 还有一些特性需要进一步完善,待完善后,将会进行开源计划。我们期望 VirtualAPK 开源后,可以让其他 App 能够无缝集成,无需考虑细节实现和兼容性问题即可轻松拥有热更新能力。