Android插件化开发之Hook StartActivity方法

第一步、先爆项目demo照片,代码不多,不要怕


 
第二步、应该知道Java反射相关知识
如果不知道或者忘记的小伙伴请猛搓这里,Android插件化开发基础之Java反射机制研究  http://blog.csdn.net/u011068702/article/details/49863931

 
第三步、应该知道Java静态代理知识
如果不知道或者忘记的小伙伴请猛搓这里,Android插件化开发基础之静态代理模式  http://blog.csdn.net/u011068702/article/details/51765578
 
第四部、应该知道Java动态代理知识
如果不知道或者忘记的小伙伴请猛搓这里,Android插件化开发基础之Java动态代理(proxy)机制的简单例子   http://blog.csdn.net/u011068702/article/details/53185210
上面只有代码的解释,如果不是很清楚的小伙伴可以再去到网上搜一搜相关知识。
 
第五步、应该知道Android里面的ActivityThread类和Instrumentation类
 如果不知道或者忘记的小伙伴请猛搓这里,Android插件化开发之AMS与应用程序(客户端ActivityThread、Instrumentation、Activity)通信模型分析  http://blog.csdn.net/u011068702/article/details/53207039
希望认真看,知道ActivityThread和Instrumentation是干嘛的,方便下面分析。

第六步、理解hook,并且分析源码
如果我们自己创建代理对象,然后把原始对象替换为我们的代理对象,那么就可以在这个代理对象为所欲为了;修改参数,替换返回值,我们称之为Hook。
接下来我们来实现Hook掉startActivity这个方法,当每次调用这个方法的时候来做我们需要做的事情,我这里之打印了一些信息,读者可以更具自己的需求来修改,
我们的目的是拦截startActivity方法,有点类是spring 里面AOP的思想。
 
1、hook一般在哪里hook?
首先分析我们需要hook哪些对象,也就是说我们要hook的地方,什么样的对象比较好Hook呢?一般是容易找到和不容易改变的对象,这思路就来了,不改变一般在我们类的里面单例对象是唯一对象,静态变量我们一般加上final static 修饰,有了final就不会改变,不变模式里面比如String类,里面就很多final,我们更加这些找到hook的地方。
 
 
2、我们hook startActivity,分析startActivity到底是怎么实现的
我们每次Context.startActivity,由于Context的实现实际上是ContextImpl来实现的,;我们看ConetxtImpl类的startActivity方法:
 

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

我们可以看到startActivity方法最后还是通过Instrumentation对象来执行execStartActivity来实现的,如果你认真看了《Android插件化开发之AMS与应用程序(客户端ActivityThread、Instrumentation、Activity)通信模型分析 》上面这篇博客,我们知道ActivityThread就是主线程,也就是我们常说的UI线程,可以去更新UI,一个进程只有一个主线程,我们可以在这里hook.
我们需要Hook掉我们的主线程对象,把主线程对象里面的mInstrumentation给替换成我们修改过的代理对象;要替换主线程对象里面的字段
1)首先我们得拿到主线程对象的引用,如何获取呢?ActivityThread类里面有一个静态方法currentActivityThread可以帮助我们拿到这个对象类;但是ActivityThread是一个隐藏类,我们需要用反射去获取,代码如下:

 

    // 先获取到当前的ActivityThread对象
    Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
    Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
    currentActivityThreadMethod.setAccessible(true);
    Object currentActivityThread = currentActivityThreadMethod.invoke(null);

 

2)拿到这个currentActivityThread之后,我们需要修改它的mInstrumentation这个字段为我们的代理对象,我们先实现这个代理对象,由于JDK动态代理只支持接口,而这个Instrumentation是一个类,我们可以手动写静态代理类,覆盖掉原始的方法,代码如下
 

    package com.example.hookstartactivity;
     
    import java.lang.reflect.Method;
     
    import android.app.Activity;
    import android.app.Instrumentation;
    import android.app.Instrumentation.ActivityResult;
    import android.content.Context;
    import android.content.Intent;
    import android.os.Bundle;
    import android.os.IBinder;
    import android.util.Log;
     
     
    public class InstrumentationProxy extends Instrumentation {
     
     
         public static final String TAG = "InstrumentationProxy";
         public static final String EXEC_START_ACTIVITY = "execStartActivity";
        
         // ActivityThread里面原始的Instrumentation对象,这里千万不能写成mInstrumentation,这样写
         //抛出异常,已亲测试,所以这个地方就要注意了
         public Instrumentation oldInstrumentation;
         
         //通过构造函数来传递对象
         public InstrumentationProxy(Instrumentation mInstrumentation) {
             oldInstrumentation = mInstrumentation;
         }
     
     
         //这个方法是由于原始方法里面的Instrumentation有execStartActivity方法来定的
         public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target,
                 Intent intent, int requestCode, Bundle options) {
             Log.d(TAG, "\n打印调用startActivity相关参数: \n" + "who = [" + who + "], " +
                  "\ncontextThread = [" + contextThread + "], \ntoken = [" + token + "], " +
                  "\ntarget = [" + target + "], \nintent = [" + intent +
                  "], \nrequestCode = [" + requestCode + "], \noptions = [" + options + "]");
     
     
            Log.i(TAG, "------------hook  success------------->");
            Log.i(TAG, "这里可以做你在打开StartActivity方法之前的事情");
            Log.i(TAG, "------------hook  success------------->");
            Log.i(TAG, "");
                
            //由于这个方法是隐藏的,所以需要反射来调用,先找到这方法
            try {
                Method execStartActivity = Instrumentation.class.getDeclaredMethod(
                        EXEC_START_ACTIVITY,
                        Context.class, IBinder.class, IBinder.class, Activity.class,
                        Intent.class, int.class, Bundle.class);
                execStartActivity.setAccessible(true);
                return (ActivityResult) execStartActivity.invoke(oldInstrumentation, who,
                            contextThread, token, target, intent, requestCode, options);
                } catch (Exception e) {
                    //如果你在这个类的成员变量Instrumentation的实例写错mInstrument,代码讲会执行到这里来
                    throw new RuntimeException("if Instrumentation paramerter is mInstrumentation, hook will fail");
                }
         }
    }


3)然后用代理对象替换,代码如下

 

    package com.example.hookstartactivity;
    
    import java.lang.reflect.Field;
    import java.lang.reflect.Method;
     
     
    import android.app.Application;
    import android.app.Instrumentation;
    import android.util.Log;
     
     
    public class MyApplication extends Application {
     
     
        public static final String TAG = "MyApplication";
        public static final String ACTIVIT_THREAD = "android.app.ActivityThread";
        public static final String CURRENT_ACTIVITY_THREAD = "currentActivityThread";
        public static final String INSTRUMENTATION = "mInstrumentation";
        
        @Override
        public void onCreate() {
            try {
                 //这个方法一般是写在Application的oncreate函数里面,如果你写在activity里面的oncrate函数里面就已经晚了
                 attachContext();
            } catch (Exception e) {
                 e.printStackTrace();
            }
        }
        
        public static void attachContext() throws Exception{
            
            //获取当前的ActivityThread对象
            Class<?> activityThreadClass = Class.forName(ACTIVIT_THREAD);
            Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod(CURRENT_ACTIVITY_THREAD);
            currentActivityThreadMethod.setAccessible(true);
            Object currentActivityThread = currentActivityThreadMethod.invoke(null);
     
     
            //拿到在ActivityThread类里面的原始mInstrumentation对象
            Field mInstrumentationField = activityThreadClass.getDeclaredField(INSTRUMENTATION);
            mInstrumentationField.setAccessible(true);
            Instrumentation mInstrumentation = (Instrumentation) mInstrumentationField.get(currentActivityThread);
     
     
            //构建我们的代理对象
            Instrumentation evilInstrumentation = new InstrumentationProxy(mInstrumentation);
            
            //通过反射,换掉字段,注意,这里是反射的代码,不是Instrumentation里面的方法
            mInstrumentationField.set(currentActivityThread, evilInstrumentation);
            
            //做个标记,方便后面查看
            Log.i(TAG, "has go in MyApplication attachContext method");
        }
    }


要注意这个替换要在Application里面的oncreate方法里面去执行,如果到Activity方法里面去执行的话就晚了,程序不会报错,但是hook不到。


然后我是在主页面写了一个按钮,点击来触发startActivity的。
MainActivity.java 文件如下
 

    package com.example.hookstartactivity;
     
     
    import android.content.Intent;
    import android.os.Bundle;
    import android.support.v7.app.ActionBarActivity;
    import android.util.Log;
    import android.view.Menu;
    import android.view.MenuItem;
    import android.view.View;
    import android.view.View.OnClickListener;
    import android.widget.TextView;
     
     
    public class MainActivity extends ActionBarActivity {
        
        public static final String TAG  =  "MainActivity";
        public TextView tv;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            tv = (TextView)findViewById(R.id.start);
            tv.setOnClickListener(new OnClickListener(){
                @Override
                public void onClick(View v) {
                    try {
                        Intent intent = new Intent(MainActivity.this, SecondActivity.class);
                        Bundle bundle = new Bundle();
                        Log.i(TAG, "-------------------------------->");
                        Log.i(TAG, "startActivity before");
                        Log.i(TAG, "-------------------------------->");
                        
                        startActivity(intent, bundle);
                        
                        Log.i(TAG, "-------------------------------->");
                        Log.i(TAG, "startActivity after");
                        Log.i(TAG, "-------------------------------->");
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            } );
        }
    }

跳转到的Second.java文件如下
 

    package com.example.hookstartactivity;
     
     
    import android.os.Bundle;
    import android.support.v7.app.ActionBarActivity;
     
     
    public class SecondActivity extends ActionBarActivity{
        
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_second);
        }
    }
 
第七步、运行代码
启动项目在ubuntu终端打印的日志图片如下:
 


 
然后我点击图标调用startActivity方法后ubuntu终端打印的日志图片如下:
 


日志上面看到,hook success了
看到ubuntu终端日志打印,前面显示了类,方便通过日志找bug,我用的是pidcat,下载pidcat
然后在ubuntu上面运行

pidcat.py 包名

就可以非常方便看的日志找bug了。
 
第八步、总结
  通过这篇日志,希望打击可以更好的理解hook、java反射、静态代理、动态代理、ActivityThread、Instrumentation
  最后附上源码下载地址,需要的小伙伴请猛搓这里  HookStartActivityDemo
 
 



  作者:chen.yu
深信服三年半工作经验,目前就职游戏厂商,希望能和大家交流和学习,
微信公众号:编程入门到秃头 或扫描下面二维码
零基础入门进阶人工智能(链接)