天天看點

Xposed開發指南 一個修改系統的項目 Xposed的工作原理 建立一個項目 把這個項目Xposed化 試着運作一下 尋找你的目标并修改它 使用應用來尋找并hook方法 最後一步: 在方法調用前後執行你的代碼 笑看最後的結果 總結

轉:http://sorcerer.farbox.com/post/android/xposedkai-fa-zhi-nan

  • 一個修改系統的項目
  • Xposed的工作原理
    • hook和取代一個方法
  • 建立一個項目
  • 把這個項目Xposed化
    • AndroidManifest.xml
    • XposedBridgeApi.jar
    • Module implementation
    • assets/xposed_init
  • 試着運作一下
  • 尋找你的目标并修改它
  • 使用應用來尋找并hook方法
  • 最後一步: 在方法調用前後執行你的代碼
  • 笑看最後的結果
  • 總結

聲明: 原創, 翻譯自rovo89的官方Development tutorial

已全部翻譯, 待修改 | 最後修改于: 2015年9月16日

水準有限, 僅供參考.

好了...你應該是想要學習如何建立一個全新的Xposed子產品吧? 閱讀這篇指南(或者說是"擴充教程")來學習吧. 這篇文章包含但不限于"教程", 還包括關于這件事背後的思考. 這樣子的思考 會讓你認識到你創造的東西的價值, 并且了解你在幹什麼和為什麼要這麼幹. 如果你覺得文章太長了, 讀不下, 你可以隻看最後的源代碼部分

Making the project an Xposed module

. 但是你如果讀完了整篇文章你會獲得更好的了解. 你會省下到時候回頭來讀的時間, 因為你如果讀完了整篇文章, 你就沒必要去親自去了解每個細節.

一個修改系統的項目

你可以在Github上找到一個建立紅色時鐘的樣例. 這個樣例修改狀态欄上面時鐘為紅色并且添加了一個笑臉. 我選擇這個樣例是因為這個樣例足夠的小, 并且很容易獲得可見的效果. 另外, 這個樣例使用了一些Xposed架構中的一些基本的方法.

Xposed的工作原理

在開始你對你的系統大動幹戈之前, 你應該對Xposed是如何工作的有一個最基本的概念(你可以跳過這個章節, 如果你覺得很無聊). 

首先, 系統裡面有一個叫作"Zygote"的程序. 這是Android運作的核心. 每一個程式都是作為這個程序的副本("fork")來打開的. 這個程序會被一個叫作

/init.rc

的腳本在開機的時候打開. 這個程序被放在

/system/bin/app_process

中, 用于在加載系統所必須的類和調用初始化方法.

下面要介紹Xposed是從哪裡闖進到你的系統裡面的. 當你安裝了這個架構, 一個拓展的可執行應用程序就被複制到了

/system/bin

當中. 這個程序通過在環境變量中加載額外的jar并從某些地方調用一些方法. 舉個例子, Zygota的main方法被調用隻是在VM被建立之後. 在這個方法裡面, 是Zygota的用來在context執行的一部分.

這個jar被放在

/data/data/de.robv.android.xposed.installer/bin/XposedBridge.jar

中, 并且你可以在這裡找到它的源代碼. 打開XposedBridge這個類, 看一下這個的main方法. 這個就是我寫在上面的東西, 用來在手機剛啟動非常早的時候被調用. 一些初始化方法會被執行, 還有一些子產品會被加載(這個會在子產品加載部分再說).

hook和取代一個方法

那到底是什麼東西讓Xposed有如此強大的能力去"hook"調用一個方法. 當你通過反編譯修改一個APK, 你可以直接插入或者修改你想要的指令. 然而, 你還要重新編譯并簽署這個APK, 并且還需要釋出整個完整地APK. 通過Hook, 你可通過Xposed實作這個功能, 你不能修改一個應用内部的方法(因為幾乎不可能去确定你到底需要什麼樣子的修改和放在什麼位置). 但是你可以在一個方法前面或者後面注入你的代碼, 這是最小的可以明确位置的Java單元.

XposedBridge擁有一個叫作

hookMethodNative

的私有原生方法. 這個方法被拓展的app_process實作. 會修改這個方法為"原生"并與這個方法的實作與系統原生的平常方法相連接配接. 這就意味着, 每一次這個被hook的方法将被調用, 這個平常的方法就會被取而代之地調用, 同時調用者并不知情. 在這個方法當中,

handleHookedMethod

會被調用, 并且将傳輸參數到這個方法,引用等當中. 而且這個方法會檢測到已經在這個方法裡面注冊過的喚醒回調信号. 這樣一來, 我們就可以修改目前方法調用的參數, 修改執行個體化的或者靜态的變量, 喚醒其他方法, 根據傳回的結果去幹一些事情...當然也可跳過任何東西. 一切都顯得非常靈活.

好了, 原理就講到這兒. 現在讓我們開始建立一個子產品吧.

建立一個項目

一個子產品就是一個普通的app, 隻是有一些特殊的meta資訊和檔案. 是以, 先建立一個全新的Android項目. 我現在假設你已經在這之前做好了這件事. 如果沒有, 在官方文檔裡面已經很細緻地教你怎麼做了. 至于SDK的問題, 我選擇了4.0.3(API 15). 我建議你嘗試也這麼去做, 并且暫時不要輕易去嘗試其他版本. 你不需要建立一個Activity, 因為修改系統不需要任何UI界面. 在我回答玩這個問題之後, 我想你應該已經有了一個空白的項目了吧.

把這個項目Xposed化

現在, 我們将這個項目轉化成一個會被Xposed加載的子產品吧. 會有一些必要的步驟.

AndroidManifest.xml

在Xposed Installer裡面的子產品清單會根據應用中是否包含特殊的meta資訊标志來判斷是否是Xposed子產品. 你可以根據

AndroidManifest.xml => Application => Application Nodes (at the bottom) => Add => Meta Data

來實作. 标簽的名字應該是

xposedmodule

并且相應的值應該是

true

. 然後保證這個項目其他資源是空的. 然後在

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. 你可以從XDA文章的一樓下載下傳

XposedBridgeApi-<version>.jar

. 将這個jar複制到項目一個叫作

lib

的子目錄, 然後右鍵這個檔案并選擇Build Path => Add to Build Path(Android Studio可以看我的文章). 檔案名當中的

<version>

就是你要作為

xposedminversion

插入manifest中的值.

確定這個API類沒有被包含在你編譯出來的APK檔案當中(僅僅去引用這個API). 否則, 你會遇到

IllegalAccessError

. 放在

libs

(注意是加`s`的)檔案夾下面的檔案會被Eclipse自動包含到APK當中, 是以不要将API檔案放在這裡面.

Module implementation

現在你可以為你的子產品建立一個class. 我現在示範的class的名字叫作"Tutorial"并且包名是

de.robv.android.xposed.mods.tutorial

:

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

public class Tutorial {

}
      

第一步, 我們會執行一些Log輸出去證明子產品已經被加載了. 一個子產品有多個入口, 至于要選擇哪一個取決與你要修改什麼東西. 你可以在任何時候讓Xposed在你的子產品中執行一些函數, 包括系統啟動的時候, 一個新的app被加載的時候, 一個app的資源檔案被初始化的時候等.

在這篇教程的後面, 你認識到一個特定的app需要一些必要的修改, 看看"let me know when a new app is loaded"中的入口. 所有入口都被IXposedMod的子接口标記. 在這個案例中, 你需要implement IXposedHookLoadPackage. 事實上這就是一個有一個參數的方法, 用于提供更多關于context資訊給子產品. 在我們的樣例當中, 讓我們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方法輸出資訊到stardard logcat(tag是

Xposed

)和/data/data/de.robv.android.xposed.installer/log/debug.log(一個可以通過Xposed Installer輕易通路的位置)當中.

assets/xposed_init

到此為止, 現在唯一還缺少的東西就是一個為XposedBridge準備的訓示. 這件事可以通過一個叫

xposed_init

的檔案來實作. 在

assets

檔案夾下面建立一個新的名叫

xposed_init

的文本檔案. 在這個檔案中, 每一行都包含一個完全授權的類名. 在這個案例當中, 這個就是

de.robv.android.xposed.mods.tutorial.Tutorial

.

試着運作一下

儲存你的檔案. 然後編譯并運作的你的Android應用. 這就是你第一次安裝這個應用, 在使用之前, 你需要授權這個應用. 打開Xposed Installer的應用, 確定你已經安裝了Xposed的架構. 然後切換到"Modules"頁面. 你應該可以找到你的應用已經出現在這兒了. 勾選複選框來授權. 然後重新開機你的手機. 你不會在手機程序中看到什麼不同, 但是當你觀察控制台輸出的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, 你大概知道現在要如何去思考. 總體來說, 你首先需要擷取一些你的目标實作對象的内部細節. 在這個教程當中, 目标就是狀态欄當中的時鐘. 這個樣例會幫助你去了解狀态欄和其他SystemUI部分. 是以, 我們要開始一些搜尋.

可能的一種方法: 反編譯一下. 這會讓你獲得implementation的具體資訊, 但是這會很難去閱讀并了解, 因為你将會獲得smali格式的代碼.

可能的另一種方法: 擷取AOSP源碼(這兒或者這兒), 這些會和你的ROM有很大的差別, 但是在這裡面有相似甚至幾乎相同的implementation. 我一般會先看AOSP來确定是否已經足夠了. 如果我需要更多的細節, 我會去看實際反編譯出來的代碼.

你可以看看那個名字是或者當中包含"clock"的類. 其他東西是資源和布局檔案. 如果你下載下傳了官方的AOSP源代碼, 你可以開始在frameworks/base/packages/SystemUI中找. 你會找到一些地方其中出現了"clock". 這是普通并且确實是一個不尋常的方法去實作一個修改. 記住你現在"隻能"去hook一個方法. 是以你需要找到一個合适的地方去插入一些代碼以實作這一個神奇的過程. 這個地方可以是一個方法的前面和後面, 或者直接取代這個方法. 你要hook的方法越明确越好, 最好不是那些會被調用成千上百次的方法, 以避免對手機性能造成影響, 以及一些不可預期的負面影響.

在這個樣例當中, 你可能發現res/layout/status_bar.xml包含了對com.android.systemui.statusbar.policy.Clock. Multiple中的自定義View的引用. 這個文字顔色是被通過一個textAppearance attribute被定義的, 是以修改顔色最輕便的方法就好似去修改這個apperance的定義. 然而, 并不能通過Xposed架構去修改系統style, 而且也不一定生效(這個在原生代碼裡面太深了). 替換整個狀态欄layout檔案似乎更有可能成功, 但是這樣子可能會過度造成一些小變化. 相反地, 看看這個類吧. 有一個叫作updateClock的方法, 似乎是用來每過一分鐘更新一次時間的:

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

這個看上去簡直就是完美的修改, 因為這個是非常明确的方法, 看上去隻是用來修改時鐘上文字的方法, 如我們在這個方法每一次被調用之後加一點東西就可以達到修改時鐘文字和色彩的目的. 是以, 我們來試試:

對于僅僅修改文字顔色, 這個明顯是一個更好的方法. 也可以在(替換資源檔案)看看一下"修改layout"

使用應用來尋找并hook方法

想象現在我們已經知道了什麼? 我們在com.android.systemui.statusbar.policy.Clock中有了一個updateClock的方法并且我們準備去攔截這個方法. 我們在SystemUI資源中找到了這個類, 是以這個應該在SystemUI的程序當中起到一定的作用. 如果我們嘗試從中去擷取一些資訊并在handleLoadPackage方法中直接應用這個類, 這樣子沒準會失敗, 因為這個沒準就是一個錯誤的程序. 是以, 當特定的包被加載, 我們先implement去執行一些代碼.

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

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

通過使用parameter, 我們可以輕易确認我是否在正确的包内. 一旦确認, 我們會嘗試去接觸包内一個叫作

ClassLoader

的類, 這個類是也是被這個變量引用的. 現在, 我們可以尋找com.android.systemui.statusbar.policy.Clock類以及其中的upclockClock方法, 告訴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的函數. 記錄靜态的輸入, 會自動添加如果你确認這個就是描述中的那個頁面. 這個方法通過SystemUI中的ClassLoader查找Clock類. 然後在其中尋找updateClock方法. 如果有任何parameter指向這個方法, 之後你需要列出所有parameter的種類. 有多種方法去做這件事情, 但是我們的方法并不包含任何parameter, 讓我們先跳過這個. 至于最後那個參數, 你需要去為implement XC_MethodHook類做準備. 為了讓修改精簡, 你可以使用匿名内部類. 如果你寫了很多的代碼, 最好去建立一個普通的類并将執行個體都寫在這兒. 然後helper會幫助你做一切上面描述的用于hook的必要事情.

在XC_MethodHook方法當中, 有兩個方法提供你去改寫. 你可以選擇都改寫或者都不. 但是如果你都不改寫, 等會兒将絕對看不到任何效果. 這些方法是

beforeHookedMethod

afterHookedMethod

. 不難猜測, 這兩個分别會在原生方法前後執行. 你可以用"before"方法去評估或者篡改傳給原生方法的參數(通過param.args), 甚至阻止對原生方法的調用(傳輸你的方法的結果). "after"方法可以用于幹一些基于原生方法輸出結果的事情. 你也可以在此時篡改結果. 當然, 你可以在原生方法調用前後添加你自己的代碼.

如果你想要完全取代原生方法, 可以看看

XC_MethodReplacement

這個子方法. 你隻需要去重寫這個方法.

XposedBridge當中有一個清單, 用于記錄每一個hook方法的注冊回調方法. 那些都方法都有着最高的優先級(定義在hookMethod中), 會被優先調用. 這個原生方法總是最低的優先級. 是以, 如果你通過回調A(高優先級)和B(預設優先級)hook了一個方法, 那麼無論何時被hook的方法被調用, 總是遵循這個流程: A.before -> B.before -> 原生方法 -> B.after -> A.after. 是以, A可以影響傳到B的參數, 會更深一層修改這個參數在它們結束之前. 原生方法輸出的結果會先被B處理, 但是A對于最終的結果會有最終話語權.

最後一步: 在方法調用前後執行你的代碼

好了, 現在你的這個方法根據會在每一次updateClock方法被調用的時候同時被調用了. 現在讓我們來修改一些東西.

首先确認一下: 我确實已經引用了Clock對象? 是的, 已經存在于param.thisObject parameter中了. 是以, 如果這個方法被

myClock.ipdateClock()

調用, 

param.thisObject

就是

myClock

.

接下去: 我們能對那個時鐘幹些什麼? 這個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);
            }
        });
    }
}
      

笑看最後的結果

現在再一次安裝并啟動的你的應用. 因為第一次安裝時候, 你已經在Xposed Installer當中授權了這個應用, 你不需要再一次進行授權, 重新開機就夠了. 然而, 你可能想要停止使用紅色時鐘, 隻需要去取消授權即可. 如果原生和你修改子產品都使用了預設的優先級, 那你也說不清哪個優先級更高(這個取決于handler方法的字元串代表, 并不依賴于子產品).

總結

我也知道這個教程挺長的. 但是我希望你不隻是實作一個綠色的時鐘, 而是更多完全不一樣的東西.如何找到好的方法去hook取決于你的經驗, 是以要從簡單的事情開始. 在一開始請嘗試用Log在控制台輸出, 以保證每一個地方都如你預期被調用. 另外, 衷心地祝福你在其中獲得快樂.