Android插件化原理解析——Hook機制之動態(tài)代理

使用代理機制進行API Hook進而達到方法增強是框架的常用手段差导,比如J2EE框架Spring通過動態(tài)代理優(yōu)雅地實現(xiàn)了AOP編程设褐,極大地提升了Web開發(fā)效率助析;同樣椅您,插件框架也廣泛使用了代理機制來增強系統(tǒng)API從而達到插件化的目的掀泳。本文將帶你了解基于動態(tài)代理的Hook機制员舵。

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

代理是什么

為什么需要代理呢凰浮?其實這個代理與日常生活中的“代理”袜茧,“中介”差不多笛厦;比如你想海淘買東西俺夕,總不可能親自飛到國外去購物吧,這時候我們使用第三方海淘服務比如惠惠購物助手等姨谷;另外梦湘,還是購物,有時候第三方購物會有折扣比如當初的米折網(wǎng)哼拔,這時候我們可以少花點錢倦逐;當然有時候這個“代理”比較坑檬姥,坑我們的錢粉怕,坑我們的貨斋荞。

從這個例子可以看出來平酿,代理可以實現(xiàn)方法增強,比如常用的日志,緩存等筑辨;也可以實現(xiàn)方法攔截棍辕,代理方法修改原方法的參數(shù)和返回值實現(xiàn)某種不可告人的目的~接下來我們用代碼解釋一下楚昭。

靜態(tài)代理

靜態(tài)代理拍顷,是最原始的代理方式;假設我們有一個購物的接口电媳,如下:

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

它有一個原始的實現(xiàn)匾乓,我們可以理解為親自拼缝,直接去商店購物:

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

好了搂根,現(xiàn)在我們自己沒時間但是需要買東西剩愧,于是我們就找了個代理幫我們買:

public class ProxyShopping implements Shopping {

    Shopping base;

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

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

        // 先黑點錢(修改輸入?yún)?shù))
        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;
    }

很不幸仁卷,我們找的這個代理有點坑锦积,坑了我們的錢還坑了我們的貨丰介;先忍忍鉴分。

動態(tài)代理

傳統(tǒng)的靜態(tài)代理模式需要為每一個需要代理的類寫一個代理類志珍,如果需要代理的類有幾百個那不是要累死柜某?為了更優(yōu)雅地實現(xiàn)代理模式,JDK提供了動態(tài)代理方式喂击,可以簡單理解為在JVM可以在運行時幫我們動態(tài)生成一系列的代理類惭等,這樣我們就不需要手寫每一個靜態(tài)的代理類了辞做。還是購物的那個例子秤茅,用動態(tài)代理實現(xiàn)如下:

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

動態(tài)代理主要處理InvocationHandlerProxy類课幕;完整代碼可以見github

代理Hook

我們知道代理有比原始對象更強大的能力乍惊,比如飛到國外買東西放仗,比如坑錢坑貨诞挨;那么很自然,如果我們自己創(chuàng)建代理對象棍郎,然后把原始對象替換為我們的代理對象涂佃,那么就可以在這個代理對象為所欲為了巡李;修改參數(shù),替換返回值侨拦,我們稱之為Hook狱从。

下面我們Hook掉startActivity這個方法季研,使得每次調用這個方法之前輸出一條日志与涡;(當然驼卖,這個輸入日志有點點弱酌畜,只是為了展示原理,如果你想可以替換參數(shù)桥胞,攔截這個startActivity過程恳守,使得調用它導致啟動某個別的Activity,指鹿為馬7废骸)

首先我們得找到被Hook的對象催烘,我稱之為Hook點;什么樣的對象比較好Hook呢缎罢?自然是容易找到的對象伊群。什么樣的對象容易找到?靜態(tài)變量和單例屁使;在一個進程之內(nèi)在岂,靜態(tài)變量和單例變量是相對不容易發(fā)生變化的奔则,因此非常容易定位蛮寂,而普通的對象則要么無法標志抽莱,要么容易改變。我們根據(jù)這個原則找到所謂的Hook點象泵。

然后我們分析一下startActivity的調用鏈忽孽,找出合適的Hook點。我們知道對于Context.startActivity(Activity.startActivity的調用鏈與之不同),由于Context的實現(xiàn)實際上是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);
}

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

接下來就是想要Hook掉我們的主線程對象,也就是把這個主線程對象里面的mInstrumentation給替換成我們修改過的代理對象糜颠;要替換主線程對象里面的字段夸政,首先我們得拿到主線程對象的引用,如何獲取呢?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);

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

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執(zhí)行了startActivity, 參數(shù)如下: \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");
        }
    }
}

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);
        
        // 創(chuàng)建代理對象
        Instrumentation evilInstrumentation = new EvilInstrumentation(mInstrumentation);
        
        // 偷梁換柱
        mInstrumentationField.set(currentActivityThread, evilInstrumentation);
    }

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

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

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

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

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

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末澳盐,一起剝皮案震驚了整個濱河市祈纯,隨后出現(xiàn)的幾起案子令宿,更是在濱河造成了極大的恐慌,老刑警劉巖腕窥,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件粒没,死亡現(xiàn)場離奇詭異,居然都是意外死亡簇爆,警方通過查閱死者的電腦和手機革娄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來冕碟,“玉大人拦惋,你說我怎么就攤上這事“菜拢” “怎么了厕妖?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長挑庶。 經(jīng)常有香客問我言秸,道長,這世上最難降的妖魔是什么迎捺? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任举畸,我火速辦了婚禮,結果婚禮上凳枝,老公的妹妹穿的比我還像新娘抄沮。我一直安慰自己,他們只是感情好岖瑰,可當我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布叛买。 她就那樣靜靜地躺著,像睡著了一般蹋订。 火紅的嫁衣襯著肌膚如雪率挣。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天露戒,我揣著相機與錄音椒功,去河邊找鬼。 笑死智什,一個胖子當著我的面吹牛动漾,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播撩鹿,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼谦炬,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了节沦?” 一聲冷哼從身側響起键思,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎甫贯,沒想到半個月后吼鳞,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡叫搁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年赔桌,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片渴逻。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡疾党,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出惨奕,到底是詐尸還是另有隱情雪位,我是刑警寧澤,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布梨撞,位于F島的核電站雹洗,受9級特大地震影響,放射性物質發(fā)生泄漏卧波。R本人自食惡果不足惜时肿,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望港粱。 院中可真熱鬧螃成,春花似錦、人聲如沸查坪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽咪惠。三九已至击吱,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間遥昧,已是汗流浹背覆醇。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留炭臭,地道東北人永脓。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像鞋仍,于是被迫代替她去往敵國和親常摧。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內(nèi)容