Android插件化初体验

本文介绍了Android插件化的概念,通过hook技术实现插件APK的加载与启动。详细阐述了从初始化插件APK类文件、hook启动流程、处理插件Activity生命周期到初始化插件资源的全过程。在hook过程中,通过代理替换Activity启动流程,使得未在AndroidManifest中注册的PluginActivity也能启动。此外,还展示了如何创建插件Resources以便插件Activity访问资源。
摘要由CSDN通过智能技术生成

*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布

最近把Activity启动流程整体看了一遍,估摸着弄个啥来巩固下,发现插件化正好是这块技术的实践,而说道插件化其实有好几种实现方式,这里我用的是hook的方式实现,主要目的呢是为了对activity启动流程有个整体的认识,当然了本篇的插件化也只是一个demo版本并没有任何兼容适配,重在流程和原理的理解。

概述

插件化顾名思义,就是将一个APK拆成多个,当需要的时候下载对应插件APK加载的技术。本文demo中除了下载是通过adb命令,其他都是模拟真实环境的,这里先理下流程。

  1. 将插件工程打包为APK,然后通过adb push命令发送到宿主APK目录(模拟下载流程)。
  2. 利用ClassLoader加载插件APK中的类文件。
  3. hook Activity启动流程中部分类,利用占坑Activity帮助PluginActivity绕过AMS验证,在真正启动的时候又替换回PluginActivity。
  4. 创建插件Apk的Resources对象,完成插件资源的加载。

对整体流程有个大概认识后,下面将结合源码和Demo来详细讲解,本文贴出的源码基于API27。

初始化插件APK类文件

既然插件APK是通过网络下载下来的,那么APK中的类文件就需要我们自己加载了,这里我们要用到DexClassLoader去加载插件APK中的类文件,然后将DexClassLoader中的Element数组和宿主应用的PathClassLoader的Element数组合并再设置回PathClassLoader,完成插件APK中类的加载。对ClassLoader不太熟悉的可以看下我另篇Android ClassLoader浅析

public class InjectUtil {
   

    private static final String TAG = "InjectUtil";
    private static final String CLASS_BASE_DEX_CLASSLOADER = "dalvik.system.BaseDexClassLoader";
    private static final String CLASS_DEX_PATH_LIST = "dalvik.system.DexPathList";
    private static final String FIELD_PATH_LIST = "pathList";
    private static final String FIELD_DEX_ELEMENTS = "dexElements";


    public static void inject(Context context, ClassLoader origin) throws Exception {
   
        File pluginFile = context.getExternalFilesDir("plugin");// /storage/emulated/0/Android/data/$packageName/files/plugin
        if (pluginFile == null || !pluginFile.exists() || pluginFile.listFiles().length == 0) {
   
            Log.i(TAG, "插件文件不存在");
            return;
        }
        pluginFile = pluginFile.listFiles()[0];//获取插件apk文件
        File optimizeFile = context.getFileStreamPath("plugin");// /data/data/$packageName/files/plugin
        if (!optimizeFile.exists()) {
   
            optimizeFile.mkdirs();
        }
        DexClassLoader pluginClassLoader = new DexClassLoader(pluginFile.getAbsolutePath(), optimizeFile.getAbsolutePath(), null, origin);
        Object pluginDexPathList = FieldUtil.getField(Class.forName(CLASS_BASE_DEX_CLASSLOADER), pluginClassLoader, FIELD_PATH_LIST);
        Object pluginElements = FieldUtil.getField(Class.forName(CLASS_DEX_PATH_LIST), pluginDexPathList, FIELD_DEX_ELEMENTS);//拿到插件Elements

        Object originDexPathList = FieldUtil.getField(Class.forName(CLASS_BASE_DEX_CLASSLOADER), origin, FIELD_PATH_LIST);
        Object originElements = FieldUtil.getField(Class.forName(CLASS_DEX_PATH_LIST), originDexPathList, FIELD_DEX_ELEMENTS);//拿到Path的Elements

        Object array = combineArray(originElements, pluginElements);//合并数组
        FieldUtil.setField(Class.forName(CLASS_DEX_PATH_LIST), originDexPathList, FIELD_DEX_ELEMENTS, array);//设置回PathClassLoader
        Log.i(TAG, "插件文件加载成功");
    }

    private static Object combineArray(Object pathElements, Object dexElements) {
   //合并数组
        Class<?> componentType = pathElements.getClass().getComponentType();
        int i = Array.getLength(pathElements);
        int j = Array.getLength(dexElements);
        int k = i + j;
        Object result = Array.newInstance(componentType, k);
        System.arraycopy(dexElements, 0, result, 0, j);
        System.arraycopy(pathElements, 0, result, j, i);
        return result;
    }

}

这里我们约定将插件APK放在/storage/emulated/0/Android/data/$packageName/files/plugin目录,然后为了尽早加载所以在Application中执行加载逻辑。

public class MyApplication extends Application {
   
    @Override
    protected void attachBaseContext(Context base) {
   
        super.attachBaseContext(base);
        try {
   
            InjectUtil.inject(this, getClassLoader());//加载插件Apk的类文件
        } catch (Exception e) {
   
            e.printStackTrace();
        }
    }
}

Hook启动流程

在说之前我们得先了解下Activity的启动流程。

上图抽象的给出了Acticity的启动过程。在应用程序进程中的Activity向AMS请求创建Activity(步骤1),AMS会对这个Activty的生命周期栈进行管理,校验Activity等等。如果Activity满足AMS的校验,AMS就会请求应用程序进程中的ActivityThread去创建并启动Activity。

那么在上一步我们已经将插件Apk的类文件加载进来了,但是我们并不能通过startActivity的方式去启动PluginActivity,因为PluginActivity并没有在AndroidManifest中注册过不了AMS的验证,既然这样我们换一个思路。

  1. 在宿主项目中提前弄一个SubActivity占坑,在启动PluginActivity的时候替换为启动这个SubActivity绕过验证。
  2. 在AMS处理完相应验证通知我们ActivityThread创建Activty的时候在替换为PluginActivity。

占坑SubActivity非常简单

public class SubActivity extends Activity {
   

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
   
        super.onCreate(savedInstanceState);
    }
}

然后在AndroidManifest注册好即可

<activity android:name=".SubActivity"/>

对于startActivity()最终都会调到ActivityManagerService的startActivity()方法。

ActivityManager.getService()//获取AMS
                .startActivity(whoThread, who.getBasePackageName(), intent,
                        intent.resolveTypeIfNeeded(who.getContentResolver()),
                        token, target != null ? target.mEmbeddedID : null,
                        requestCode, 0, null, options);

那么我们可以通过动态代理hook ActivityManagerService,然后在startActivity()的时候将PluginActivity替换为SubActivity,不过对于ActivityManagerService的获取不同版本方式有所不同。

在Android7.0以下会调用ActivityManagerNative的getDefault方法获取,如下所示。

    static public IActivityManager getDefault(
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值