Android插件化之startActivity hook的几种姿势

关于拦截activity的启动这个想法还是之前组内成员在做业务时碰到的一个小难题,具体的业务场景就是关于当前activity需要进行判断然后点击完button后跳转下一个activity也需要判断 然后弹出一个dialog。具体的业务不细说了,所以看MagiLu在某乎上的回答 最后在对于开发者的建议里面有句话是这样说的现在想想深表赞同

很多同学都想向更高阶进阶,但在实际的工作中,这是很困难的,我见过技术上很强的人都是用大型项目,不断解决其中的困难和问题,慢慢喂出来的。

看到这里 首先说下这篇文章比较基础 如果是插件化大神的话 就没有看下去的必要了 出门右转不送。。。

先不说插件化了 首先关于startActivity 我们平时会经常使用到 在activity内 直接startActivity(intent)
其实这里还有一种start方式 我们可能没怎么用过 getApplicationContext.startActivity(intent) 通过这种方式可以开启其它程序中的activity

  • 代理
    在说Hook之前先说下代理包括静态代理和动态代理如果不熟悉的可以看下
http://www.jianshu.com/p/8507b4364f11
  • 反射
    其次就是java反射方面的知识
    如何通过变量获取实例对象再进行替换

Hook:我们把修改参数,替换返回值,称之为Hook
Hook点:静态变量和单利;即就是不容易发生改变的对象,容易定位 普通的对象容易发生改变 所以我们可以根据这个原则去hook

1. 首先我们分析下我们平时直接使用的startActivity()方法

源码

@Override
    public void startActivity(Intent intent) {
        this.startActivity(intent, null);
    }
@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);
        }
    }
@Override
    public void startActivityForResult(
            String who, Intent intent, int requestCode, @Nullable Bundle options) {
        Uri referrer = onProvideReferrer();
        if (referrer != null) {
            intent.putExtra(Intent.EXTRA_REFERRER, referrer);
        }
        Instrumentation.ActivityResult ar =
            mInstrumentation.execStartActivity(
                this, mMainThread.getApplicationThread(), mToken, who,
                intent, requestCode, options);
        if (ar != null) {
            mMainThread.sendActivityResult(
                mToken, who, requestCode,
                ar.getResultCode(), ar.getResultData());
        }
        cancelInputsAndStartExitTransition(options);
    }

最终调用了startActivityForResult()方法 在方法内调用了mInstrumentation变量的execStartActivity()方法 所以最终startActivity的是Instrumentation类的execStartActivity()方法(nativie层面的)其实是通过远端的ActivityManagerService启动的 是一个跨进程通信的过程

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

这里呢 我们可以去hook掉activity类内变量 mInstrumentation 将它替换成我们自己的Instrumentation
通过手写静态代理类,替换掉原始的方法

public class EvilInstrumentation extends Instrumentation {

    private static final String TAG = "EvilInstrumentation";

    // ActivityThread中原始的对象, 保存起来
    Instrumentation mBase;

    public EvilInstrumentation(Instrumentation base) {
        mBase = base;
    }

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

        // Hook之前, XXX到此一游!
        Log.e(TAG, "\n执行了startActivity, 参数如下: \n" + "who = [" + who + "], " +
                "\ncontextThread = [" + contextThread + "], \ntoken = [" + token + "], " +
                "\ntarget = [" + target + "], \nintent = [" + intent +
                "], \nrequestCode = [" + requestCode + "], \noptions = [" + options + "]");
        Uri uri = Uri.parse("http://www.jianshu.com/u/3d228ed8d54d");
        Intent intent1 = new Intent(Intent.ACTION_VIEW,uri);
        // 开始调用原始的方法, 调不调用随你,但是不调用的话, 所有的startActivity都失效了.
        // 由于这个方法是隐藏的,因此需要使用反射调用;首先找到这个方法
        try {
            
            //这里通过反射找到原始Instrumentation类的execStartActivity方法
            Method execStartActivity = Instrumentation.class.getDeclaredMethod(
                    "execStartActivity",
                    Context.class, IBinder.class, IBinder.class, Activity.class,
                    Intent.class, int.class, Bundle.class);
            execStartActivity.setAccessible(true);
            
            //执行原始Instrumentation的execStartActivity方法 再次之前 you can do whatever you want ...
            return (ActivityResult) execStartActivity.invoke(mBase, who,
                    contextThread, token, target, intent1, requestCode, options);
        } catch (Exception e) {
            // 某该死的rom修改了  需要手动适配
            throw new RuntimeException("do not support!!! pls adapt it");
        }
    }
}

在这里我们可以拿到Intent 当然你可以将这个intent对象进行替换 跳转到另一个界面(whatever you want)

有了代理对象 下面使用反射直接进行替换

public static void replaceInstrumentation(Activity activity){
            Class<?> k = Activity.class;
        try {
            //通过Activity.class 拿到 mInstrumentation字段
            Field field = k.getDeclaredField("mInstrumentation");
            field.setAccessible(true);
            //根据activity内mInstrumentation字段 获取Instrumentation对象
            Instrumentation instrumentation = (Instrumentation)field.get(activity);
            //创建代理对象
            Instrumentation instrumentationProxy = new EvilInstrumentation(instrumentation);
            //进行替换
            field.set(activity,instrumentationProxy);
        } catch (IllegalAccessException e){
            e.printStackTrace();
        }catch (NoSuchFieldException e){
            e.printStackTrace();
        }

    }

有注释不用过多解释了

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        HookActivityHelper.replaceInstrumentation(this);
       Button tv = new Button(this);
        tv.setText("测试界面");
        setContentView(tv);
        tv.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(Intent.ACTION_VIEW);
                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                intent.setData(Uri.parse("http://www.baidu.com"));           
                startActivity(intent);
            }
        });
    }

测试当我们点击button后 跳转到了自己的简书主页而不是百度 同时看到log

image.png

可见 我们确实hook成功了

2.下面使用 getApplicationContext().startActivity(intent)这种方式开启另一个activity

Context开启activity 实质为 ContextImpl(Context的实现类)去真正开启的

final ActivityThread mMainThread;
 @Override
    public void startActivity(Intent intent, Bundle options) {
        warnIfCallingFromSystemProcess();
        if ((intent.getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) == 0) {
            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);
    }

可以看到是调用了ActivityThread类的getInstrumentation()方法返回mInstrumentation变量后 再调用execStartActivity()方法

Instrumentation mInstrumentation;
public Instrumentation getInstrumentation()
    {
        return mInstrumentation;
    }

整个流程 almost like this


未命名文件.png

所以现在变成了我们需要替换ActivityThread类(不是主线程类 final类只不过运行在主线程而已)中的mInstrumentation,首先我们需要拿到ActivityThread对象吧 刚好在ActivityThread类中有个静态方法currentActivityThread可以帮助我们拿到这个对象类

private static ActivityThread sCurrentActivityThread;
public static ActivityThread currentActivityThread() {
        return sCurrentActivityThread;
    }

so 我们的反射代码如下

      // 先获取到当前的ActivityThread对象
        Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
        Method currentActivityThreadMethod =       
        activityThreadClass.getDeclaredMethod("currentActivityThread");
        currentActivityThreadMethod.setAccessible(true);
        //currentActivityThread是一个static函数所以可以直接invoke,不需要带实例参数
        Object currentActivityThread = currentActivityThreadMethod.invoke(null);

拿到ActivityThread对象之后 后面的就和第一种方法一样了 将ActivityThread类中的mInstrumentaion换为EvilInstrumentation

其实比较这两种方法的区别:

  • Activity的startActivity 是替换 Activity类内部的mInstrumentation
  • Context的startActivity 是替换 ActivityThread类内部的mInstrumentation

还有一点就是拦截的时机
第一种方式在Activity的onCreate()方法内部拦截即可 而第二种方式需要在onCreate()方法之前attachBaseContext()方法中进行拦截
这里就牵扯到一些应用启动后的生命周期:当启动应用时,会孵化一个新进程,启动应用的启动入口为ActivityThread.main(),后面会新建一个ContextImpl实例给application,在后续的启动中,会进行activity的创建,其中ActivityThread会调用performLaunchActivity,先通过createBaseContextForActivity创建Context,然后再activity.attach()

在attach()方法中

final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor) {
        attachBaseContext(context);
        mInstrumentation = instr;
}

我们看到首先调用了attachBaseContext(context)这个方法
然后给成员mInstrumentation赋值
so 如果我们在onCreate()方法中去做hook是hook不到的 时机已晚

@Override
    protected void attachBaseContext(Context newBase) {
        super.attachBaseContext(newBase);
        try {
            Log.e("activity","attachBaseContext");
            // 在这里进行Hook
            HookHelper.attachContext();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

测试同样ok

3.前两种方法都是替换Instrumentation 这种方法我们看看在Instrumentation类中的execStartActivity()方法

 int result = ActivityManagerNative.getDefault()
                .startActivity(whoThread, who.getBasePackageName(), intent,
                        intent.resolveTypeIfNeeded(who.getContentResolver()),
                        token, target != null ? target.mEmbeddedID : null,
                        requestCode, 0, null, options);

真正开启的是这句 ActivityManagerNative.getDefault().startActivity()
首先ActivityManagerNative是个什么鬼 点开后发现

ublic abstract class ActivityManagerNative extends Binder implements IActivityManager

继承了Binder 实现了 IActivityManager 而且是个抽象类 类图如下

image.png

这张图得看半天
继续往下走

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;
        }

getDefault返回的是一个IActivityManager对象
这里gDefault借助Singleton类实现单利模式 gDefault为static final 形式 根据我们的hook原则 这是一个比较好hook的点

代码如下:

public class HookUtil {
    private static final String TAG = "HookUtil";
    public void hookIActivityManager() {
        try {
            Class<?> c = Class.forName("android.app.ActivityManagerNative");

            Field field = c.getDeclaredField("gDefault");

            field.setAccessible(true);

            //返回ActivityManagerNative对象
            Object amn = field.get(null);

            Class<?> k = Class.forName("android.util.Singleton");

            Field field1 = k.getDeclaredField("mInstance");

            field1.setAccessible(true);

            //拿到ActivityManagerProxy对象 代理ActivityManagerNative对象的子类ActivityManagerService
            //为什么不是IActivityManager对象
            //因为在gDefault对象的 实现方法 onCreate()方法中 asInterface(b)返回的是  return new ActivityManagerProxy(obj) 具体可以看源码

            Object iActivityManager = field1.get(amn);

            IamInvocationHandler iamInvocationHandler = new IamInvocationHandler(iActivityManager);

            Object object = Proxy.newProxyInstance(iamInvocationHandler.getClass().getClassLoader(),iActivityManager.getClass().getInterfaces(),iamInvocationHandler);

            field1.set(amn,object);

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


    class IamInvocationHandler implements InvocationHandler {

        Object iamObject;

        public IamInvocationHandler(Object iamObject) {
            this.iamObject = iamObject;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//            Log.e(TAG, method.getName());
            if ("startActivity".equals(method.getName())) {
                Log.e(TAG, "要开始启动了 啦啦啦啦啦啦  ");
            }
            return method.invoke(iamObject, args);
        }
    }
}
image.png

测试也没问题

so that's all thanks for your time

参考文章:
Weishu's Notes
thanks weishu在某乎上答疑

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

推荐阅读更多精彩内容