Android插件化初体验

Posted zhuliyuan丶

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android插件化初体验相关的知识,希望对你有一定的参考价值。

*本篇文章已授权微信公众号 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() 
        return gDefault.get();
    

    private static final Singleton<IActivityManager> gDefault = new Singleton<IActivityManager>() 
        protected IActivityManager create() 
            IBinder b = ServiceManager.getService("activity");//获取ams
            if (false) 
                Log.v("ActivityManager", "default service binder = " + b);
            
            IActivityManager am = asInterface(b);//拿到ams代理对象
            if (false) 
                Log.v("ActivityManager", "default service = " + am);
            
            return am;
        
    ;

getDefault()返回的是IActivityManager,而gDefault是一个单例对象Singleton并且是静态的是非常容易用反射获取。

Android8.0会调用ActivityManager的getService方法获取,如下所示。

    public static IActivityManager getService() 
        return IActivityManagerSingleton.get();
    

    private static final Singleton<IActivityManager> IActivityManagerSingleton =
            new Singleton<IActivityManager>() 
                @Override
                protected IActivityManager create() 
                    final IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE);//拿到ams
                    final IActivityManager am = IActivityManager.Stub.asInterface(b);//拿到ams代理对象
                    return am;
                
            ;

返回一个IActivityManager,而IActivityManagerSingleton是一个单例对象Singleton并且是静态非常容易获取。

在看下上面提到的Singleton等会hook会用到

public abstract class Singleton<T> 
    private T mInstance;
    protected abstract T create();
    public final T get() 
        synchronized (this) 
            if (mInstance == null) 
                mInstance = create();
            
            return mInstance;
        
    

到这里会发现其实返回的都是AMS的接口IActivityManager,那么我们只要能通过反射拿到,然后通过动态代理去Hook这个接口在启动的时候把PluginActivity替换为SubActivity即可绕过AMS的验证。

public class IActivityManagerProxy implements InvocationHandler //动态代理

    private final Object am;

    public IActivityManagerProxy(Object am) //传入代理的AMS对象
        this.am = am;
    

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable 
        if ("startActivity".equals(method.getName())) //startActivity方法
            Intent oldIntent = null;
            int i = 0;
            for (; i < args.length - 1; i++) //获取startActivity Intent参数
                if (args[i] instanceof Intent) 
                    oldIntent = (Intent) args[i];
                    break;
                
            
            Intent newIntent = new Intent();//创建新的Intent
            newIntent.setClassName("rocketly.demo", "rocketly.demo.SubActivity");//启动目标SubActivity
            newIntent.putExtra(HookHelper.TRANSFER_INTENT, oldIntent);//保留原始intent
            args[i] = newIntent;//把插件Intent替换为占坑Intent
        
        return method.invoke(am, args);
    

动态代理写好后,我们还需要通过反射去hook住原始AMS。因为会用到反射弄了一个简单的工具类

public class FieldUtil 
    public static Object getField(Class clazz, Object target, String name) throws Exception 
        Field field = clazz.getDeclaredField(name);
        field.setAccessible(true);
        return field.get(target);
    

    public static Field getField(Class clazz, String name) throws Exception 
        Field field = clazz.getDeclaredField(name);
        field.setAccessible(true);
        return field;
    

    public static void setField(Class clazz, Object target, String name, Object value) throws Exception 
        Field field = clazz.getDeclaredField(name);
        field.setAccessible(true);
        field.set(target, value);
    

接下来是hook代码

public class HookHelper 
    public static final String TRANSFER_INTENT = "transfer_intent";

    public static void hookAMS() throws Exception 
        Object singleton = null;
        if (Build.VERSION.SDK_INT >= 26) //大于等于8.0
            Class<?> clazz = Class.forName("android.app.ActivityManager");
            singleton = FieldUtil.getField(clazz, null, "IActivityManagerSingleton");//拿到静态字段
         else //8.0以下
            Class<?> activityManagerNativeClazz = Class.forName("android.app.ActivityManagerNative");
            singleton = FieldUtil.getField(activityManagerNativeClazz, null, "gDefault");//拿到静态字段
        
        Class<?> singleClazz = Class.forName("android.util.Singleton");
        Method getMethod = singleClazz.getMethod("get");
        Object iActivityManager = getMethod.invoke(singleton);//拿到AMS
        Class<?> iActivityManagerClazz = Class.forName("android.app.IActivityManager");
        Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]iActivityManagerClazz, new IActivityManagerProxy(iActivityManager));//生成动态代理
        FieldUtil.setField(singleClazz, singleton, "mInstance", proxy);//将代理后的对象设置回去
    

接下来我们需要在Application去执行hook

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

那么这里我们已经实现了第一步

在宿主项目中提前弄一个SubActivity占坑,在启动PluginActivity的时候替换为启动这个SubActivity绕过验证。

接下来我们在看如何在收到AMS创建Activity的通知时替换回PluginActivity。

AMS创建Activity的通知会先发送到ApplicationThread,然后ApplicationThread会通过Handler去执行对应逻辑。

private class ApplicationThread extends IApplicationThread.Stub 
            @Override
        public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident,
                ActivityInfo info, Configuration curConfig, Configuration overrideConfig,
                CompatibilityInfo compatInfo, String referrer, IVoiceInteractor voiceInteractor,
                int procState, Bundle state, PersistableBundle persistentState,
                List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents,
                boolean notResumed, boolean isForward, ProfilerInfo profilerInfo) //收到AMS启动Activity事件
            ActivityClientRecord r = new ActivityClientRecord();
            r.intent = intent;//给r赋上要启动的intent
           	...//省略很多r属性初始化
            sendMessage(H.LAUNCH_ACTIVITY, r);//发送r到Handler
        
    
        private void sendMessage(int what, Object obj) 
        	sendMessage(what, obj, 0, 0, false);
    	
    
        private void sendMessage(int what, Object obj, int arg1, int arg2, boolean async) 
        Message msg = Message.obtain();
        msg.what = what;
        msg.obj = obj;
        msg.arg1 = arg1;
        msg.arg2 = arg2;
        if (async) 
            msg.setAsynchronous(true);
        
        mH.sendMessage(msg);//发送到mH
    


private class H extends Handler 
    public static final int LAUNCH_ACTIVITY         = 100;
    public void handleMessage(Message msg) 
        switch (msg.what) 
            case LAUNCH_ACTIVITY: 
                final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
                r.packageInfo = getPackageInfoNoCheck(
                    r.activityInfo.applicationInfo, r.compatInfo);
                handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");//执行启动activity
             break;
        
    

既然是通过sendMessage()方式通知Handler去执行对应的方法,那么在调用handleMessage()之前会通过dispatchMessage()分发事件。

public class Handler 
    final Callback mCallback;
    public void dispatchMessage(Message msg) 
            if (msg.callback != null) 
                handleCallback(msg);
             else 
                if (mCallback != null) 
                    if (mCallback.handleMessage(msg)) 
                        return;
                    
                
                handleMessage(msg);
            
	
    
    public interface Callback 
        public boolean handleMessage(Message msg);
    

可以发现一个很好的hook点就是mCallback这个接口,可以让我们在handleMessage方法之前将ActivityClientRecord中的SubActivity Intent替换回PluginActivity Intent。

public class HCallback implements Handler.Callback //实现Callback接口

    public static final int LAUNCH_ACTIVITY = 100;

    @Override
    public boolean handleMessage以上是关于Android插件化初体验的主要内容,如果未能解决你的问题,请参考以下文章

Android 插件化Hook 插件化框架 ( 合并 “插件包“ 与 “宿主“ 中的 Element[] dexElements | 设置合并后的 Element[] 数组 )

Android 插件化插件化原理 ( JVM 内存数据 | 类加载流程 )

插件化技术:宿主访问插件资源

插件化技术:宿主访问插件资源

Activity插件化解决方案

Android-Plugin-Framework集成开发