插件框架原理解析——Hook機制之動態代理

來自: http://weishu.me/2016/01/28/understand-plugin-framework-proxy-hook/

使用代理機制進行API Hook進而達到方法增強是框架的常用手段,比如J2EE框架Spring通過動態代理優雅地實現了AOP編程,極大地提升了Web開發效率;同樣,插件框架也廣泛使用了代理機制來增強系統API從而達到插件化的目的。本文將帶你了解基于動態代理的Hook機制。

閱讀本文之前,可以先clone一份 understand-plugin-framework ,參考此項目的 dynamic-proxy-hook 模塊。另外,插件框架原理解析系列文章見索引。

代理是什么

為什么需要代理呢?其實這個代理與日常生活中的“代理”,“中介”差不多;比如你想海淘買東西,總不可能親自飛到國外去購物吧,這時候我們使用第三方海淘服務比如惠惠購物助手等;另外,還是購物,有時候第三方購物會有折扣比如當初的米折網,這時候我們可以少花點錢;當然有時候這個“代理”比較坑,坑我們的錢,坑我們的貨。

從這個例子可以看出來,代理可以實現 方法增強 ,比如常用的 日志 , 緩存 等;也可以實現方法攔截,代理方法修改原方法的參數和返回值實現某種不可告人的目的~接下來我們用代碼解釋一下。

靜態代理

靜態代理,是最原始的代理方式;假設我們有一個購物的接口,如下:

public interface Shopping {
    Object[] doShopping(long money);
}

</div>

它有一個原始的實現,我們可以理解為親自,直接去商店購物:

public class ShoppingImpl implements Shopping {
    @Override
    public Object[] doShopping(long money) {
        System.out.println("逛淘寶 ,逛商場,買買買!!");
        System.out.println(String.format("花了%s塊錢", money));
        return new Object[] { "鞋子", "衣服", "零食" };
    }
}

</div>

好了,現在我們自己沒時間但是需要買東西,于是我們就找了個代理幫我們買:

public class ProxyShopping implements Shopping {

    Shopping base;

    ProxyShopping(Shopping base) {
        this.base = base;
    }

    @Override
    public Object[] doShopping(long money) {

        // 先黑點錢(修改輸入參數)
        long readCost = (long) (money * 0.5);

        System.out.println(String.format("花了%s塊錢", readCost));

        // 幫忙買東西
        Object[] things = base.doShopping(readCost);

        // 偷梁換柱(修改返回值)
        if (things != null && things.length > 1) {
            things[0] = "被掉包的東西!!";
        }

        return things;
    }

</div>

很不幸,我們找的這個代理有點坑,坑了我們的錢還坑了我們的貨;先忍忍。

動態代理

傳統的靜態代理模式需要為每一個需要代理的類寫一個代理類,如果需要代理的類有幾百個那不是要累死?為了更優雅地實現代理模式,JDK提供了動態代理方式,可以簡單理解為在JVM可以在運行時幫我們動態生成一系列的代理類,這樣我們就不需要手寫每一個靜態的代理類了。還是購物的那個例子,用動態代理實現如下:

public static void main(String[] args) {
    Shopping women = new ShoppingImpl
    // 正常購物
    System.out.println(Arrays.toString(women.doShopping(100)
    // 招代理
    women = (Shopping) Proxy.newProxyInstance(Shopping.class.getClassLoader(), 
            women.getClass().getInterfaces(), new ShoppingHandler(women
    System.out.println(Arrays.toString(women.doShopping(100)));
}

</div>

動態代理主要處理 InvocationHandler 和 Proxy 類;完整代碼可以見 github

代理Hook

我們知道代理有比原始對象更強大的能力,比如飛到國外買東西,比如坑錢坑貨;那么很自然,如果我們自己創建代理對象,然后把原始對象替換為我們的代理對象,那么就可以在這個代理對象為所欲為了;修改參數,替換返回值,我們稱之為Hook。

下面我們Hook掉 startActivity 這個方法,使得每次調用這個方法之前輸出一條日志;(當然,這個輸入日志有點點弱,只是為了展示原理,如果你想可以替換參數,攔截這個 startActivity 過程,使得調用它導致啟動某個別的Activity,指鹿為馬!)

首先我們得找到被Hook的對象,我稱之為Hook點;什么樣的對象比較好Hook呢?自然是 容易找到的對象 。什么樣的對象容易找到? 靜態變量和單例 ;在一個進程之內,靜態變量和單例變量是相對不容易發生變化的,因此非常容易定位,而普通的對象則要么無法標志,要么容易改變。我們根據這個原則找到所謂的Hook點。

然后我們分析一下 startActivity 的調用鏈,找出合適的Hook點。我們知道對于 Context.startActivity (Activity.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);
}

</div>

這里,實際上使用了 ActivityThread 類的 mInstrumentation 成員的 execStartActivity 方法;注意到, ActivityThread 實際上是主線程,而主線程一個進程只有一個,因此這里是一個良好的Hook點。

接下來就是想要Hook掉我們的主線程對象,也就是把這個主線程對象里面的 mInstrumentation 給替換成我們修改過的代理對象;要替換主線程對象里面的字段,首先我們得拿到主線程對象的引用,如何獲取呢? ActivityThread 類里面有一個靜態方法 currentActivityThread 可以幫助我們拿到這個對象類;但是 ActivityThread 是一個隱藏類,我們需要用反射去獲取,代碼如下:

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

</div>

拿到這個 currentActivityThread 之后,我們需要修改它的 mInstrumentation 這個字段為我們的代理對象,我們先實現這個代理對象,由于JDK動態代理只支持接口,而這個 Instrumentation 是一個類,沒辦法,我們只有手動寫靜態代理類,覆蓋掉原始的方法即可。( cglib 可以做到基于類的動態代理,這里先不介紹)

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.d(TAG, "\n執行了startActivity, 參數如下: \n" + "who = [" + who + "], " +
                "\ncontextThread = [" + contextThread + "], \ntoken = [" + token + "], " +
                "\ntarget = [" + target + "], \nintent = [" + intent +
                "], \nrequestCode = [" + requestCode + "], \noptions = [" + options + "]");

        // 開始調用原始的方法, 調不調用隨你,但是不調用的話, 所有的startActivity都失效了.
        // 由于這個方法是隱藏的,因此需要使用反射調用;首先找到這個方法
        try {
            Method execStartActivity = Instrumentation.class.getDeclaredMethod(
                    "execStartActivity",
                    Context.class, IBinder.class, IBinder.class, Activity.class, 
                    Intent.class, int.class, Bundle.class);
            execStartActivity.setAccessible(true);
            return (ActivityResult) execStartActivity.invoke(mBase, who, 
                    contextThread, token, target, intent, requestCode, options);
        } catch (Exception e) {
            // 某該死的rom修改了 需要手動適配
            throw new RuntimeException("do not support!!! pls adapt it");
        }
    }
}

</div>

Ok,有了代理對象,我們要做的就是偷梁換柱!代碼比較簡單,采用反射直接修改:

public static void attactContext() throws Exception{
        // 先獲取到當前的ActivityThread對象
        Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
        Field currentActivityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread");
        currentActivityThreadField.setAccessible(true);
        Object currentActivityThread = currentActivityThreadField.get(null);

        // 拿到原始的 mInstrumentation字段
        Field mInstrumentationField = activityThreadClass.getField("mInstrumentation");
        mInstrumentationField.setAccessible(true);
        Instrumentation mInstrumentation = (Instrumentation) mInstrumentationField.get(currentActivityThread);

        // 創建代理對象
        Instrumentation evilInstrumentation = new EvilInstrumentation(mInstrumentation);

        // 偷梁換柱
        mInstrumentationField.set(currentActivityThread, evilInstrumentation);
    }

</div>

好了,我們啟動一個Activity測試一下,結果如下:

可見,Hook確實成功了!這就是使用代理進行Hook的原理——偷梁換柱。整個Hook過程簡要總結如下:

  1. 尋找Hook點,原則是靜態變量或者單例對象,盡量Hook pulic的對象和方法,非public不保證每個版本都一樣,需要適配。
  2. 選擇合適的代理方式,如果是接口可以用動態代理;如果是類可以手動寫代理也可以使用cglib。
  3. 偷梁換柱——用代理對象替換原始對象

完整代碼參照: understand-plugin-framework ;里面留有一個作業:我們目前僅Hook了 Context 類的 startActivity 方法,但是 Activity 類卻使用了自己的 mInstrumentation ;你可以嘗試Hook掉Activity類的 startActivity 方法。

喜歡就點個贊吧~持續更新,請關注github項目 understand-plugin-framework 和我的博客!

</div>

 本文由用戶 xuayzhou15 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!