Xposed 开发教程(翻译自官方)
官方原文:https://github.com/rovo89/XposedBridge/wiki/Development-tutorial
開發教程
好吧 …… 你打算學習怎么建立一個新的 Xposed 模塊嗎?那就讀讀這個教程(或者叫它 “泛談” 也可以)并且學習怎么一步步地達成這個目標。這不僅包含了例如 “新建并插入” 的技術性內容,還包含了一些背后的思想,這些思想可以逐漸使你知道你在做什么、為什么要做這個玩意、做這個玩意的價值。如果你覺得 “ 文章好長我不想讀 ”,你可以只看看最后的源代碼以及 “建立 Xposed 模塊項目” 一章。由于你不一定要理解透徹每一樣事物,你可以把閱讀這份教程的時間節省下來。但是仍然建議您完整閱讀這個教程,這會讓你更好地理解 Xposed 模塊的開發。
教程目標
你將重新編寫 "red clock" 示例,這個示例可以從第一篇文章下載或者從 ?Github ?找到。它可以將狀態欄時鐘的顏色變成紅色并且加一個小笑臉。因為這個項目它非常小型但是可以非常明顯地看到變化,并且使用了 Xposed 框架的一些基礎方法,我選擇了這個項目作為示例。
Xposed 如何工作
在修改工作開始之前,你應該先粗略認識一下 Xposed 框架是怎么工作的(如果你覺得這很枯燥,你也可以跳過)。那么,它是怎么工作的呢:
有一個進程它叫 ?"Zygote",是 Android 運行庫的心臟。每個應用都由它啟動并且由它托管系統服務。這個進程由 /init.rc 這個腳本在手機引導時啟動。這個進程會和 /system/bin/app_process 這個加載必需的類和調用初始化函數的家伙一起啟動。
現在輪到 Xposed 出場了。當你安裝框架,一個從 system/bin 復制而來的可擴展、可執行的 app_process。 ?這個擴展會在進程啟動時加載一個額外的 jar 到 ?classpath 并且在這里調用別處的一些函數。例如,在虛擬機剛剛啟動,要調用 ?Zygote 的 ?main ?函數時,就會做上面的事情。在這里面,Xposed 就是 ?Zygote 的一部分,能夠在它的內部活動。
jar 文件就是 ?/data/xposed/XposedBridge.jar ,它的源代碼可以在這里找到。觀察 ?XposedBridge ?類,你可以找到 ?main ?函數。這就是我上面提到的東西,這個函數會在進程非常初期的階段調用。一些加載工作已經完成并且模塊被加載的時候(我會在之后談及模塊加載).
函數的掛鉤 / 替換
Xposed 的實現依賴于函數調用 “鉤子”。當你對 APK 文件做修改,接觸到 smali 代碼的時候,你可以直接插入 / 修改代碼。如果你不想修改 APK 但卻想達到同樣的效果,可以修改二進制代碼或者編譯了的代碼,但不推薦。因為那需要完全一樣的代碼來表現你做的修改。 即使你在它運行的時候反編譯了它并且嘗試對基于 pattern search 得到的 smali 代碼做些修改,這也可能因為使用了不同的變量(聲明的)數字而使結果發生偏差。所以我決定對 Java 里面的能被清晰定義的最小單位做修改:函數。
XposedBridge 這個類有一個私有的、本地的函數 hookMethodNative。這個函數
XposedBridge has a private, native method hookMethodNative. This method is implemented in the extended app_process as well. It takes a Method object that you can get via Java reflection and change the VM internal definition of the method. It will change the method type to "native" and link the method implementation to its own native, generic method. That means that every time the hooked method is called, the generic method will be called instead without the caller knowing about it. In this method, the method handleHookedMethod in XposedBridge is called, passing over the arguments to the method call, the this reference etc. And this method then takes care of calling methods that have registered for this method call. Those can change the arguments for the call, then call the original method, then do something with the result. Or skip anything of that. It is very flexible.
好了,理論課程到這里就結束了,讓我們動手建立一個 Xposed 模塊吧!
新建項目
一個 Xposed 模塊就是一個標準的應用。只是有一些特別的元數據和文件。所以首先要建立一個新的 Android 項目。我假設你已經建立了一個新的 Android 項目。如果不會,官方開發文檔里面有許多詳細的步驟和信息。當選擇 SDK 版本時,我選擇了 4.0.3(API 15),因為這是我的手機正運行的版本。我建議你也選擇這個 SDK,暫時不要做小白鼠。你不需要建立一個 activity,因為修改不需要任何的用戶界面。設置好項目后,你應該得到一個空白的項目。
讓你的項目變成一個 Xposed 模塊
現在讓我們將這個項目變成可以讓 Xposed 加載的一個東西——模塊吧。這需要幾個步驟。
AndroidManifest.xml
Xposed 安裝器的模塊列表會尋找帶有特定元數據的應用。你可以通過 AndroidManifest.xml => Application => Application Nodes (在底部) => Add => Meta Data 來新建它。名稱應為 xposedmodule,值應為 true。讓 resource 保持空白。你應該重復這一步驟來修改 xposedminversion,然后把值設為你正在使用的 API 版本(例如下面那樣)。這時候 XML 源代碼就像下面那樣:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <?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:value="true" android:name="xposedmodule"/> ????????<meta-data android:value="2.0*" android:name="xposedminversion"/> ????????<meta-data android:value="Demonstration of the Xposed framework.nMakes the status bar clock red." android:name="xposeddescription"/> ????</application> </manifest> |
XposedBridgeApi.jar
下一步,聲明 XposedBridge API。你可以導入 XposedBridge 項目然后通過 引用 來添加它,但那樣 Eclipse 會在你測試應用時嘗試去安裝它(到一個錯誤的位置)。所以更好的方法是,從這里下載 ?XposedBridgeApi.jar 然后把它復制到你項目的根目錄文件夾。然后右擊它,選擇 Build Path => Add to Build Path。
更好的替代方法是:下載 XposedLibrary 項目然后把它導入到你的 Eclipse 工作臺。這樣你就可以在你的項目引用 XposedBridgeApi.jar 了:在你的項目的 build path configuration 的 "Libraries" 標簽,點擊 "Add JARs",然后選擇 "XposedLibrary => XposedBridgeApi.jar"。這樣做的好處是只要保留一份你所使用 API 的副本,這樣你就能通過檢查新版本 API 來第一時間升級你的模塊(最好用 Git 檢查一下 repository)。 如果你使用這個方法,你可以在覆蓋用戶設置時使用一些偏好 UI 類。在未來,會添加更多東西。你可以在這里找到如何搞定它。
要獲知你正在使用的 API 版本,在你的項目中打開 ?Package Explorer,它就在 project => Referenced Libraries => XposedBridgeApi.jar => assets => VERSION.
Module implementation
現在你可以為你的模塊新建一個類了。我就把他命名為 "Tutorial" ,包名為 de.robv.android.xposed.mods.tutorial :
| 1 2 3 4 5 | package de.robv.android.xposed.mods.tutorial; ? public class Tutorial { ? } |
首先,我們需要輸出一些日志以表明這個模塊已被加載。一個模塊只有幾種入口點。從哪一個進入取決于你想要修改什么。例如你可以在 Android 系統啟動的時候讓 Xposed 調用你的函數,或者在一個應用即將被加載的時候,或者在一個應用的資源文件加載的時候,等等。
在本教程中,你將會學習到在一個特定的應用中必須做出的必要更改,所以現在我們使用 “一個應用被加載時提示我” 的入口點。所有的入口點被一個 IXposedMod 的 sub-interface 所標記。在本例中,它是 IXposedHookLoadPackage which you need to implement. 實際上它只是一個方法,帶有一個參數,這個參數可以帶給你更多別的信息,例如導入模塊的上下文。實際上它是只有一個參數的一個函數,這個傳入的參數可以告訴你更多信息 that gives more information about the context to the implementing module. 現在讓我們輸出被加載應用的信息吧,就像下面一樣:
| 1 2 3 4 5 6 7 8 9 10 11 | 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); ????} } |
這個方法會在標準的 logcat 中輸出信息,tag 為 Xposed ,并且保存在 /data/xposed/debug.log。
assets/xposed_init
現在你不知道的唯一一件事就是到底入口點存在于 XposedBridge 的哪個類里面。其實它通過調用 xposed_init 來實現的。 在 assets 文件夾建立一個以前面的名字命名的文件。在這個文件中,在一行里面寫上你想要做入口點的類名。本例中,它就是 de.robv.android.xposed.mods.tutorial.Tutorial
試一試
保存你的文件。然后以安卓應用的方式編譯并運行你的項目。如果你是第一次安裝,你需要在安裝完畢后到 Xposed 安裝器啟用它,這樣它才能工作。先在 Xposed 安裝器中核實你是否安裝了 Xposed 安裝文件。然后前往 “模塊” 頁面。你應該在這個頁面找到你的模塊。把對應的方框打上勾以啟用它。然后重啟。這時候你會發現系統并沒有什么不一樣,但是只要檢查一下日志文件,你應該會看到和下面類似的內容:
| 1 2 3 4 5 6 | 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 ... (還有更多的應用,就不一一列舉了) |
Voilà! That worked. 現在你已經有了一個 Xposed 模塊了。但是它還可以做一些比寫日志更有用的事情……
尋找你的獵物,想方設法去修改它
好了,現在我們要進入全新的一部分教程,你要做的事情不同,教程的內容也不同。如果你之前已經有過修改 APK 的經驗了,你或許知道如何在這部分思考。總體上,你先要知道目標的一些接口信息。在這個教程中,我們的目標是狀態欄的時鐘,那么它就可以幫我們了解狀態欄的很多事情。那么現在先開展我們的搜索工作吧。
可能的一種方式:反編譯它。這樣會給你更清晰的接口信息。但由于反編譯出來的是 smali 代碼,可讀性非常差。另一種可能:獲取 AOSP 源代碼(例如這里或者這里)并且閱讀它。不過根據 ROM 種類的不同,代碼可能會有些出入。但是這樣子的話可以獲取到非常接近甚至相同的接口信息。我更喜歡先閱讀 AOSP 代碼,如果信息還是不夠,那么就看看反編譯的代碼。
你可以以?"clock" 為關鍵字在函數名或者字串符中搜索。或者在資源、布局文件中找找。如果你下載了官方的 AOSP 源代碼,你可以從?frameworks/base/packages/SystemUI 開始閱讀代碼。你會找到一些出現 "clock" 的地方。這是很正常的,事實上有好幾種去注入修改的方法。記住,你只能掛鉤方法。所以你必須要去找到一個可以插入你要用來實現功能的代碼的地方,你可以在函數被調用之前、之后注入,或者干脆把整個函數替換掉。你應該注入盡可能深入的函數,而不是那些被調用很多次的函數,這樣可以避免性能問題和無法預料的副作用。
這時候,你可能發現?res/layout/status_bar.xml 這個布局文件引用了一個自定義 View,它的類是?com.android.systemui.statusbar.policy.Clock。現在你可能有很多想法。文本的顏色是通過 textAppearanceattribute 來定義的,所以最干凈利落的方法是改變 textAppearanceattribute 的定義。然而,this 指針是不可能改變樣式的(它在二進制代碼里面隱藏的太深了)。替換狀態欄的布局文件倒是有可能,但是對于你所做的一點點小修改來說,實在有點殺雞用牛刀的意味。好吧,那么我們來看看這個類。這里有個叫?updateClock 的函數,在每分鐘要更新時間的時候,它會被調用來更新時間:
| 1 2 3 4 | final void updateClock() { ????mCalendar.setTimeInMillis(System.currentTimeMillis()); ????setText(getSmallTime()); } |
看上去這是一個做修改的好地方,它是一個非常具體的方法,它只會將時鐘的文字設置一下,不會做別的什么事情。如果我們在它調用之后加一些可以修改顏色和文本的修改代碼,那應該就能達成我們的目的了。開始干吧!
如果你只想修改文本的顏色,有一個更好的辦法。你可以查看 “替換資源” 的“修改布局”章,那里說明了如何用反射機制來尋找和掛鉤一個函數。
那么現在我們來總結一下我們得到的信息。我們找到在?com.android.systemui.statusbar.policy.Clock 找到了一個叫?updateClock 的函數,我們將在這個函數進行注入修改。而我們是在?SystemUI 的源代碼中找到它的,所以它只會對?SystemUI 這個進程起作用,一些框架下的類也會起作用。如果我們嘗試在?handleLoadPackage 函數中直接取得這個類一些信息和引用,很可能會因為進程不符而失敗。所以現在我們先開始讓代碼只在對的包里面運行:
| 1 2 3 4 5 6 | public void handleLoadPackage(LoadPackageParam lpparam) throws Throwable { ????if (!lpparam.packageName.equals("com.android.systemui")) ????????return; ? ????XposedBridge.log("we are in SystemUI!"); } |
使用傳入的參數,我們可以很容易地檢查我們是否正在正確的包中運行。只要我們確認是正確的包,我們就使用 ClassLoader?(this 變量中有引用)來獲取訪問包中的這個類的權限。現在我們就尋找?com.android.systemui.statusbar.policy.Clock 這個類的?updateClock 函數,并且告訴 XposedBridge 去做一個掛鉤:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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 是一個助手函數。注意靜態導入標識,它會被自動地添加 function. Note the static import, which is automatically added if you configure it as described in the linked page. This method looks up the Clock class using the ClassLoader for the SystemUI package. Then it looks for the updateClock method in it. If there were any parameters to this method, you would have to list the types (classes) of these parameters afterwards. There are different ways to do this, but as our method doesn't have any parameters, let's skip this for now. As the last argument, you need to provide an implementation of the XC_MethodHook class. For smaller modifications, you can use a anonymous class. If you have much code, it's better to create a normal class and only create the instance here. The helper will then do everything necessary to hook the method as described above.
在 XC_MethodHook 中有兩個你能重載的函數。你可以兩個都重載也可以一個都不重載,但一個都不重載這樣當然說不過去。這兩個函數是 beforeHookedMethod 和 afterHookedMethod。不難猜出它們會在原函數執行之前 / 之后被執行。你可以使用 "before" 函數來獲得 / 修改原函數獲得的參數(從 param.args 修改),甚至還能阻止原函數被調用(返回你自己的結果)。"after" 函數可以用做一些基于原函數結果的修改。你也可以在這個函數里面修改原函數返回的結果。當然,你也可以在原函數調用之前 / 之后執行你自己的代碼。
如果你想要完全替換一個函數,看看子類 XC_MethodReplacement,重載里面的 replaceHookedMethod 函數即可。
XposedBridge 有一個列表,里面記錄了與每個被修改的函數相對應的回調函數。這里面擁有最高優先級(可在 hookMethod 里定義)的回調函數將會被首先調用。原函數總是被最后調用。所以如果你用一個回調函數 A(優先級高)和一個回調函數 B(優先級默認)來修改一個函數,無論原函數何時運行,都會按以下控制流程執行:A.before -> B.before -> 原函數 -> B.after -> A.after。所以函數 A 可以影響函數 B 可能會獲得的參數,可能會導致參數在執行前被過度修改。?The result of the original method can be processed by B first, but A has the final word what the original caller gets.
最后一步:在函數調用之前 / 之后執行你的代碼
?
Alright, you have now a method that is called every time the updateClock method is called, with exactly that context (i.e. you're in the SystemUI process). Now let's modify something.
First thing to check: Do we have a reference to the concrete Clock object? Yes we have, it's in the param.thisObject parameter. So if the method was called with myClock.updateClock(), then param.thisObject would be myClock.
下一步:我們能對這個時鐘做些什么?類 Clock 并不能使用,你不能將 param.thisObject 轉換為類?(don't even try to)。然而它從 TextView 繼承而來。只要你將 Clockreference?轉換為 TextView,你就可以使用諸如 setText、getText、setTextColor 的函數。更改應該在原函數設定新的時間值后完成。由于在原函數執行前沒有什么事情要做,我們可以讓 beforeHookedMethod 保持空白。也不需要調用空的 “超類” 函數。. Calling the (empty) "super" method is not necessary.
下面是完整的源代碼:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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); ????????????} ????????}); ????} } |
令人滿意的結果
現在重新安裝 / 啟動你的應用。由于你已經在第一次打開時啟用了模塊,所以你就不用再啟用模塊了,只需要重啟一次。然而,如果你正在使用?red clock 示例模塊,你最好去禁用掉。如果兩個都啟用,它們都會使用默認優先級來注入?updateClock ,這樣你就不知道哪個模塊在工作。?(it actually depends on the string representation of the handler method, but don't rely on that).
總結
我知道這個教程非常冗長。但我希望你現在不僅可以實現一個 "green clock",更可以完成一些完全不同的事情。尋找一個絕佳的掛鉤原函數需要一定的經驗,所以先從比較簡單的事情開始把。在初期,建議你多嘗試使用 log 函數,以確保所有函數按你期望的方式來調用。現在,祝你玩得開心!
總結
以上是生活随笔為你收集整理的Xposed 开发教程(翻译自官方)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 安卓锁屏音乐控件开发
- 下一篇: 布局技巧-等高布局 圣杯布局 双飞翼布