乐趣区

关于android:浅谈-Android-插件化原理

转自 Kindem 的博客,欢送转载,但要注明出处

???? 意识插件化

想必大家都晓得,在 Android 零碎中,利用是以 Apk 的模式存在的,利用都须要装置能力应用。但实际上 Android 零碎装置利用的形式相当简略,其实就是把利用 Apk 拷贝到零碎不同的目录下、而后把 so 解压进去而已。

常见的利用装置目录有:

  • /system/app:零碎利用
  • /system/priv-app:零碎利用
  • /data/app:用户利用

那可能大家会想问,既然装置这个过程如此简略,Android 是怎么运行利用中的代码的呢,咱们先看 Apk 的形成,一个常见的 Apk 会蕴含如下几个局部:

  • classes.dexJava 代码字节码
  • res:资源目录
  • libso 目录
  • assets:动态资产目录
  • AndroidManifest.xml:清单文件

其实 Android 零碎在关上利用之后,也只是开拓过程,而后应用 ClassLoader 加载 classes.dex 至过程中,执行对应的组件而已。

那大家可能会想一个问题,既然 Android 自身也是应用相似反射的模式加载代码执行,凭什么咱们不能执行一个 Apk 中的代码呢?

这其实就是插件化的目标,让 Apk 中的代码(次要是指 Android 组件)可能免装置运行,这样可能带来很多收益,最不言而喻的劣势其实就是通过网络热更新、热修复,设想一下,你的利用领有 Native 利用个别极高的性能,又能获取诸如 Web 利用一样的收益。

嗯,现实很美妙不是嘛?

???? 难点在哪

大家其实都晓得,Android 利用自身是基于魔改的 Java 虚拟机的,动静加载代码几乎不要太简略,只须要应用 DexClassLoader 加载 Apk,而后反射外面的代码就能够了。

然而光能反射代码是没有意义的,插件化真正的魅力在于,能够动静加载执行 Android 组件(即 ActivityServiceBroadcastReceiverContentProviderFragment)等。

认真想一下,其实要做到这一点是有难度的,最次要的妨碍是,四大组件是在零碎外面中注册的,具体来说是在 Android 零碎的 ActivityManagerService (AMS)PackageManagerService (PMS) 中注册的,而四大组件的解析和启动都须要依赖 AMSPMS,如何坑骗零碎,让他抵赖一个未装置的 Apk 中的组件,就是插件化的最大难点。

另外,资源(特指 R 中援用的资源,如 layoutvalues 等)也是一大问题,设想一下你在宿主过程中应用反射加载了一个插件 Apk,代码中的 R 对应的 id 却无奈援用到正确的资源,会产生什么结果。

总结一下,其实做到插件化的要点就这几个:

  • 反射并执行插件 Apk 中的代码(ClassLoader Injection
  • 让零碎能调用插件 Apk 中的组件(Runtime Container
  • 正确辨认插件 Apk 中的资源(Resource Injection

当然还有其余一些小问题,但可能不是所有场景下都会遇到,咱们前面再独自说。

???? 解决方案

首先来谈一谈常见插件化框架的整体架构,市面上的插件化框架理论很多,如 Tecent 的 Shadow、Didi 的 VirtualApk、360 的 RePlugin,我本人也写了一个简略的插件化框架 Zed。他们各有各的短处,不过大体上差不多,如果要具体学习,我举荐 Shadow,它的劣势在于 0 Hook,没有应用公有 API 意味着能够走的很远,不会被 Google 搞。

他们大体原理其实都差不多,运行时会有一个宿主 Apk 在过程中跑,宿舍 Apk 是真正被装置的利用,宿主 Apk 能够加载插件 Apk 中的组件和代码运行,插件 Apk 能够任意热更新。

接下来咱们依照下面要点的程序,大抵讲一下每一个方面怎么攻克。

ClassLoader Injection

简略来说,插件化场景下,会存在同一过程中多个 ClassLoader 的场景:

  • 宿主 ClassLoader:宿主是装置利用,运行即主动创立
  • 插件 ClassLoader:应用 new DexClassLoader 创立

咱们称这个过程叫做 ClassLoader 注入。实现注入后,所有来自宿主的类应用宿主的 ClassLoader 进行加载,所有来自插件 Apk 的类应用插件 ClassLoader 进行加载,而因为 ClassLoader 的双亲委派机制,实际上零碎类会不受 ClassLoader 的类隔离机制所影响,这样宿主 Apk 就能够在宿主过程中应用来自于插件的组件类了。

Runtime Container

下面说到只有做到 ClassLoader 注入后,就能够在宿主过程中应用插件 Apk 中的类,然而咱们都晓得 Android 组件都是由零碎调用启动的,未装置的 Apk 中的组件,是未注册到 AMSPMS 的,就好比你间接应用 startActivity 启动一个插件 Apk 中的组件,零碎会通知你无奈找到。

咱们的解决方案很简略,即运行时容器技术,简略来说就是在宿主 Apk 中预埋一些空的 Android 组件,以 Activity 为例,我预置一个 ContainerActivity extends Activity 在宿主中,并且在 AndroidManifest.xml 中注册它。

它要做的事件很简略,就是帮忙咱们作为插件 Activity 的容器,它从 Intent 承受几个参数,别离是插件的不同信息,如:

  • pluginName
  • pluginApkPath
  • pluginActivityName

等,其实最重要的就是 pluginApkPathpluginActivityName,当 ContainerActivity 启动时,咱们就加载插件的 ClassLoaderResource,并反射 pluginActivityName 对应的 Activity 类。当实现加载后,ContainerActivity 要做两件事:

  • 转发所有来自零碎的生命周期回调至插件 Activity
  • 承受 Activity 办法的零碎调用,并转发回零碎

咱们能够通过复写 ContainerActivity 的生命周期办法来实现第一步,而第二步咱们须要定义一个 PluginActivity,而后在编写插件 Apk 中的 Activity 组件时,不再让其集成 android.app.Activity,而是集成自咱们的 PluginActivity,前面再通过字节码替换来自动化实现这部操作,前面再说为什么,咱们先看伪代码。

public class ContainerActivity extends Activity {
    private PluginActivity pluginActivity;

    @Override
    protected void onCreate(Bundle savedInstanceState) {String pluginActivityName = getIntent().getString("pluginActivityName", "");
        pluginActivity = PluginLoader.loadActivity(pluginActivityName, this);
        if (pluginActivity == null) {super.onCreate(savedInstanceState);
            return;
        }

        pluginActivity.onCreate();}

    @Override
    protected void onResume() {if (pluginActivity == null) {super.onResume();
            return;
        }
        pluginActivity.onResume();}

    @Override
    protected void onPause() {if (pluginActivity == null) {super.onPause();
            return;
        }
        pluginActivity.onPause();}
    
    // ...
}
public class PluginActivity {
    private ContainerActivity containerActivity;

    public PluginActivity(ContainerActivity containerActivity) {this.containerActivity = containerActivity;}

    @Override
    public <T extends View> T findViewById(int id) {return containerActivity.findViewById(id);
    }

    // ...
}
// 插件 `Apk` 中真正写的组件
public class TestActivity extends PluginActivity {// ......}

Emm,是不是感觉有点看懂了,尽管真正搞的时候还有很多小坑,但大略原理就是这么简略,启动插件组件须要依赖容器,容器负责加载插件组件并且实现双向转发,转发来自零碎的生命周期回调至插件组件,同时转发来自插件组件的零碎调用至零碎。

Resource Injection

最初要说的是资源注入,其实这一点相当重要,Android 利用的开发其实崇尚的是逻辑与资源拆散的理念,所有资源(layoutvalues 等)都会被打包到 Apk 中,而后生成一个对应的 R 类,其中蕴含对所有资源的援用 id

资源的注入并不容易,好在 Android 零碎给咱们留了一条后路,最重要的是这两个接口:

  • PackageManager#getPackageArchiveInfo:依据 Apk 门路解析一个未装置的 ApkPackageInfo
  • PackageManager#getResourcesForApplication:依据 ApplicationInfo 创立一个 Resources 实例

咱们要做的就是在下面 ContainerActivity#onCreate 中加载插件 Apk 的时候,用这两个办法创立进去一份插件资源实例。具体来说就是先用 PackageManager#getPackageArchiveInfo 拿到插件 ApkPackageInfo,有了 PacakgeInfo 之后咱们就能够本人组装一份 ApplicationInfo,而后通过 PackageManager#getResourcesForApplication 来创立资源实例,大略代码像这样:

PackageManager packageManager = getPackageManager();
PackageInfo packageArchiveInfo = packageManager.getPackageArchiveInfo(
    pluginApkPath,
    PackageManager.GET_ACTIVITIES
    | PackageManager.GET_META_DATA
    | PackageManager.GET_SERVICES
    | PackageManager.GET_PROVIDERS
    | PackageManager.GET_SIGNATURES
);
packageArchiveInfo.applicationInfo.sourceDir = pluginApkPath;
packageArchiveInfo.applicationInfo.publicSourceDir = pluginApkPath;

Resources injectResources = null;
try {injectResources = packageManager.getResourcesForApplication(packageArchiveInfo.applicationInfo);
} catch (PackageManager.NameNotFoundException e) {// ...}

拿到资源实例后,咱们须要将宿主的资源和插件资源 Merge 一下,编写一个新的 Resources 类,用这样的形式实现主动代理:

public class PluginResources extends Resources {
    private Resources hostResources;
    private Resources injectResources;

    public PluginResources(Resources hostResources, Resources injectResources) {super(injectResources.getAssets(), injectResources.getDisplayMetrics(), injectResources.getConfiguration());
        this.hostResources = hostResources;
        this.injectResources = injectResources;
    }

    @Override
    public String getString(int id, Object... formatArgs) throws NotFoundException {
        try {return injectResources.getString(id, formatArgs);
        } catch (NotFoundException e) {return hostResources.getString(id, formatArgs);
        }
    }

    // ...
}

而后咱们在 ContainerActivity 实现插件组件加载后,创立一份 Merge 资源,再复写 ContainerActivity#getResources,将获取到的资源替换掉:

public class ContainerActivity extends Activity {
    private Resources pluginResources;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // ...
        pluginResources = new PluginResources(super.getResources(), PluginLoader.getResources(pluginApkPath));
        // ...
    }

    @Override
    public Resources getResources() {if (pluginActivity == null) {return super.getResources();
        }
        return pluginResources;
    }
}

这样就实现了资源的注入。

???? 黑科技 —— 字节码替换

下面其实说到了,咱们被迫扭转了插件组件的编写形式:

class TestActivity extends Activity {}
->
class TestActivity extends PluginActivity {}

有没有什么方法能让插件组件的编写与原来没有任何差异呢?

Shadow 的做法是字节码替换插件,我认为这是一个十分棒的想法,简略来说,Android 提供了一些 Gradle 插件开发套件,其中有一项性能叫 Transform Api,它能够染指我的项目的构建过程,在字节码生成后、dex 文件生成钱,对代码进行某些变换,具体怎么做的不说了,能够本人看文档。

实现的性能嘛,就是用户配置 Gradle 插件后,失常开发,仍然编写:

class TestActivity extends Activity {}

而后实现编译后,最初的字节码中,显示的却是:

class TestActivity extends PluginActivity {}

到这里根本的框架就差不多完结了。

✨ 写在最初

插件化是一门很有意思的学识,Emm,怎么说呢,用一句话来形容就是偷天换日灯下黑,在各种坑的限度下一直跟零碎博弈寻找前途。随着理解的深刻,大家必定能了解我这句话,本文也只是抛砖引玉,更多的乐趣还是要本人去挖掘。

退出移动版