Hook一个Activity的启动过程

在前一篇文章Activity启动过程分析中,通过源码分析的方式介绍了Activity的大致启动过程。今天就来实战一下,一个是加深对Activity启动过程的理解,另外一个就是让我们知道阅读源码有助于拓宽视野,提升开发能力。

首先先抛出需求:

  1. 我们想启动一个Activity A页面,但是想要进入这个A页面必须是已经登录过的,如果没有登录的话就启动登录页面B,并且在B页面登录成功之后需要跳转到页面A

  2. 提升一下难度,Activity页面A、B均没有在清单文件中注册,但是要完成正常的跳转(这是为插件化的研究做准备)

在阅读本文之前,可以先clone一份 apk-plugin-technology,参考此项目的binder-hook模块。运行一下Demo,让你有个更感性的认识

Hook技术

Hook,就是钩子,也就是说可以干预某些代码的正常执行流程。关于Hook的详细介绍,可以自行搜索相关文章。

完成Hook过程,需要注意3点,也可以说是3个步骤:

  1. 寻找Hook点,Hook点一般要选择类的静态成员变量,因为静态成员变量一般不容易发生变化,只需要Hook一次就好了。如果想要Hook的成员变量不是静态的,那么可以找这个变量所持有的引用中是否有静态的成员变量。而且最好是public的,public的一般不容易发生变化。如果是非public的,就要考虑适配问题了。

  2. 选择合适的代理方式,一般来说我们不可能Hook一个对象的所有方法,所以就要通过代理的方式来Hook,如果是想要Hook的方法,就要走我们自己的逻辑,如果是不需要Hook的方法,还是要调用原对象的方法。

  3. 用代理对象替换原始对象

这个过程可能还有点不是很清晰,没关系,继续往下看就明白了。

Activity启动过程寻找Hook点

我们知道,启动一个Activity,可以通过Activity本身的startActivity方法,也可以调用Context的startActivity方法,虽然Activity也是继承自Context,但是Activity重写了相关的启动方法,这是因为Activity本身有Activity任务栈,而其它的Context,比如Service中并没有任务栈,所以启动的时候需要加上一个flag Intent.FLAG_ACTIVITY_NEW_TASK,Context是一个抽象类,其启动Activity的方法,实际上调用的是ContextImpl的startActivity方法,例如:

@Override
public void startActivity(Intent intent, Bundle options) {
    warnIfCallingFromSystemProcess();

    // Calling start activity from outside an activity without FLAG_ACTIVITY_NEW_TASK is
    // generally not allowed, except if the caller specifies the task id the activity should
    // be launched in.
    if ((intent.getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) == 0
            && options != null && ActivityOptions.fromBundle(options).getLaunchTaskId() == -1) {
        throw new AndroidRuntimeException(
                "Calling startActivity() from outside of an Activity "
                + " context requires the FLAG_ACTIVITY_NEW_TASK flag."
                + " Is this really what you want?");
    }
    mMainThread.getInstrumentation().execStartActivity(
            getOuterContext(), mMainThread.getApplicationThread(), null,
            (Activity) null, intent, -1, options);
}

可以看到,如果不加FLAG_ACTIVITY_NEW_TASK标记的话会抛出异常。
另外,从这个方法结合我们之前Activity启动过程分析中所分析的,不管是通过Activity调用还是通过Context调用,最终调用的均是Instrumentation的execStartActivity方法。

public ActivityResult execStartActivity(
        Context who, IBinder contextThread, IBinder token, Activity target,
        Intent intent, int requestCode, Bundle options) {
   ...
    try {
      ... 
        int result = ActivityManagerNative.getDefault()
                .startActivity(whoThread, who.getBasePackageName(), intent,
                        intent.resolveTypeIfNeeded(who.getContentResolver()),
                        token, target != null ? target.mEmbeddedID : null,
                        requestCode, 0, null, options);
        ...
    } catch (RemoteException e) {
        throw new RuntimeException("Failure from system", e);
    }
    return null;
}

execStartActivity中会调用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");
        if (false) {
            Log.v("ActivityManager", "default service binder = " + b);
        }
        IActivityManager am = asInterface(b);
        if (false) {
            Log.v("ActivityManager", "default service = " + am);
        }
        return am;
    }
};

static public IActivityManager asInterface(IBinder obj) {
    if (obj == null) {
        return null;
    }
    IActivityManager in =
            (IActivityManager)obj.queryLocalInterface(descriptor);
    if (in != null) {
        return in;
    }

    return new ActivityManagerProxy(obj);
}

ActivityManagerNative的gDefault 是一个静态变量,它是Singleton的一个匿名类,Singleton类其实就是用于获取单例对象的,gDefault的get方法获取的是IActivityManager的一个实现类。知道Binder的应该知道,这个获取的实际上是ActivityManagerProxy对象,如果不明白的建议先去看看 Binder学习概要这篇文章。

ActivityManagerProxy对应的server端就是ActivityManagerService,也就是真正负责管理启动Activity的地方。我们启动一个Activity就是调用的ActivityManagerService的startActivity方法。

那么我们想一想,是否可以在gDefault这个方法做点文章呢。我们推理一下这个逻辑:

  1. hook 的是Activity的启动过程
  2. ActivityManagerService负责管理启动,但是它在Server端,我们拿不到,但是通过gDefault我们可以拿到它在本地的代理对象ActivityManagerProxy对象。
  3. 我们需要为这个ActivityManagerProxy对象创建一个代理对象,当它调用startActivity方法的时候,需要做一些处理,比如按照需求1,判断被跳转的页面是否需要登录,如果需要登录的话就更改这个Intent,跳转到登录页面。当调用其它的方法的时候,直接使用原始的ActivityManagerProxy对象去处理。

讲到这,其实我们就可以解决需求1了。但是我想把需求2一起解决了,这样的话上面的逻辑就有点不够完善,毕竟我们所要启动的Activity A和登录页面B都是没有在AndroidManifest.xml中声明的,启动一个未声明的Activity肯定会报一个ActivityNotFoundException。

再来回想一下Activity的启动过程:

调用startActivity方法启动一个目标Activity的时候,实际上会通过Instrumentation进行启动,再通过ActivityManagerService的本地代理对象调用ActivityManagerService的方法来启动一个Activity,这是一个IPC过程,在ActivityManagerService中会校验被启动的Activity的合法性,如果合法,会通过IPC过程调用ApplicationThread的方法,进而调用ActivityThread的handleLaunchActivity方法创建Activity,并执行Activity的生命周期方法。

看到没,Activity的启动过程是分两步的

  • ActivityManagerService去校验被启动Activity合法性,并做好启动Activity的必要准备
  • 在ActivityThread中真正的创建Activity,并完成Activity的启动阶段的生命周期回调。

既然没法办通过AMS去启动一个未注册的Activity,那么我们换一个思路来:

  1. 我们找一个在AndroidManifest.xml中声明一个代理页面ProxyActivity,当发起请求A页面的时候,我们hook ActivityManagerProxy的startActivity方法,把A页面的Intent替换为请求启动ProxyActivity页面的Intent,这样的话至少可以通过ActivityManagerService校验这一关。
  2. 当调用回我们ActivityThread的内部中的时候,做一下处理,把代理页面ProxyActivity对应的Intent替换成我们想要启动的Activity A对应的Intent,当然了,在这一过程还需要判断是否需要登录,如果需要登录的话,就需要替换成B页面。示例代码如下:
private void hookActivityManagerApi25() {
    try {
        // 反射获取ActivityManagerNative的静态成员变量gDefault, 注意,在8.0的时候这个已经更改了
        Class<?> activityManagerNativeClass = Class.forName("android.app.ActivityManagerNative");
        Field gDefaultField = activityManagerNativeClass.getDeclaredField("gDefault");
        gDefaultField.setAccessible(true);
        Object gDefaultObj = gDefaultField.get(null);
        
        // 我们在这里拿到的instanceObj对象一定不为空,如果为空的话就没办法使用
        Class<?> singletonClass = Class.forName("android.util.Singleton");
        Field mInstanceField = singletonClass.getDeclaredField("mInstance");
        mInstanceField.setAccessible(true);

        Object instanceObj = mInstanceField.get(gDefaultObj);

        // 需要动态代理IActivityManager,把Singleton的成员变量mInstance的值设置为我们的这个动态代理对象
        // 但是有一点,我们不可能完全重写一个IActivityManager的实现类
        // 所以还是需要用到原始的IActivityManager对象,只是在调用某些方法的时候做一些手脚
        Class<?> iActivityManagerClass = Class.forName("android.app.IActivityManager");
        InterceptInvocationHandler interceptInvocationHandler = new InterceptInvocationHandler(instanceObj);
        Object iActivityManagerObj = Proxy.newProxyInstance(context.getClassLoader(), new Class[]{iActivityManagerClass}, interceptInvocationHandler);
        mInstanceField.set(gDefaultObj, iActivityManagerObj);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

class InterceptInvocationHandler implements InvocationHandler {
    Object originalObject;
    public InterceptInvocationHandler(Object originalObject) {
        this.originalObject = originalObject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        LogUtils.d("method:" + method.getName() + " called with args:" + Arrays.toString(args));
        //如果是startActivity方法,需要做一些手脚
        if (METHOD_START_ACTIVITY.equals(method.getName())) {
            Intent newIntent = null;
            int index = 0;
            for (int i = 0; i < args.length; i++) {
                Object arg = args[i];
                if (arg instanceof Intent) {
                    Intent wantedIntent = (Intent) arg;
                    // 加入目标Activity没有在清单文件中注册,我们就欺骗ActivityManagerService,启动一个代理页面
                    // 真正启动页面,会开始回调ActivityThread的handleLaunchActivity方法
                    // 调用这个方法前可以做点文章,启动我们想要启动的页面
                    newIntent = new Intent();
                    ComponentName componentName = new ComponentName(context, ProxyActivity.class);
                    newIntent.setComponent(componentName);
                    //把原始的跳转信息当作参数携带给代理类
                    newIntent.putExtra(EXTRA_REAL_WANTED_INTENT, wantedIntent);
                    index = i;
                }
            }
            args[index] = newIntent;
        }
        return method.invoke(originalObject, args);
    }
}

那么在ActivityThread中怎么找Hook点呢?
首先要明确一点,我们找找个Hook点是要为了替换之前代理ProxyActivity的Intent的,有了找个思路,我们就可以有目的的去寻找了。

AMS启动一个Activity,会调用ApplicationThread的scheduleLaunchActivity方法,这个方法应该是在Activity启动过程中我们的App最先被AMS调用的了,在这个方法中第一个参数就是Intent,这个Intent就是我们发起请求启动ProxyActivity的Intent。

@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) {

    updateProcessState(procState, false);

    ActivityClientRecord r = new ActivityClientRecord();

    r.token = token;
    r.ident = ident;
// 注意这个参数
    r.intent = intent;
  ...

    sendMessage(H.LAUNCH_ACTIVITY, r);
}

ApplicationThread继承自ApplicationThreadNative,而ApplicationThreadNative继承自Binder并实现了IApplicationThread接口。我们考虑一下,能否像Hook ActivityManagerProxy那样,采用一个动态代理的方式,创建IApplicationThread的代理类,当调用IApplicationThread的scheduleLaunchActivity方法的时候,我们更改这个方法的Intent参数,变为我们想要的那个Intent,然后就可以按照我们的需求来跳转了。

private class ApplicationThread extends ApplicationThreadNative

public abstract class ApplicationThreadNative extends Binder
        implements IApplicationThread

想法是很好的,但是很遗憾,我们做不到,至于为什么,请接着往下看。

我们想要Hook ApplicationThread的scheduleLaunchActivity,那么我们先看一下这个ApplicationThread对象是什么时候创建的。ApplicationThread是ActivityThread的非静态内部类,在ActivityThread中,它的创建时机是在ActivityThread对象初始化的时候,

 final ApplicationThread mAppThread = new ApplicationThread();

由于它没有采用多态的方式来创建ApplicationThread,我们创建的动态代理对象实际上是没有办法赋值给mAppThread这个变量的,也就是说实际上这个点我们是没有办法hook的。

那么我们接着这个方法来看,在scheduleLaunchActivity方法中,通过ActivityThread的一个H类型的成员mH来发送一个类型为 H.LAUNCH_ACTIVITY (int 型,值为100)的消息,这个H是ActivityThread的非静态内部类,实际上是继承自Handler的。

private class H extends Handler

至于为什么需要用Handler来切换,在Binder学习概要
已经介绍过,因为scheduleLaunchActivity是在Binder线程池中被调用的,需要用Hander来切换到主线程。H.LAUNCH_ACTIVITY类型的消息发送之后,H的handleMessage方法会被调用,在这里就会根据msg.what的来处理,对应LAUNCH_ACTIVITY类型的,会调用ActivityThread的handleLaunchActivity来创建Activity并完成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);
// 调用ActivityThread的方法来启动Activity
            handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");
            Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
        } break;
        ....
    }
   ...
}

再往下就要走到ActivityThread的handleLaunchActivity方法中了,难道我们要去Hook ActivityThread的handleLaunchActivity方法?

首先,获取这个ActivityThread对象是没有难度的,ActivityThread对象可以在它的类成员变量sCurrentActivityThread获取。

private static volatile ActivityThread sCurrentActivityThread;

应用的启动入口是ActivityThread的main方法,在这个方法中会创建ActivityThread对象,接着又会调用它的attach方法,在这个方法中,把ActivityThread对象赋值给其类的静态成员变量sCurrentActivityThread。静态成员变量就很好说了,通过反射就可以获取这个对象。

public static void main(String[] args) {
 ...
    ActivityThread thread = new ActivityThread();
    thread.attach(false);
 ...
}

private void attach(boolean system) {
    sCurrentActivityThread = this;
    ...
}

虽然获取了这个ActivityThread对象,但是我们怎么准备一个代理对象来代理ActivityThread对象呢?

由于ActivityThread没有继承或实现任何类或接口,好像为它准备代理对象有点难度。

public final class ActivityThread

难道没有办法了吗?
当然不是,否则我还写这篇文章干嘛?

想一想Handler那块,从发送消息到处理消息,实际上中间是有一个消息分发过程的,也就是Handler的dispatchMessage方法会被调用,在这个方法中实际上Handler本身的handleMessage方法是最后才可能会被调用到的。msg.callback 这块我们没办法处理,因为消息创建是我们控制不了,而在else中,mCallback != null 这块,我们似乎可以给Hander设置一个mCallback,由这个Callback先一步处理消息,替换Intent。

public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}

我们看一下mCallBack的类型,它是Handler内部的一个接口。

public interface Callback {
    public boolean handleMessage(Message msg);
}

那我们赶紧去找找怎么设置Callback,不过很遗憾的是设置Callback只有一种方式就是作为Handler构造方法参数传递,但我们的mH对象已经创建了。既然正常的路径没办法了,那只要采用反射的方式来设置成员变量了。

private void hookActivityThreadHandler() {
    //需要hook ActivityThread
    try {
        //获取ActivityThread的成员变量 sCurrentActivityThread
        Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
        Field sCurrentActivityThread = activityThreadClass.getDeclaredField("sCurrentActivityThread");
        sCurrentActivityThread.setAccessible(true);
        Object activityThreadObj = sCurrentActivityThread.get(null);

        //获取ActivityThread的成员变量 mH
        Field mHField = activityThreadClass.getDeclaredField("mH");
        mHField.setAccessible(true);
        Handler mHObj = (Handler) mHField.get(activityThreadObj);

        Field mCallbackField = Handler.class.getDeclaredField("mCallback");
        mCallbackField.setAccessible(true);
        mCallbackField.set(mHObj, new ActivityCallback(mHObj));

    } catch (Exception e) {
        e.printStackTrace();
    }
}

private class ActivityCallback implements Handler.Callback {
    private Handler mH;
    public ActivityCallback(Handler mH) {
        this.mH = mH;
    }

    @Override
    public boolean handleMessage(Message msg) {
        if (msg.what == LAUNCH_ACTIVITY) {
            handleLaunchActivity(msg);
        }
        return false;
    }

    private void handleLaunchActivity(Message msg) {
        //替换我们真正想要的intent
        try {
            Object activityClientRecord = msg.obj;
            Field intentField = activityClientRecord.getClass().getDeclaredField("intent");
            intentField.setAccessible(true);
            //这个是代理ProxyActivity
            Intent interceptedIntent = (Intent) intentField.get(activityClientRecord);

            //真正想要跳转的 SecondActivity
            Intent realWanted = interceptedIntent.getParcelableExtra(EXTRA_REAL_WANTED_INTENT);
            if (realWanted != null) {
                //如果不需要登录
                Class<?> real = Class.forName(realWanted.getComponent().getClassName());
                NeedLogin annotation = real.getAnnotation(NeedLogin.class);

                if (annotation != null && !SPHelper.getBoolean("login", false)) {
                    //如果需要登录并且没有登录,跳转登录页面
                    Intent loginIntent = new Intent(context, LoginActivity.class);
                    loginIntent.putExtra(EXTRA_REAL_WANTED_INTENT, realWanted);
                    interceptedIntent.setComponent(loginIntent.getComponent());
                } else {
                    interceptedIntent.setComponent(realWanted.getComponent());
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

到此,就可以通过Hook的方式来启动一个未在AndroidManifest.xml声明的Activity了,并且可以根据是否需要登录来跳转到不同的页面。

继承AppCompatActivity会遇到的问题

如果你所有的Activity均继承的是Activity,上面的代码逻辑已经是没有问题了。但是,如果你的Activity类继承的是AppCompatActivity,是会报一个异常:

android.content.pm.PackageManager$NameNotFoundException

报这个异常的原因是在AppCompatActivity的onCreate方法中经过层层调用,会调用到NavUtils的getParentActivityName方法。在这个方法中会调用到PackageManager的getActivityInfo方法,返回的ActivityInfo对象是Activity在AndroidManifest.xml中注册信息对应的一个JavaBean对象,调用这个方法实际上会再检查一次Activity的合法性。

# android.support.v4.app.NavUtils
public static String getParentActivityName(Context context, ComponentName componentName)
        throws NameNotFoundException {
    PackageManager pm = context.getPackageManager();
    ActivityInfo info = pm.getActivityInfo(componentName, PackageManager.GET_META_DATA);
    if (Build.VERSION.SDK_INT >= 16) {
        String result = info.parentActivityName;
        if (result != null) {
            return result;
        }
    }
    if (info.metaData == null) {
        return null;
    }
    String parentActivity = info.metaData.getString(PARENT_ACTIVITY);
    if (parentActivity == null) {
        return null;
    }
    if (parentActivity.charAt(0) == '.') {
        parentActivity = context.getPackageName() + parentActivity;
    }
    return parentActivity;
}

在上面方法中,context.getPackageManager()实际上会调用ContextImpl的getPackageManager方法,而这个实际上返回的是ApplicationPackageManager对象,这个类是把IPackageManager进行了包装,实际上的功能还是由PackageManagerService调用。

# android.app.ContextImpl
public PackageManager getPackageManager() {
    if (mPackageManager != null) {
        return mPackageManager;
    }

    IPackageManager pm = ActivityThread.getPackageManager();
    if (pm != null) {
        // Doesn't matter if we make more than one instance.
        return (mPackageManager = new ApplicationPackageManager(this, pm));
    }

    return null;
}

而ApplicationPackageManager 调用getActivityInfo实际上调用的IPackageManagerd的getActivityInfo方法

# android.app.ApplicationPackageManager
@Override
public ActivityInfo getActivityInfo(ComponentName className, int flags)
        throws NameNotFoundException {
    try {
        ActivityInfo ai = mPM.getActivityInfo(className, flags, mContext.getUserId());
        if (ai != null) {
            return ai;
        }
    } catch (RemoteException e) {
        throw e.rethrowFromSystemServer();
    }

    throw new NameNotFoundException(className.toString());
}

这个过程就是经过一个IPC过程调用PackageManagerService的getActivityInfo方法。

为了不报NameNotFoundException异常,我们需要Hook这个IPackageManager,当调用PackageManager的getActivityInfo的时候,不让它进行IPC调用,而是直接返回一个不为null的ActivityInfo对象,这样就可以解决问题了。

Hook PMS

我们要去Hook PMS,还是遵循之前讲的3个步骤,那么就先来找这个Hook点,上面我们在贴出的代码中也看到了,ContextImpl的getPackageManager方法中首先会获取调用ActivityThread的静态方法getPackageManager来获取一个IPackageManager对象。我们来看一下这个方法。

# android.app.ActivityThread
public static IPackageManager getPackageManager() {
    if (sPackageManager != null) {
        //Slog.v("PackageManager", "returning cur default = " + sPackageManager);
        return sPackageManager;
    }
    IBinder b = ServiceManager.getService("package");
    //Slog.v("PackageManager", "default service binder = " + b);
    sPackageManager = IPackageManager.Stub.asInterface(b);
    //Slog.v("PackageManager", "default service = " + sPackageManager);
    return sPackageManager;
}

在这里有一个静态变量sPackageManager,如果不为空的话直接就返回了,这个静态变量的类型是接口类型,那么这个Hook点就很好,静态的很好获取对象,而接口类型更容易使用代理。

static volatile IPackageManager sPackageManager;

前面讲了那么多,怎么去Hook应该也知道 了,我们目前只Hook IPackageManager的getActivityInfo方法,废话也不多说了,直接贴代码,更直观。

private void HookPackageManager() {
    //需要hook ActivityThread
    try {
        //获取ActivityThread的成员变量 sCurrentActivityThread
        Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
        Field sPackageManagerField = activityThreadClass.getDeclaredField("sPackageManager");
        sPackageManagerField.setAccessible(true);
        Object iPackageManagerObj = sPackageManagerField.get(null);


        Class<?> iPackageManagerClass = Class.forName("android.content.pm.IPackageManager");
        InterceptPackageManagerHandler interceptInvocationHandler = new InterceptPackageManagerHandler(iPackageManagerObj);
        Object iPackageManagerObjProxy = Proxy.newProxyInstance(context.getClassLoader(), new Class[]{iPackageManagerClass}, interceptInvocationHandler);

        sPackageManagerField.set(null, iPackageManagerObjProxy);

    } catch (Exception e) {
        e.printStackTrace();
    }
}

private class InterceptPackageManagerHandler implements InvocationHandler {
    Object originalObject;

    public InterceptPackageManagerHandler(Object originalObject) {
        this.originalObject = originalObject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        LogUtils.d("method:" + method.getName() + " called with args:" + Arrays.toString(args));
        if (METHOD_GET_ACTIVITY_INFO.equals(method.getName())) {
            return new ActivityInfo();
        }
        return method.invoke(originalObject, args);
    }
}

AMS与ActivityThread通过token进行通信

虽然我们启动了Activity A或者B,但是AMS实际上还是以为我们启动的是ProxyActivity。不信的话,可以使用命令行查看。

adb shell dumpsys activity activities | grep mFocusedActivity

结果如下,可以看到,在AMS端记录的Activity实际上是ProxyActivity。

mFocusedActivity: ActivityRecord{4ff4194 u0 com.sososeen09.binder.hook/.ProxyActivity t5057}

那么通过这种Hook方式启动的Activity ,还具有完整的生命周期吗?

答案是肯定的。

我们知道Activity是不具有跨进程通讯的能力的,那么AMS是如何管理Activity,控制Activity的声明周期的呢?答案就是一个通过一个Ibinder类型的token变量来控制。ASM通过这个token来与ApplicationThread进行通讯,进行控制Activity的声明周期。在AMS那边,它以为token表示的是ProxyActivity,但是在客户端这边,token实际上指的是Activity A或者B。

这个token是在AMS在回到IApplicationThread的scheduleLaunchActivity方法中传递过来的第二个参数。

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) {

这个会存在ActivityClientRecord中。这个token和对应的ActivityClientRecord会以键值对的形式存储在ActivityThread的变量mActivities中。后面再对Activity进行生命周期方法调用的时候,均可以通过AMS端传过来的token来获取正确的Activity。

final ArrayMap<IBinder, ActivityClientRecord> mActivities = new ArrayMap<>();
AMS与ActivityThread中相同token对应不同的Activity.png

总结

前面讲了一大堆,我们现在来总结概括一下这个过程和原理。
再来看一下需求:

  1. 我们想启动一个Activity A页面,但是想要进入这个A页面必须是已经登录过的,如果没有登录的话就启动登录页面B,并且在B页面登录成功之后需要跳转到页面A

  2. Activity页面A、B均没有在清单文件中注册,但是要完成正常的跳转

为什么我们可以跳转到一个未在AndroidManifest.xml中声明的Activity中,而且可以根据不同的逻辑跳转到不同的页面呢?

调用startActivity方法启动一个目标Activity的时候,实际上会通过Instrumentation进行启动,再通过ActivityManagerService的本地代理对象调用ActivityManagerService的方法来启动一个Activity,这是一个IPC过程,在ActivityManagerService中会校验被启动的Activity的合法性,如果合法,会通过IPC过程调用ApplicationThread的方法,ApplicationThread是一个Binder对象,它的方法运行是在Binder线程池中的,所以需要采用一个Handler把方法调用切换到主线程,ApplicationThread通过发送消息,进而调用ActivityThread的handleLaunchActivity方法创建Activity,并执行Activity的生命周期方法。

传递到ActivityManagerService的被启动的Activity信息必须是声明过的,而如果我们想要启动一个没有在AndroidManifest.xml中声明的Activity,可以通过欺上瞒下的方法,hook ActivityManagerService在本地的代理对象,如果调用的是ActivityManagerProxy的startActivity方法,那么就更改这个Intent,替换成启动一个声明过的ProxyActivity,当ActivityManagerService校验完启动的合法性之后,会通过ApplicationThread调用到ActivityThread的一个叫做mH的Handler中来。当Handler收到消息的时候,会有一个消息分发的过程,如果给Handler设置了一个Callback,这个Callback的handleMessage方法就会先于Handler本身的handleMessage方法调用。所以可以想办法给这个叫做mH的Handler对象设置Callback,并且在Callback的handleMessage方法中从Message上面拿到相关的Intent信息,此时的Intent还是跳转到代理页面,可以根据当前是否登录,是否需要重定向到登录页面等对这个Intent进行相应的处理,比如设置为跳转到登录页或者真正想要跳转的页面,并由后续的mH的handleMessage来调用。

更多细节,请查看项目 apk-plugin-technology

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,961评论 5 473
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,444评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,009评论 0 333
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,082评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,101评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,271评论 1 278
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,738评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,395评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,539评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,434评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,481评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,160评论 3 317
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,749评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,816评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,038评论 1 256
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,548评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,140评论 2 341

推荐阅读更多精彩内容