Android插件化框架使用心得(原理篇)

工作原理

android 系统运行的基础是基于四大组件,那么插件化框架工作过程也离不开四大组件相关的流程(生命周期等),而 DroidPlugin 是一种非侵入式的设计方案,即插件程序,可以完全按照正常的app开发方式,和宿主间几乎无耦合,同时,宿主启动apk的过程,完全按照android原生api的调用方式,进行启动。

以Activity为例,简要描述下在安装插件app的情况下,启动一个插件中Activityin的原理。

首先看下Activity的启动原理,这个是源码层面的问题,有很多分析源码层面的文章,会详细说明,这里借用一张图来说明,

pic

如果看过源码分析,会发现其实这张图,只是简略的描述了下启动过程,不过还是可以看出启动一个Activity,最主要的两个方法,startActivity(),handleLaunchActivity(),前者是启动时,最早调用的方法,后者是android内部,实现activity跳转时,一个重要的方法。

由于插件的Activity,在插件的manifest中定义,而通过宿主启动插件的时候,如果是系统API的正常流程,在startActivity和handleLaunchActivity之间,就会有流程进行判断,检测出启动的activity包名,和宿主不对应,直接就会启动失败了。

于是DroidPlugin框架想了个办法,加载DroidPlugin后,预先注册了一些Activity,startActivity时,通过Hook的方式,将需要跳转的插件,Activity信息先替换为 DroidPlugin 框架中占坑预埋的 Activity (同时备份一份),是后续流程的检查和验证流程能顺利通过,然后在handleLaunchActivity 流程中,将备份的信息再替换回去。后续的流程,不会再有检查类的相关信息。

简单看下DroidPlugin中的这两个方法是如何Hook的,翻看源码,

以下是Activity相关Hook的类,继承了抽象类ProxyHook

public class IActivityManagerHook extends ProxyHook {

    private static final String TAG = IActivityManagerHook.class.getSimpleName();

    public IActivityManagerHook(Context hostContext) {
        super(hostContext);
    }
    ……
}

ProxyHook最终实现了InvocationHandler

public abstract class ProxyHook extends Hook implements InvocationHandler {

    protected Object mOldObj;

    public ProxyHook(Context hostContext) {
        super(hostContext);
    }
    ……
}

这是ProxyHook中的关键代码,可以大致看出,invoke执行的hook方法,可能和HookedMethodHandler有关。

    try {
        if (!isEnable()) {
            return method.invoke(mOldObj, args);
        }
        HookedMethodHandler hookedMethodHandler = mHookHandles.getHookedMethodHandler(method);
        if (hookedMethodHandler != null) {
            return hookedMethodHandler.doHookInner(mOldObj, method, args);
        }
        return method.invoke(mOldObj, args);
    }

找到 Activity 相关的 HookedMethodHandler 为 IActivityManagerHookHandle
从来找到了这个类中 hook 的 startActivity 方法

   private static class startActivity extends ReplaceCallingPackageHookedMethodHandler {

        public startActivity(Context hostContext) {
            super(hostContext);
        }

        protected boolean doReplaceIntentForStartActivityAPIHigh(Object[] args) throws RemoteException {
            int intentOfArgIndex = findFirstIntentIndexInArgs(args);
            if (args != null && args.length > 1 && intentOfArgIndex >= 0) {
                Intent intent = (Intent) args[intentOfArgIndex];
                //XXX String callingPackage = (String) args[1];
                if (!PluginPatchManager.getInstance().canStartPluginActivity(intent)) {
                    PluginPatchManager.getInstance().startPluginActivity(intent);
                    return false;
                }
                ActivityInfo activityInfo = resolveActivity(intent);
                if (activityInfo != null && isPackagePlugin(activityInfo.packageName)) {
                    ComponentName component = selectProxyActivity(intent);
                    if (component != null) {
                        Intent newIntent = new Intent();
                        try {
                            ClassLoader pluginClassLoader = PluginProcessManager.getPluginClassLoader(component.getPackageName());
                            setIntentClassLoader(newIntent, pluginClassLoader);
                        } catch (Exception e) {
                            Log.w(TAG, "Set Class Loader to new Intent fail", e);
                        }
                        newIntent.setComponent(component);
                        newIntent.putExtra(Env.EXTRA_TARGET_INTENT, intent);
                        newIntent.setFlags(intent.getFlags());


                        String callingPackage = (String) args[1];
                        if (TextUtils.equals(mHostContext.getPackageName(), callingPackage)) {
                            newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

                        args[intentOfArgIndex] = newIntent;
                        args[1] = mHostContext.getPackageName();
                    } else {
                        Log.w(TAG, "startActivity,replace selectProxyActivity fail");
                    }
                }
            }

            return true;
        }

上述这个方法中,最关键的就是这个 doReplaceIntentForStartActivityAPIHigh 方法

首先,拿到 Intent 参数,然后根据包名判断通过Intent启动的Activity是否在插件中,
如果存在,执行后续 hook 流程

Intent intent = (Intent) args[intentOfArgIndex]
ActivityInfo activityInfo = resolveActivity(intent);
if (activityInfo != null && isPackagePlugin(activityInfo.packageName))
……

然后通过 selectProxyActivity 从插件化框架之前占坑的 Activity 中选择一个出来

ComponentName component = selectProxyActivity(intent);
if (component != null) {
    Intent newIntent = new Intent();
    try {
        ClassLoader pluginClassLoader = PluginProcessManager.getPluginClassLoader(component.getPackageName());
        setIntentClassLoader(newIntent, pluginClassLoader);
    } catch (Exception e) {
        Log.w(TAG, "Set Class Loader to new Intent fail", e);
    }
}

之后构造一个新的Intent对象:newIntent,将之前选出的占坑的 Activity 中的信息调入到这个Intent中,同时将 启动插件的 Intent 信息 备份一份在 newIntent 中 newIntent.putExtra(Env.EXTRA_TARGET_INTENT, intent);

newIntent.setComponent(component);
newIntent.putExtra(Env.EXTRA_TARGET_INTENT, intent);
newIntent.setFlags(intent.getFlags());

然后把newIntent 重新复制到 原来Intent应该在的参数位置。这样Intent信息就被替换掉,于是就能跳过后续,Activity启动机制的检查了。

args[intentOfArgIndex] = newIntent;
args[1] = mHostContext.getPackageName();

简单回顾下Activity启动过程中的一个流程,中间有个重要的环节就是,AMS 通过一系列交互,走到了ActivityThread中的 scheduleLaunchActivity 这个方法中,会发送一个启动Activity的消息出去,给 Handler H 处理(这个 Handler 在源码中的命名就是 H),而 Hook Activity 关键流程的第二步就在这个 Handler 的处理中,翻看 ActivityThread 源码可以发现,关键一步在于 handleLaunchActivity

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);
          Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
      } 
      break;
      …… 
  }

再往之后的流程,就走到了,创建Application, 创建Activity的流程,于是,插件化框架考虑在handleLaunchActivity,通过Hook的方式,将先前 startActivity 中改写的信息在替换回来。
主要通过了PluginCallbackHook 这个类进行了hook

protected void onInstall(ClassLoader classLoader) throws Throwable {
        Object target = ActivityThreadCompat.currentActivityThread();
        Class ActivityThreadClass = ActivityThreadCompat.activityThreadClass();

        /*替换ActivityThread.mH.mCallback,拦截组件调度消息*/
        Field mHField = FieldUtils.getField(ActivityThreadClass, "mH");
        Handler handler = (Handler) FieldUtils.readField(mHField, target);
        Field mCallbackField = FieldUtils.getField(Handler.class, "mCallback");
        //*这里读取出旧的callback并处理*/
        Object mCallback = FieldUtils.readField(mCallbackField, handler);
        if (!PluginCallback.class.isInstance(mCallback)) {
            PluginCallback value = mCallback != null ? new PluginCallback(mHostContext, handler, (Handler.Callback) mCallback) : new PluginCallback(mHostContext, handler, null);
            value.setEnable(isEnable());
            mCallbacks.add(value);
            FieldUtils.writeField(mCallbackField, handler, value);
            Log.i(TAG, "PluginCallbackHook has installed");
        } else {
            Log.i(TAG, "PluginCallbackHook has installed,skip");
        }
    }

上述代码,主要是在通过反射拿到 ActivityThread 中的“mH” 对象,就是前文所述 Handler H,之后在拿到 Handler 类中 mCallback 对象,之后使用 PluginCallback 替换掉这个 callback,此 callback 中,就完成了对 Handler 消息处理,所以handleLaunchActivity就在这个流程中。

下面来看下 PluginCallback 中的具体操作。其实和 ActivityThread 源码比较类似。都是通过一个 handleMessage 来处理消息,真正的“替换”流程,还是在 handleLaunchActivity 中。
先拿到之前 在 startActivity 时,备份的 插件中目标Activity 的 Intent 信息

Intent stubIntent = (Intent) FieldUtils.readField(obj, "intent");
stubIntent.setExtrasClassLoader(mHostContext.getClassLoader());
Intent targetIntent = stubIntent.getParcelableExtra(Env.EXTRA_TARGET_INTENT);

之后将targetIntent 替换掉原来消息对象中的 Intent 信息

FieldUtils.writeDeclaredField(msg.obj, "intent", targetIntent);

然后后续继续处理,msg信息

if (mCallback != null) {
            return mCallback.handleMessage(msg);
        } else {
            return false;
        }

Acitivity 启动中,第二个重要的 hook 流程就结束了。中间过程还有很多细节处理,这里暂时不作过多说明。

参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值