1. 程式人生 > >Xposed模塊開發教程

Xposed模塊開發教程

git 導入 idg 錯誤 過程 called true 而且 pda

轉:http://vbill.github.io/2015/02/10/xposed-1/

http://blog.csdn.net/zhangmiaoping23/article/details/54891387

原文地址。這是開發者所寫的,可以說是官方開發指南。文章講述了Xposed的原理,以及怎麽開發Xposed框架的模塊。頭一次翻譯技術文檔,有錯誤的話請多包涵。

好了,你想學習怎麽為Xposed開發新的模塊麽?那麽讀讀這篇教程(或者我們可以稱他為”泛讀短文”)學著怎麽去做。這不僅包括“創建這個文件然後插入…”這類的技巧,也包括這些技巧背後的思想。這些思想正是創造價值的步驟以及你真正需要了解你做了什麽和為什麽這麽做的原因。如果你覺得本文“太長,不想讀”,那麽你可以只看最後的源代碼和閱讀“使工程成為Xposed模塊“部分。但是如果你讀了整篇文章你就會有更好的理解。你之後會節省出來閱讀這個的時間,因為你不必憑自己弄清楚每件事。

修改主題


你將重新創建在github上可以找到的紅色鐘表的的例子。它包括將狀態欄的鐘表變為紅色並且加入一個笑臉的功能。我選擇這個例子是因為它非常小,而且容易看見所做的修改。並且,它也使用了框架所提供的一些基本方法。

Xposed如何工作


在你開始做出自己的修改之前,你應當大致了解Xposed如何工作(如果覺得這部分無聊可以跳過)。以下就是原理:

有一個叫做”Zygote”的進程,它是android運行環境的核心。每個應用都從一份它的拷貝(“fork”)產生。這個進程在手機啟動時由一個叫 /init.rc 的腳本啟動。這個進程的啟動在 /system/bin/app_process 加載所需要的類和調用初始化方法後完成。

這裏就是Xposed發揮用處的地方了。當你安裝完框架後,一個擴展過的app_process就會被復制到 /system/bin 下。這個擴展過的啟動進程會將一個額外的jar包添加到環境變量,並在特定場合調用裏面的方法。比如:當虛擬機創建完成後和Zygote的main方法被調用前。並且在那個方法當中,我們已經是Zygote的一部分,而且能夠在它的上下文context中活動。

jar包的位置是 /data/data/de.robv.android.xposed.installer/bin/XposedBridge.jar 它的源代碼可以在這裏找到。查看XposedBridge的類,你能找到main方法。這就是我上文中所寫過的,它在每個進程的最開始部分被調用。一些初始化的工作在那裏完成,並且我們的模塊在那裏加載(之後我再講模塊的加載)。

方法的hook/替換


真正使Xpoesed有威力的就是hook方法調用。當你反編譯並修改APK時,你能夠在任何你想的地方直接修改/替換指令。然而,你事後需要重新編譯/給APK簽名,並且只能發布整個安裝包。使用Xposed能讓你放置的hook,你並不能修改程序內部的方法代碼(清楚地定義你想要在何處做什麽樣的修改是不可能的)。然而,你可以在方法調用的前後註入你的代碼。這也是java中能夠被清楚尋址的最小單位。

XposedBridge 有一個私有的 native 方法叫做 hookMethodNative。這個方法也在擴展後的 app_process 中被實現了。它會將方法類型轉為“native”,並把方法的實現與本地的通用方法相連。這意味著,每當被hook的方法調用後,調用者不知道實際調用的是通用的方法。在這個方法中,位於 XposedBridge 的 handleHookedMethod 方法會被調用,並向方法調用傳遞參數、this指針以及其他東西。之後這個方法負責喚起之前方法調用註冊過的回調。上述這些行為能夠改變調用的參數、實例/靜態變量、喚起其他方法、處理調用結果。。。或者跳過這些東西。它的彈性非常大。

好了,理論講夠了。我們現在創建一個模塊吧!

創建工程


一個模塊就是一個普通的app,只不過多了一些特殊的文件和元數據。所以在我們創建新的android工程以前,我假設你已經做過這個了。如果沒有,官方文檔講的很詳細。對於SDK,我選擇了4.0.3(API15)。我建議你也使用這個,並且不要立刻開始。你不需要創建Activity,因為我們的修改不需要任何用戶界面。回答過了這個問題後,你應該有一個空白的工程項目。

使工程成為Xposed模塊


現在我們把工程變成Xposed能加載的東西。我們需要以下幾個步驟。

AndroidManifest.xml

Xposed Installer的模塊列表搜尋所有有一種特殊元數據標記的應用程序。你可以到 AndroidManifest.xml => Application => Application Nodes (在底部) => Add => Meta Data 下面去創建這個標記。標記名稱應該是 xposedmodule ,值應該是 true。給resource留空。重復以上過程創建 xposedminversion (見下文) 和 xposeddescription (你創建的模塊的簡單描述)。XML文件現在就是這個樣子:

<?xml version="1.0" encoding="utf-8"?>
 <manifest  xmlns:android="http://schemas.android.com/apk/res/android"
    package="de.robv.android.xposed.mods.tutorial"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk android:minSdkVersion="15" />

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <meta-data
            android:name="xposedmodule"
            android:value="true" />
        <meta-data
            android:name="xposeddescription"
            android:value="Easy example which makes the status bar clock red and adds a smiley" />
        <meta-data
            android:name="xposedminversion"
            android:value="30" />
    </application>
</manifest>

XposedBridgeApi.jar

接下來,讓程序能夠找到 XposedBridge 的API。你可以從
這裏下載 XposedBridgeApi-<version>.jar 的最新版。把它復制到叫做lib的子文件夾下。右鍵單擊選擇Build Path => Add to Build Path。文件名當中的<version>是你在manifest文件的xposedminversion標簽所插入的版本。

保證API類沒有被包含(但僅僅是參考)在你編譯過的APK裏,否則你會得到一個IllegalAccessError錯誤。libs(含有s)文件夾是eclipse自動生成的,不要把API文件放在那裏。

模塊的實現

現在你可以給你的模塊創建一個類了。我的類叫做”Tutorial”,位於de.robv.android.xposed.mods.tutorial這個包中。

package de.robv.android.xposed.mods.tutorial;

public class Tutorial {

}

第一步,我們僅僅生成一些日誌表明模塊已經加載。一個模塊可以有多個入口點。你選擇哪個取決於你想修改什麽。你可以在安卓系統啟動時、在一個app將要啟動時、在一個app的資源文件初始化時或其他時候,調用一個函數。

在這個教程靠後面的一部分,你將了解到在一個特定的app中需要做出的修改。那麽先讓我們了解一下 “讓我知道什麽時候加載一個新app” 這個入口點。所有入口點都被標記為IXposedMod的子接口。這種情況下,你需要實現 IXposedHookLoadPackage 這個接口。其實它只有一個僅有一個參數的方法。這個方法向被實現的模塊提供更多關於運行環境上下文的信息。在我們的例子中,我們用log輸出加載的app的名稱。

package de.robv.android.xposed.mods.tutorial;

import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam;

public class Tutorial implements IXposedHookLoadPackage {
    public void handleLoadPackage(final LoadPackageParam lpparam) throws Throwable {
        XposedBridge.log("Loaded app: " + lpparam.packageName);
    }
}

這個log方法向標準logcat以及 /data/data/de.robv.android.xposed.installer/log/debug.log(通過Xposed Installer可以輕易訪問到)輸出信息(tag Xposed)。

assets/xposed_init

現在唯一遺漏的就是提示XposedBridge哪些類包含了入口點。這項工作通過一個叫 xposed_init 的文件完成。在assets文件夾下創建一個新的名叫xposed_init的text文件。在該文件中每行包含一個類的全名。在這個例子中,它是 de.robv.android.xposed.mods.tutorial.Tutorial。

試試看


保存你的文件。以Android Application的方式運行你的程序。因為這是你第一次安裝它,在使用前你需要先啟用。打開Xposed Installer這個app並確保你安裝了xposed框架。之後切換到Modules標簽。你應該能在那裏找到你的app。在選擇框內打鉤使得它可用。然後重啟。你當然什麽變化也看不到,但如果檢查log記錄,以應該會看見以下的東西:

Loading Xposed (for Zygote)...
Loading modules from   /data/app/de.robv.android.xposed.mods.tutorial-1.apk
Loading class de.robv.android.xposed.mods.tutorial.Tutorial
Loaded app: com.android.systemui
Loaded app: com.android.settings
... (many more apps follow)

瞧!它起作用了。現在你擁有了一個Xposed模塊。它能夠變得比寫一些log更加有用…

探索你的目標並尋找修改它的方式


好了,下面要開始講的部分也許會非常不同,這取決於你想做什麽。如果你之前修改過apk,也許你會知道在這裏應當如何思考。總的來說,你需要了解目標的一些實現細節。在本教程中,目標選定為狀態欄的時鐘。這有助於了解到狀態欄以及其他一些東西都是系統UI的一部分。現在讓我們在那裏開始我們的探索。

可能性1:反匯編。這會告訴你它實際的實現,但是會很難閱讀和理解,因為你得到的都是smali格式的東西。可能性2:獲得AOSP源代碼。比如這裏,這裏。ROM不同代碼也很不一樣,但在本例中他們的實現是相似的甚至是相同的。我會先看AOSP,然後看看這麽做夠不夠。如果我需要細節,我會看看實際的反匯編的代碼。

你可以找找名稱中有“clock”的類。其他需要找的是用到的資源和布局。如果你下載官方的AOSP代碼,你可以從 frameworks/base/packages/SystemUI 開始找。你會找到好幾處“clock”出現的地方。找到有好幾種方式實現修改是正常而且真實的。時刻記住你“只能” hook方法。所以你必須另找其他能夠插入代碼實現功能的地方,要麽在方法的前面或是後面,或者是替換掉方法。你應當hook一個盡可能明確的方法,而不是一個被調用成千上萬次的用於解決性能問題和非計劃中的副作用的方法。

在本例當中,你或許會發現布局 res/layout/status_bar.xml 包含一個指向帶有類com.android.systemui.statusbar.policy.Clock的自定義view。現在你腦子裏也許會有好多點子。文字的顏色是通過textAppearance屬性定義的,所以最幹凈的更改它的方法就是修改外觀的定義。然而,用Xposed框架改變外觀屬性幾乎是不可能的(這需要深入本地代碼)。替換狀態欄的布局也許是可能的,但對於你試圖做出的小小修改來說是殺雞用牛刀。取而代之的是,看看這個類。有一個叫updateLock的方法,似乎每分鐘都調用一次用於更新時間。

final void updateClock() {
    mCalendar.setTimeInMillis(System.currentTimeMillis());
    setText(getSmallTime());
}

這個方法用於修改來說是很好的,因為這是一個足夠具體的看似唯一能夠修改時鐘文字的方法。如果我們在這個方法的每次調用之後都加些修改時鐘文字和顏色的東西,應該就能起作用。那麽,我們開始做吧。

對於單獨修改字體顏色部分,有一種更好的辦法。參見“替換資源”中“修改布局”的例子。

使用反射尋找並hook方法


現在我們已經知道了哪些東西?我們在com.android.systemui.statusbar.policy.Clock有一個叫做updateClock的希望幹涉的方法。我們在系統UI資源中找到了這個類,所以它僅僅在系統UI進程當中有效。其它一些類屬於框架,而且在任何地方都有效。如果我們在 handleLoadPackage 中試圖直接獲取任何這個類的信息和引用,就會失敗。因為處於錯誤的進程中。所以讓我們實現一種僅在某個特定包即將加載時執行特定代碼的情況:

public void handleLoadPackage(LoadPackageParam lpparam) throws Throwable {
    if (!lpparam.packageName.equals("com.android.systemui"))
        return;

    XposedBridge.log("we are in SystemUI!");
}

使用參數,我們能輕松檢查是否在正確的包中。一旦我們核實了這一點,我們就通過ClassLoader取得那個包中的也被這個變量引用的類。現在我們可以尋找com.android.systemui.statusbar.policy.Clock這個類以及它的updateClock方法,然後告訴XposedBridge去hook這個方法:

package de.robv.android.xposed.mods.tutorial;

import static de.robv.android.xposed.XposedHelpers.findAndHookMethod;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam;

public class Tutorial implements IXposedHookLoadPackage {
    public void handleLoadPackage(final LoadPackageParam lpparam) throws Throwable {
        if (!lpparam.packageName.equals("com.android.systemui"))
            return;

        findAndHookMethod("com.android.systemui.statusbar.policy.Clock", lpparam.classLoader, "updateClock", new XC_MethodHook() {
                @Override
                protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                // this will be called before the clock was updated by the original method
            }
            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                // this will be called after the clock was updated by the original method
            }
    });
    }
}

findAndHookMethod是一個Helper函數。註意靜態導入,如果你像連接中說的一樣做了,那就會被自動加入。這個方法使用系統UI包的ClassLoader尋找 Clock 這個類,然後尋找其中的updateClock方法。如果這個方法中有任何參數,你必須事後列出這些參數的類型。有許多方法做這件事,但因為我們的方法沒有參數,所以就先跳過這個步驟。至於最後一點,你需要提供一個 XC_MethodHook 類的實現。對於更小的更改,你可以使用匿名類。如果你的代碼很多,最好創建普通的類並且只在這裏創建實例。helper之後會如之前所說的,做出所有hook這個方法的必要工作。

XC_MethodHook中有兩個你可以重寫的方法。你可以兩個都重寫,或者都不重寫。但後者的做法顯然是沒有道理的。這些方法就是beforeHookedMethod 和 afterHookedMethod。不難猜出他們在原有方法之前/後執行。你可以使用“before”方法在方法調用前估計/操縱參數(通過param.args)。甚至阻止調用原來的方法(發送你自己的結果)。“after”方法可以用來做一些基於原來方法的結果的事。你也可以在這個地方操縱結果。當然,你可以在方法調用的前/後添加你自己要執行的代碼。

如果你要徹底替換一個方法,去看看子類XC_MethodReplacement。在那裏,你只需要重寫replaceHookedMethod。

XposedBridge為每個hook過的方法維護一個註冊過的回調的表。擁有最高優先級(在hookMethod中被定義)的將被優先調用。原有的方法總是具有最低的優先級。所以如果你hook了一個擁有回調A(高優先級)和回調B(默認優先級)的方法,那麽不管被hook的方法是如何被調用的,執行順序總是這樣的:A.before -> B.before -> original method -> B.after -> A.after。所以A可以影響B看到的參數,即把它們傳遞下去以前大幅度地修改它們。原有的方法的結果會先被B處理,但是A擁有原先調用者最終將得到什麽樣結果的決定權。

最終步驟:在方法調用之前/後執行你自己的代碼

好了,你已經在正確的上下文運行環境中(比如:系統UI進程)有了一個在updateClock方法每次被調用時都會被調用的方法。現在讓我們修改一些東西吧。

第一個要檢查的:我們有具體的Clock類型的引用麽?是的,我們有。這就是param.thisObject參數。所以如果方法通過myClock.updateClock()被調用,那麽param.thisObject 就是 myClock。

接下來:我們對clock做什麽?Clock類型並不可用,你不能將param.thisObject轉換為類(也不要試著這樣做)。然而它是從TextView繼承而來。所以一旦你把Clock的引用轉換為TextView,你可以使用像setText, getText 和 setTextColor之類的方法。我們的改動應該在原有的方法設置了新的時間以後進行。因為在方法被調用前什麽都沒有做,我們可以空著beforeHookedMethod。沒有必要調用(空的)“super”方法。

所以以下是完整的源代碼:

package de.robv.android.xposed.mods.tutorial;

import static de.robv.android.xposed.XposedHelpers.findAndHookMethod;
import android.graphics.Color;
import android.widget.TextView;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam;

public class Tutorial implements IXposedHookLoadPackage {
    public void handleLoadPackage(final LoadPackageParam lpparam) throws Throwable {
        if (!lpparam.packageName.equals("com.android.systemui"))
            return;

        findAndHookMethod("com.android.systemui.statusbar.policy.Clock", lpparam.classLoader, "updateClock", new XC_MethodHook() {
            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                TextView tv = (TextView) param.thisObject;
                String text = tv.getText().toString();
                tv.setText(text + " :)");
                tv.setTextColor(Color.RED);
            }
        });
    }
}

對結果感到滿意


現在啟動/安裝你的app。因為你第一次啟動它時已經在Xposed Installer中把它設置為了可用,你就不需要在做這一步了。重啟即可。然而,如果你正在使用這個紅色鐘表的例子,你也許想禁用它。兩者都對它們的updateClock 處理程序使用了默認的優先級。所以你不清楚哪個會勝出(實際上這依賴於處理方法的字符串表示形式,但不要依賴這個方式)。

結論


我知道這個教程很長。但我希望你現在不但能實現一個綠色的表,也能夠做出一些完全不同的東西。找到好的被hook的方法是經驗的問題,所以先從簡單的做起。試著一開始多使用log函數確保每次調用都是你預期的。現在:祝玩得開心!

Xposed模塊開發教程