一、Xposed簡介
Xposed是一款優秀的android java層 hook 框架。它允許你在不修改apk源碼的情況下,通過編寫自己的模塊來改變apk的行爲。它的優點是採用了插件機制,模塊能夠適用不同版本的框架和rom。模塊改變apk行爲的操作發生在內存中,對源apk不進行任何修改。你只需要安裝編寫的模塊並重啓相應的設備即可。
二、相關資源
Xposed 官網:http://repo.xposed.info/
Xposed 項目 github 地址:https://github.com/rovo89
Xposed 官方教程 :https://github.com/rovo89/XposedBridge/wiki/Development-tutorial
Xposed Api 之XposedBridge.jar 下載:https://jcenter.bintray.com/de/robv/android/xposed/api/
三、使用Xposed
1.安裝Xposed 框架
首先你需要在手機設備上安裝Xposed 框架,官網上給我們提供了一個apk 來完成框架的安裝。
Xposed Installer官方下載:http://repo.xposed.info/module/de.robv.android.xposed.installer
由於Android系統5.0以上默認採用了ART模式,Android系統5.0 以下默認採用Davik模式。所以目前Xposed框架主要有兩個版本:一個是Android 4.0.3 to Android 4.4版本,一個是Android 5.0 以上版本。
我們以Android 4.0.3 to Android 4.4版本爲例。
下載Xposed installer 成功後,將de.robv.android.xposed.installer.apk安裝在海馬模擬中或者已經root真機上
選擇 框架 ,可看到app_process和XposedBridge.jar需要激活
選擇 安裝/更新,會提示重啓
重啓後,會發現app_process和XposedBridge.jar已經激活。
2.編寫相關的模塊
現在我們來編寫Xposed模塊。
1.創建一個Android 工程
一個Xposed 模塊本質上是一個正常的apk,只是這個apk沒有與用戶交互的Activity界面,它僅僅包含一些mete 數據和文件,並且該apk安裝後在桌面應用中沒有圖標顯示。所以你只需要創建一個空的安卓工程,不需要添加任何Actitity.
2. 將Android 工程變成Xposed模塊
在工程中添加 Xposed Framework API
Xposed模塊爲了使用Xposed框架的api ,需要下載相應的 XposedBridge.jar 包。
Android Studio (Gradle-based)
Xposed Framework API 在下面網址上可進行查看:
https://bintray.com/rovo89/de.robv.android.xposed/api
在 Androdri Studio 工程中app/build.gradle
文件中添加依賴項:
repositories {
jcenter();
}
dependencies {
provided 'de.robv.android.xposed:api:53'
}
重要的事情說三遍!使用 provided 不要使用 compile! compile 會將整個 API 類 編譯進你的apk中而導致出問題。provided 則只是提供了API 類的引用,API 類真正的實現則在 Xposed FramWork中。
在大多數情況下,repositories
已經存在,並且已經有一些依賴,所以只需要將provided
這一行添加到存在的dependcies
模塊中即可。
如果 需要查看api資源,則添加下面的兩行:
provided 'de.robv.android.xposed:api:53'
provided 'de.robv.android.xposed:api:53:sources'
確保關閉Instant Run項File -> Settings -> Build, Execution, Deployment -> Instant Run
,否者你的apk中將不包含這些類
Eclipse
Eclipse創建的工程需要創建相應的jar包,jar包地址下載:https://jcenter.bintray.com/de/robv/android/xposed/api/
推薦將下載的api-XX.jar
包放入到lib
子目錄中,不要將jar包放入到libs
子目錄中。因爲libs
目錄中的jar包會使Eclipse將API類編譯到APK中,而事實上API只需要引用即可。
右擊api-XX.jar
文件選擇Build Path -> Add to Build Path
即可
Xposed Framework API
android 5.0以前的版本,推薦使用API 53
http://api.xposed.info/reference/packages.html
AndroidManifest.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="53" /> <!--版本信息-->
</application>
</manifest>
模塊實現
在模塊中創建一個類,並命名爲 ’Tutorial‘,該類放在de.robv.android.xposed.mods.tutorial 包中。類名和包名是可以任意取的。
package de.robv.android.xposed.mods.tutorial;
public class Tutorial {
}
教程的第一步,我們只需要將加載的模塊信息打印出來。
一個模塊有一些進入點,這些進入點的選擇取決於你想修改的位置。你可以讓Xposed框架調用你模塊中的函數在下面幾個位置:安卓系統啓動的時候(使用 IXposedHookZygoteInit 接口)、一個新的app被加載的時候(使用 IXposedHookLoadPackage 接口)、一個資源被初始化的時候( 使用 IXposedHookInitPackageResources 接口)。
所有的進入點 都是 XposedMod 接口的子接口。在本示例中,進入點是“一個新的app被加載的時候”,我們需要實現
IXposedHookLoadPackage 接口。事實上該接口只有一個帶有一個參數的方法,該參數中給出了加載的應用的Context 信息:
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方法將寫信息到標準的ogcat中(/data/data/de.robv.android.xposed.installer/log/debug.log
)
assets/xposed_init
XposedBridge 從assets
目錄中的xposed_init
文件中獲取入口點。xposed_init
文件中每行配置一個進入點,使用完全限定名。在該例子中,進行如下配置
de.robv.android.xposed.mods.tutorial.Tutorial.
3.運行模塊
在設備上安裝該模塊,安裝成功後看不到模塊的界面。第一次安裝該模塊後,你需要在Xposed Install app中啓動模塊。打開Xposed Install app,選擇“Modules”tab 後。勾選模塊,並重啓Android系統。
這時你打開Xposed Install app 日誌模塊,會發現下面的輸出:
四、替換資源
替換Boolean, Color, Integer, int[], String and String[]類型的簡單資源
1.替換系統框架(Android Framwork)資源
替換系統框架資源(對所有app 起作用)需要實現 IXposedHookZygoteInit
接口的 initZygote
方法,並在該方法中調用Resources.setSystemWideReplacement(...)
方法替換資源
package de.robv.android.xposed.mods.tutorial;
import android.content.res.XResources;
import de.robv.android.xposed.IXposedHookZygoteInit;
public class Tutorial2 implements IXposedHookZygoteInit{
@Override
public void initZygote(StartupParam arg0) throws Throwable {
XResources.setSystemWideReplacement("android", "bool", "config_unplugTurnsOnScreen", false);
}
}
2.替換app應用資源
替換app應用資源需要實現 IXposedHookInitPackageResources
類的
andleInitPackageResources
方法,並在該方法中調用res.setReplacement(...)
方法替換資源,注意在該方法中不要使用XResources.setSystemWideReplacement
方法
package de.robv.android.xposed.mods.tutorial;
import android.content.res.XResources;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import de.robv.android.xposed.IXposedHookInitPackageResources;
import de.robv.android.xposed.callbacks.XC_InitPackageResources.InitPackageResourcesParam;
public class Tutorial3 implements IXposedHookInitPackageResources {
@Override
public void handleInitPackageResources(InitPackageResourcesParam resparam) throws Throwable {
//只替換systemui應用的資源
if (!resparam.packageName.equals("com.android.systemui"))
return;
// 替換資源的不同方式
resparam.res.setReplacement(0x7f080083, "YEAH!"); // WLAN toggle text. You should not do this because the id is not fixed. Only for framework resources, you could use android.R.string.something
resparam.res.setReplacement("com.android.systemui:string/quickpanel_bluetooth_text", "WOO!");
resparam.res.setReplacement("com.android.systemui", "string", "quickpanel_gps_text", "HOO!");
resparam.res.setReplacement("com.android.systemui", "integer", "config_maxLevelOfSignalStrengthIndicator", 6);
resparam.res.setReplacement("com.android.systemui", "drawable", "status_bar_background", new XResources.DrawableLoader() {
@Override
public Drawable newDrawable(XResources res, int id) throws Throwable {
return new ColorDrawable(Color.WHITE);
}
});//你不能直接使用Drawble類進行替換,因爲Drawble類可以影響其他引用Ddrawble類實例的ImageView ,最好使用一個包裝器。
}
}
替換複雜的資源
對於複製的資源,如動畫資源 ,我們也能夠替換,下面我們來替換battery icon
動畫資源佈局
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="true" >
<item android:drawable="@drawable/icon1" android:duration="150"></item>
<item android:drawable="@drawable/icon2" android:duration="150"></item>
<item android:drawable="@drawable/icon3" android:duration="150"></item>
<item android:drawable="@drawable/icon4" android:duration="150"></item>
<item android:drawable="@drawable/icon5" android:duration="150"></item>
<item android:drawable="@drawable/icon6" android:duration="150"></item>
</animation-list>
代碼:
package de.robv.android.xposed.mods.tutorial;
import com.example.xposedmoduletest.R;
import android.content.res.XModuleResources;
import de.robv.android.xposed.IXposedHookInitPackageResources;
import de.robv.android.xposed.IXposedHookZygoteInit;
import de.robv.android.xposed.callbacks.XC_InitPackageResources.InitPackageResourcesParam;
public class Tutorial4 implements IXposedHookZygoteInit, IXposedHookInitPackageResources {
private static String MODULE_PATH = null;
@Override
public void initZygote(StartupParam startupParam) throws Throwable {
MODULE_PATH = startupParam.modulePath;
}
@Override
public void handleInitPackageResources(InitPackageResourcesParam resparam) throws Throwable {
if (!resparam.packageName.equals("com.android.systemui"))
return;
XModuleResources modRes = XModuleResources.createInstance(MODULE_PATH, resparam.res);
resparam.res.setReplacement("com.android.systemui", "drawable", "stat_sys_battery",
modRes.fwd(R.drawable.animation));
resparam.res.setReplacement("com.android.systemui", "drawable", "stat_sys_battery_charge",
modRes.fwd(R.drawable.animation));
}
}
Xposed框架會將模塊請求資源的請求指向你模塊中的資源
替換的效果:
替換佈局
你可以用替換資源的方法來替換佈局文件,但這樣你不得不將目標apk中的整個layout文件複製出來進行修改,這樣會使模塊的Rom兼容性降低。並且如果兩個以上的模塊修改佈局後,最後修改佈局的模塊會起作用。更重要的是,佈局中指向其它資源的ID很難確定下來。推薦使用下面的方法修改佈局:
package de.robv.android.xposed.mods.tutorial;
import android.graphics.Color;
import android.widget.TextView;
import de.robv.android.xposed.IXposedHookInitPackageResources;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.callbacks.XC_InitPackageResources.InitPackageResourcesParam;
import de.robv.android.xposed.callbacks.XC_LayoutInflated;
import de.robv.android.xposed.callbacks.XC_LayoutInflated.LayoutInflatedParam;
public class Tutorial5 implements IXposedHookInitPackageResources{
@Override
public void handleInitPackageResources(InitPackageResourcesParam resparam) throws Throwable {
if (!resparam.packageName.equals("com.android.systemui"))
return;
resparam.res.hookLayout("com.android.systemui", "layout", "status_bar", new XC_LayoutInflated() {
@Override
public void handleLayoutInflated(LayoutInflatedParam liparam) throws Throwable {
TextView clock = (TextView) liparam.view.findViewById(
liparam.res.getIdentifier("clock", "id", "com.android.systemui"));
clock.setTextColor(Color.RED);
XposedBridge.log("layout resNames.fullname:"+liparam.resNames.fullName);
XposedBridge.log("layout resNames.id:"+liparam.resNames.id);
XposedBridge.log("layout resNames.name:"+liparam.resNames.name);
XposedBridge.log("layout resNames.pkg:"+liparam.resNames.pkg);
XposedBridge.log("layout resNames.type:"+liparam.resNames.type);
XposedBridge.log("layout resNames.variant:"+liparam.variant);
XposedBridge.log("layout resNames.view:"+liparam.view);
}
});
}
}
回調方法handleLayoutInflated
會在layout文件被填充後回調,在方法的 LayoutInflatedParam 對象 參數中,你可以找到你想修改的View組件。你也可以通過調用resNames
來確定加載的那一個佈局文件。用 variant
來確定加載的佈局的目錄’layout-land‘。res
同時也會幫你獲取資源的ID和其它的資源。
五、用反射來hook方法
每當應用加載的時候,IXposedHookLoadPackPage接口的handLoadPackage方法就會被調用執行,爲了讓我們在正確的進程中執行,需要先判斷被加載的包是不是正確的包
package de.robv.android.xposed.mods.tutorial;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam;
public class Tutorial6 implements IXposedHookLoadPackage {
@Override
public void handleLoadPackage(LoadPackageParam param) throws Throwable {
if(!param.packageName.equals("com.android.systemui"))
return;
}
}
一旦我們進入到正確的進程進後,我們就能用param變中的ClassLoad來訪問該進程中加載的類
package de.robv.android.xposed.mods.tutorial;
import android.webkit.WebView.FindListener;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XC_MethodHook.MethodHookParam;
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam;
import static de.robv.android.xposed.XposedHelpers.findAndHookMethod;
public class Tutorial6 implements IXposedHookLoadPackage {
@Override
public void handleLoadPackage(LoadPackageParam param) throws Throwable {
if(!param.packageName.equals("com.android.systemui"))
return;
findAndHookMethod("com.android.systemui.statusbar.policy.Clock",param.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
}
});
}
}
XposedHelpers是一個重要的工具類,推薦用Eclipse的同學靜態導入該類中的方法 import static de.robv.android.xposed.XposedHelpers.findAndHookMethod;
。該類能夠通過反射機制來訪問方法、構造器、域。
findAndHookMehthod(String packageName,Class<?> clazz, String methodName, Object... args))
方法來對函數進行Hook。如果在方法前和方法後Hook,該方法最後一個參數需要實現XC_MethodHook
類的beforeHookedMethod
和afterHookedMethod
方法,如果想要替換整個方法,則需要實現XC_MethodReplacement
類的replaceHookedMethod
方法
XposedBridge保存了每個Hook方法的回調方法。優先級高的回調方法被優先調用A.before -> B.before -> original method -> B.after -> A.after
package de.robv.android.xposed.mods.tutorial;
import android.graphics.Color;
import android.webkit.WebView.FindListener;
import android.widget.TextView;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XC_MethodHook.MethodHookParam;
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam;
import static de.robv.android.xposed.XposedHelpers.findAndHookMethod;
public class Tutorial6 implements IXposedHookLoadPackage {
@Override
public void handleLoadPackage(LoadPackageParam param) throws Throwable {
if(!param.packageName.equals("com.android.systemui"))
return;
findAndHookMethod("com.android.systemui.statusbar.policy.Clock",param.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 {
TextView tv = (TextView) param.thisObject;//獲取調用該方法類的對象
String text = tv.getText().toString();
tv.setText(text + " :)");
tv.setTextColor(Color.RED);
}
});
}
}