Android插件化開發(fā)之Hook StartActivity方法

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


 
第二步、應(yīng)該知道Java反射相關(guān)知識
如果不知道或者忘記的小伙伴請猛搓這里,Android插件化開發(fā)基礎(chǔ)之Java反射機制研究  http://blog.csdn.net/u011068702/article/details/49863931

 
第三步、應(yīng)該知道Java靜態(tài)代理知識
如果不知道或者忘記的小伙伴請猛搓這里,Android插件化開發(fā)基礎(chǔ)之靜態(tài)代理模式  http://blog.csdn.net/u011068702/article/details/51765578
 
第四部、應(yīng)該知道Java動態(tài)代理知識
如果不知道或者忘記的小伙伴請猛搓這里,Android插件化開發(fā)基礎(chǔ)之Java動態(tài)代理(proxy)機制的簡單例子   http://blog.csdn.net/u011068702/article/details/53185210
上面只有代碼的解釋,如果不是很清楚的小伙伴可以再去到網(wǎng)上搜一搜相關(guān)知識。
 
第五步、應(yīng)該知道Android里面的ActivityThread類和Instrumentation類
 如果不知道或者忘記的小伙伴請猛搓這里,Android插件化開發(fā)之AMS與應(yīng)用程序(客戶端ActivityThread、Instrumentation、Activity)通信模型分析  http://blog.csdn.net/u011068702/article/details/53207039
希望認真看,知道ActivityThread和Instrumentation是干嘛的,方便下面分析。

第六步、理解hook,并且分析源碼
如果我們自己創(chuàng)建代理對象,然后把原始對象替換為我們的代理對象,那么就可以在這個代理對象為所欲為了;修改參數(shù),替換返回值,我們稱之為Hook。
接下來我們來實現(xiàn)Hook掉startActivity這個方法,當每次調(diào)用這個方法的時候來做我們需要做的事情,我這里之打印了一些信息,讀者可以更具自己的需求來修改,
我們的目的是攔截startActivity方法,有點類是spring 里面AOP的思想。
 
1、hook一般在哪里hook?
首先分析我們需要hook哪些對象,也就是說我們要hook的地方,什么樣的對象比較好Hook呢?一般是容易找到和不容易改變的對象,這思路就來了,不改變一般在我們類的里面單例對象是唯一對象,靜態(tài)變量我們一般加上final static 修飾,有了final就不會改變,不變模式里面比如String類,里面就很多final,我們更加這些找到hook的地方。
 
 
2、我們hook startActivity,分析startActivity到底是怎么實現(xiàn)的
我們每次Context.startActivity,由于Context的實現(xiàn)實際上是ContextImpl來實現(xiàn)的,;我們看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對象來執(zhí)行execStartActivity來實現(xiàn)的,如果你認真看了《Android插件化開發(fā)之AMS與應(yīng)用程序(客戶端ActivityThread、Instrumentation、Activity)通信模型分析 》上面這篇博客,我們知道ActivityThread就是主線程,也就是我們常說的UI線程,可以去更新UI,一個進程只有一個主線程,我們可以在這里hook.
我們需要Hook掉我們的主線程對象,把主線程對象里面的mInstrumentation給替換成我們修改過的代理對象;要替換主線程對象里面的字段
1)首先我們得拿到主線程對象的引用,如何獲取呢?ActivityThread類里面有一個靜態(tài)方法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這個字段為我們的代理對象,我們先實現(xiàn)這個代理對象,由于JDK動態(tài)代理只支持接口,而這個Instrumentation是一個類,我們可以手動寫靜態(tài)代理類,覆蓋掉原始的方法,代碼如下
 

    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;
         
         //通過構(gòu)造函數(shù)來傳遞對象
         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打印調(diào)用startActivity相關(guān)參數(shù): \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, "");
                
            //由于這個方法是隱藏的,所以需要反射來調(diào)用,先找到這方法
            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,代碼講會執(zhí)行到這里來
                    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函數(shù)里面,如果你寫在activity里面的oncrate函數(shù)里面就已經(jīng)晚了
                 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);
     
     
            //構(gòu)建我們的代理對象
            Instrumentation evilInstrumentation = new InstrumentationProxy(mInstrumentation);
            
            //通過反射,換掉字段,注意,這里是反射的代碼,不是Instrumentation里面的方法
            mInstrumentationField.set(currentActivityThread, evilInstrumentation);
            
            //做個標記,方便后面查看
            Log.i(TAG, "has go in MyApplication attachContext method");
        }
    }


要注意這個替換要在Application里面的oncreate方法里面去執(zhí)行,如果到Activity方法里面去執(zhí)行的話就晚了,程序不會報錯,但是hook不到。


然后我是在主頁面寫了一個按鈕,點擊來觸發(fā)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();
                    }
                }
            } );
        }
    }

跳轉(zhuǎn)到的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終端打印的日志圖片如下:
 


 
然后我點擊圖標調(diào)用startActivity方法后ubuntu終端打印的日志圖片如下:
 


日志上面看到,hook success了
看到ubuntu終端日志打印,前面顯示了類,方便通過日志找bug,我用的是pidcat,下載pidcat
然后在ubuntu上面運行

pidcat.py 包名

就可以非常方便看的日志找bug了。
 
第八步、總結(jié)
  通過這篇日志,希望打擊可以更好的理解hook、java反射、靜態(tài)代理、動態(tài)代理、ActivityThread、Instrumentation
  最后附上源碼下載地址,需要的小伙伴請猛搓這里  HookStartActivityDemo
 
 



  作者:chen.yu
深信服三年半工作經(jīng)驗,目前就職游戲廠商,希望能和大家交流和學(xué)習(xí),
微信公眾號:編程入門到禿頭 或掃描下面二維碼
零基礎(chǔ)入門進階人工智能(鏈接)