Android 插件化原理及实践

概述

插件化是一个非常大的话题,他包含很多的知识点,我们今天简单的学习一下他的原理,并且从零开始实现插件化,这里主要用到了Hook技术

关联文章

Android APK资源加载流程

Android 中的ClassLoader

Android App启动过程

Android 热修复原理实战

设计模式 – 代理模式

插件化需要解决的问题和技术

  • Hook技术
  • 插件的类加载
  • 插件的资源加载
  • 启动插件Activity

Hook技术

如果我们自己创建代理对象,然后把原始对象替换为我们的代理对象(劫持原始对象),那么就可以在这个代理对象为所欲为了,修改参数,替换返回值,我们称之为 Hook

我们可用用Hook技术来劫持原始对象,被劫持的对象叫做Hook点,什么样的对象比较容易Hook呢?当然是单例和静态对象,在一个进程内单例和静态对象不容易发生改变,用代理对象来替代Hook点,这样我们就可以在代理对象中实现自己想做的事情,我们这里Hook常用的startActivity方法来举例

对于 startActivity过程有两种方式:Context.startActivityActivity.startActivity。这里暂不分析其中的区别,以 Activity.startActivity 为例说明整个过程的调用栈。

Activity 中的 startActivity 最终都是由 startActivityForResult 来实现的。

  @Override
    public void startActivity(Intent intent, @Nullable Bundle options) {
        if (options != null) {
            startActivityForResult(intent, -1, options);
        } else {
            // Note we want to go through this call for compatibility with
            // applications that may have overridden the method.
            startActivityForResult(intent, -1);
        }
    }
    
     public void startActivityForResult(@RequiresPermission Intent intent, int requestCode) {
        startActivityForResult(intent, requestCode, null);
    }
    
     public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
            @Nullable Bundle options) {
    ...
            //注释1
            Instrumentation.ActivityResult ar =
                mInstrumentation.execStartActivity(
                    this, mMainThread.getApplicationThread(), mToken, this,
                    intent, requestCode, options);
    ...
    }

我们看注释1处,调用了mInstrumentation.execStartActivity,来启动Activity,这个mInstrumentationActivity成员变量,我们选择mInstrumentation作为Hook

首先先写出代理Instrumentation类

public class ProxyInstrumentation extends Instrumentation {

    private final Instrumentation instrumentation;

    public ProxyInstrumentation(Instrumentation instrumentation){
        this.instrumentation=instrumentation;
    }

    public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {


        Log.d("mmm", "Hook成功,执行了startActivity"+who);

        Intent replaceIntent = new Intent(target, TextActivity.class);
        replaceIntent.putExtra(TextActivity.TARGET_COMPONENT, intent);
        intent = replaceIntent;

        try {
            Method execStartActivity = Instrumentation.class.getDeclaredMethod(
                    "execStartActivity",
                    Context.class,
                    IBinder.class,
                    IBinder.class,
                    Activity.class,
                    Intent.class,
                    int.class,
                    Bundle.class);
            return (ActivityResult) execStartActivity.invoke(instrumentation, who, contextThread, token, target, intent, requestCode, options);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }
}

ProxyInstrumentation类继承Instrumentation,并包含原始Instrumentation的引用,实现了execStartActivity方法,其内部会打印log并且反射调用原始Instrumentation对象的execStartActivity方法

接下来我们用ProxyInstrumentation类替换原始的Instrumentation,代码如下:

    public static void doInstrumentationHook(Activity activity){
        // 拿到原始的 mInstrumentation字段
        Field mInstrumentationField = null;
        try {
            mInstrumentationField = Activity.class.getDeclaredField("mInstrumentation");
            mInstrumentationField.setAccessible(true);

            // 创建代理对象
            Instrumentation originalInstrumentation = (Instrumentation) mInstrumentationField.get(activity);
            mInstrumentationField.set(activity, new ProxyInstrumentation(originalInstrumentation));
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

然后再MainActivity中调用这个方法

  protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ProxyUtils.doInstrumentationHook(this);
    }

然后启动一个Activity

   findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(MainActivity.this,TextActivity.class);
                startActivity(intent);
            }
        })

看日志

12-19 10:25:29.911 8957-8957/com.renxh.hook D/mmm: Hook成功,执行了startActivitycom.renxh.hook.MainActivity@71f98a6

这样我们就成功Hook住了Instrumentation

插件的类加载

这块需要了解ClassLoader,以及如何合并不同ClassLoader中的类,这个我之前分析过,不太了解的请看

Android 中的ClassLoader

Android 热修复原理实战

public class PluginHelper {
    private static final String TAG = "mmm";


    public static void loadPluginClass(Context context, ClassLoader hostClassLoader) {
        // 获取到插件apk,通常都是从网络上下载,这里为了演示,直接将插件apk push到手机
        String pluginPath = copyFile("/sdcard/plugin1.apk", context);

        String dexopt = context.getDir("dexopt", 0).getAbsolutePath();
        DexClassLoader pluginClassLoader = new DexClassLoader(pluginPath, dexopt, null, hostClassLoader);

        //  通过反射获取到pluginClassLoader中的pathList字段
        Field baseDexpathList = null;
        try {
            //获取插件中的类
            baseDexpathList = BaseDexClassLoader.class.getDeclaredField("pathList");
            baseDexpathList.setAccessible(true);
            Object pathlist = baseDexpathList.get(pluginClassLoader);
            Field dexElementsFiled = pathlist.getClass().getDeclaredField("dexElements");
            dexElementsFiled.setAccessible(true);
            Object[] dexElements = (Object[]) dexElementsFiled.get(pathlist);

            //获取应用内的类
            Field baseDexpathList1 = BaseDexClassLoader.class.getDeclaredField("pathList");
            baseDexpathList1.setAccessible(true);
            Object pathlist1 = baseDexpathList1.get(hostClassLoader);
            Field dexElementsFiled1 = pathlist1.getClass().getDeclaredField("dexElements");
            dexElementsFiled1.setAccessible(true);
            Object[] dexElements1 = (Object[]) dexElementsFiled1.get(pathlist1);


            //创建一个数组
            Object[] finalArray = (Object[]) Array.newInstance(dexElements.getClass().getComponentType(),
                    dexElements.length + dexElements1.length);
            //合并插件和应用内的类
            System.arraycopy(dexElements, 0, finalArray, 0, dexElements.length);
            System.arraycopy(dexElements1, 0, finalArray, dexElements.length, dexElements1.length);
            //把新数组替换掉原先的数组
            dexElementsFiled1.set(pathlist1, finalArray);
            Log.d("mmm","插件加载完成");
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

介绍下上方的代码,首先利用DexClassLoader加载未安装的插件apk,然后取出插件apkdexElements数组,我们知道dexElements封装了Element,而Element内部封装了DexFileDexFile用于加载dex文件,也就是说dexElements数组储存着插件所有的类,然后再拿到应用中的dexElements数组,他存储着应用中所有的类,最后把这俩个dexElements合并,然后把合并后的数组赋值给应用的dexElements变量,这时应用中就有了插件中所有类

启动插件Activity

我们知道没有在AndroidManifest中注册的Activity是不能启动的,但是我们插件中的Activity本来就没有在AndroidManifest中注册,无法启动,那么我们改咋么办呢?

使用占坑的Activity通能过AMS的验证

先在 Manifest 中预埋 TextActivity,启动时 hook时,将 Intent 替换成 TextActivity。

我们在上面的ProxyInstrumentationexecStartActivity方法加入点逻辑

public class ProxyInstrumentation extends Instrumentation {

    private final Instrumentation instrumentation;

    public ProxyInstrumentation(Instrumentation instrumentation){
        this.instrumentation=instrumentation;
    }

    public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {
            
        Log.d("mmm", "Hook成功,执行了startActivity"+who);


        Intent replaceIntent = new Intent(target, TextActivity.class);
        replaceIntent.putExtra(TextActivity.TARGET_COMPONENT, intent);
        intent = replaceIntent;

        try {
            Method execStartActivity = Instrumentation.class.getDeclaredMethod(
                    "execStartActivity",
                    Context.class,
                    IBinder.class,
                    IBinder.class,
                    Activity.class,
                    Intent.class,
                    int.class,
                    Bundle.class);
            return (ActivityResult) execStartActivity.invoke(instrumentation, who, contextThread, token, target, intent, requestCode, options);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }
}

我们把原来的intent保存起来,然后创建一个TextActivity的intent,达到通过AMS验证的目的,这里利用占坑的TextActivity通过验证,不过我还最终还是要打开插件的PluginActivity的,所以需要找个合适的时候,再把intent还原回来

那么什么时候还原回来呢?

我们知道在Activity启动时ActivityThread会收到Handler的消息,然后再打开Activity

 public void handleMessage(Message msg) {
            if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
            switch (msg.what) {
                case LAUNCH_ACTIVITY: {
                    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
                    final ActivityClientRecord r = (ActivityClientRecord) msg.obj;

                    r.packageInfo = getPackageInfoNoCheck(
                            r.activityInfo.applicationInfo, r.compatInfo);
                    handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");
                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                } break;

最终会到handleMessage方法中,然后对LAUNCH_ACTIVITY消息进行处理,最终会调用ActivityonCreate方法

那么我们改在哪里还原intent呢?

我们看一下Handler的源码

 public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }
  • msg中有callback时,则调用message.callback.run();方法,其中的callback指的Runnable
  • 如果callback为空,那么则看一下成员变量的mCallback是否为空,这个是Handler的构造方法传入的
  • 如果mCallback也为空,则调用handleMessage方法,这个一般在Handler的子类中重写

我们看到只要mCallback不为null,就执行callbackhandleMessage方法,我们可以以mCallbackHook点,自定义mCallback,来替换当前·handler·对象的Callback

public class ProxyHandlerCallback implements Handler.Callback {
    private Handler mBaseHandler;

    public ProxyHandlerCallback(Handler mBaseHandler) {
        this.mBaseHandler = mBaseHandler;
    }

    @Override
    public boolean handleMessage(Message msg) {
        Log.d("mmm", "接受到消息了msg:" + msg);
        if (msg.what == LAUNCH_ACTIVITY) {
            try {
                Object obj = msg.obj;
                Field intentField = obj.getClass().getDeclaredField("intent");
                intentField.setAccessible(true);
                Intent intent = (Intent) intentField.get(obj);

                Intent targetIntent = intent.getParcelableExtra(TextActivity.TARGET_COMPONENT);
                intent.setComponent(targetIntent.getComponent());
                Log.e("mmmintentField", targetIntent.toString());
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
        mBaseHandler.handleMessage(msg);
        return true;
    }
}

这个类继承了了Handler.Callback,重写了handleMessage方法,收到消息的类型为LAUNCH_ACTIVITY,在这个方法内还原intent

然后我们定义一个Hook这个Handler的方法

  public static void doHandlerHook() {
        try {
            Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
            Method currentActivityThread = activityThreadClass.getDeclaredMethod("currentActivityThread");
            Object activityThread = currentActivityThread.invoke(null);

            Field mHField = activityThreadClass.getDeclaredField("mH");
            mHField.setAccessible(true);
            Handler mH = (Handler) mHField.get(activityThread);

            Field mCallbackField = Handler.class.getDeclaredField("mCallback");
            mCallbackField.setAccessible(true);
            mCallbackField.set(mH, new ProxyHandlerCallback(mH));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

现在插件类加载到了应用中,插件Activity也已经还原了,还差一步,就是插件资源的加载

插件资源的加载

首先我们把插件apk的资源加载出来

public class ResourceHelper {
    public static Resources sPluginResources;
    public static AssetManager sNewAssetManager ;

    public static void addResource(Context context, String path) {
        //利用反射创建一个新的AssetManager

        try {
            sNewAssetManager = AssetManager.class.getConstructor().newInstance();
            //利用反射获取addAssetPath方法
            Method mAddAssetPath = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
            mAddAssetPath.setAccessible(true);
            //利用反射调用addAssetPath方法加载外部的资源(SD卡)
            if (((Integer) mAddAssetPath.invoke(sNewAssetManager, path)) == 0) {
                throw new IllegalStateException("Could not create new AssetManager");
            }

            sPluginResources = new Resources(sNewAssetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());

            Log.d("mmm","资源加载完毕");
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    }
}

首先创建一个AssetsManager,然后加载外部资源,然后构建一个新的Resouce

然后再应用的Aplication重写getResourcesgetAssets返回我们插件的资源

  @Override
    public Resources getResources() {
        return ResourceHelper.sPluginResources == null ? super.getResources() : ResourceHelper.sPluginResources;
    }

    @Override
    public AssetManager getAssets() {
        return ResourceHelper.sNewAssetManager == null ? super.getAssets() : ResourceHelper.sNewAssetManager;
    }

因为插件的四大组件都是在宿主中创建的,所以拿到的Application其实也是宿主的,所以插件Activity只需要getApplication().getResources()就可以方便的使用插件中的资源

最后在插件Activity重写getResources和getAssets方法

public class PluginActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_plugin);
    }


    @Override
    public Resources getResources() {
        return getApplication() != null && getApplication().getResources() != null ? getApplication().getResources() : super.getResources();
    }

    @Override
    public AssetManager getAssets() {
        return getApplication() != null && getApplication().getAssets() != null ? getApplication().getAssets() : super.getAssets();
    }
}

现在资源也已经加载完了

注意事项

插件Activitystyle要切换成NoActionBar,不然会出bug,这个bug我目前也不知道为什么

<resources>

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>

</resources>

Demo

Demo地址

demo使用很简单,把assets文件夹下的plugin1.apk推入sdcard就可以使用

参考

《Android开发进阶解密》

https://segmentfault.com/a/1190000015688023#item-4

https://www.jianshu.com/p/d3231a15afee

https://www.jianshu.com/p/ba00ac520aad

发布了100 篇原创文章 · 获赞 5 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/qq_34760508/article/details/103627874