文章目录
下载安装
XposedInstaller_3.1.5.apk
如果是MENU手机,可能会出现错误,cp: can't create '/system/xposed.prop': Read-only file system
这时解决方案:https://bbs.pediy.com/thread-228375.htm
你想要学习如何创建一个Xposed的module吗?通过阅读本文就可以,不仅包括技术上“创建这个文件,并插入…”等等,同时让你知道背后的实现原理,知其然,也知其所以然。你会得到一个更好的理解如果你通读整篇文章。
修改主题
本文带领大家重新创建Red Clock例子,它可以在Github上找到。它包括状态栏时钟的颜色更改为红色和添加一个笑脸表情。我选择这个例子,因为它是一个很小,但容易看到效果。此外,它使用了框架提供的一些基本的方法。
Xposed如何工作
动手coding之前你应该阅读下本章节,关于Xposed的工作原理,如果觉得很无聊也可以直接跳过。
Android系统里有一个叫“Zygote
”的进程,它是Android运行时的核心,每个Android应用都通过它的副本形式被fork
出来。Zygote
进程在系统启动时被/init.rc
脚本启动,同时启动了用来加载必要的classes和执行初始化方法的/system/bin/app_process
进程。
这就到了Xposed
起作用的地方,当Xposed framework安装成功后,一个扩展的app_process被拷贝到/system/bin
,这个扩展的app_process添加了一个额外的jar
到classpath中,以在一定的时机调用jar里的方法。比如,在VM创建完成后,甚至Zygote
的main方法被调用前。这样我们就可以在这个上下文context
执行我们想要的操作了。
这个jar位于/data/data/de.robv.android.xposed.installer/bin/XposedBridge.jar
,它的源码在这里。打开XposedBridge这个类,可以看到main
方法,这就是我上文中所写过的,它在每个进程的最开始部分被调用。一些初始化和modules的加载也在这个时机被完成(稍后会讲模块的加载)。
方法的hook/替换
Xposed真正强大的地方在于对Method
调用的hook
,如果通过反编译来验证逻辑猜想的话,只能多次的反编译代码,修改代码,重新签名打包来完成。而使用Xposed的hook
功能,你不能修改Method中的代码,然而,你可以在目标Method
前后插入要执行的代码,Method
是java中最小的执行单元。
XposedBridge有一个private native方法:hookMethodNative
,这个方法也在扩展后的 app_process 中被实现了,这意味着,每次被hook的方法被调用时,hookMethodNative
将被调用,在这个方法里,XposedBridge中的handleHookedMethod
将被调用,通过this参数传递了被调用method
的信息。然后这个方法回调注册过该method
的hook方法,对参数进行修改,修改运行结果等,非常灵活。
理论到此为止,接下来开始创建一个xposed module
创建工程
一个xposed module本质上是一个普通的app,只是多了几个meta data
和文件而已。因此,首先创建一个新的Android项目,我假设你以前做过这个。如果没有,安卓官方文档讲的很详细。对于SDK,我选择了4.0.3(API15)。我建议你也使用这个,因为xposed module没有界面,因此不必要创建Activity,这时,应该已经创建好了一个空的工程项目。
使工程成为Xposed模块
现在我们把工程变成Xposed能加载的模块, 分为以下几个步骤:
将Xposed Framework API添加到项目中
每个Xposed module需要引用这个API库,下面描述了正确做法, 请务必记住API版本,您将在下一步中使用它。
Android Studio(基于Gradle)
Xposed Framework API发布在Bintray / jCenter上:
https://bintray.com/rovo89/de.robv.android.xposed/api
只需在app/build.gradle
中添加Gradle依赖项,即可轻松引用它:
repositories {
jcenter();
}
dependencies {
provided 'de.robv.android.xposed:api:82'
}
需要特别注意的是,这里使用了provide
而不是compile
,后者会把api库的classes打包到apk中,会导致bug,特别是在Android 4.x设备上。使用provided
只是让module可以通过编译,在apk中只有对api的引用,真正的实现在由运行设备的Xposed Framework提供。
大部分情况下,repositories
声明已经存在。这时只需要添加provided
这行到dependencies
中就可以了。
可以通过如下方式引入文档和源码:
provided 'de.robv.android.xposed:api:82'
provided 'de.robv.android.xposed:api:82:sources'
通过这种方式Gradle会缓存文件,Android Studio会自动把第二个jar设置为第一个jar的源。
另外,需要禁用AS的instant run(File -> Settings -> Build, Execution, Deployment -> Instant Run
),否则你自己的classes不是直接打包进apk,而是通过一个子应用,xposed不能处理这种情况。
Eclipse
在Eclipse中,您必须从此处手动下载jar:https://jcenter.bintray.com/de/robv/android/xposed/api/
我建议jar放到一个名为lib的新建目录中,不要直接放到libs,因为放到libs中,jar包的classes文件将会被自动打包进apk,但我们只能引用它们(参看上文)。右键jar文件,Build Path -> Add to Build Path
。
API 版本
一般来说,API version
等于构建它的Xposed version
,但是只有部分framework修改会导致API接口改变,只有在API接口改变时,我才会更新API version
,因此API version
小于等于对应的Xposed version
,当你使用API version
82编译了一个module,它在Xposed version
90也有可能正常工作。
我一直建议用户使用最新的Xposed version
,以保证可以使用最高的API version
。在AndroidManifest.xml里设置xposedminversion
的值为你使用的API version
。如果你依赖一个未修改API version
的修改(bugfix),只要修改xposedminversion
的值为最新的Xposed version
就可以了。
Xposed Framework API的JAVADOC文档
http://api.xposed.info
AndroidManifest.xml
Xposed Installer的模块列表搜寻所有带有特定meta flag
(xposedmodule
)标记的应用程序,到AndroidManifest.xml => Application => Application Nodes (at the bottom) => Add => Meta Data.在Application节点底部添加几个meta data
.
- name:
xposedmodule
,value:true
:搜寻的特定标记,value必须为true。 - name
:xposedminversion
,value:上一节获得的API version
。 - name:
xposeddescription
,value:module简单的描述。
添加完成后,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>
Module实现
现在可以为module创建一个class了,我创建的class名字是Tutorial,包名是:de.robv.android.xposed.mods.tutorial:
package de.robv.android.xposed.mods.tutorial;
public class Tutorial {
}
作为第一步,我们只添加一些log,用来证明我们的module被加载了。module有几个hook入口,具体选择那个入口取决于你想修改的内容。你可以让Xposed调用你module中的函数,在系统启动时,或一个新的app被加载时,或一个app的资源被初始化时等待。
所有的hook入口类必须是IXposedMod
的实现类,在这里(app被加载),你只需要实现IXposedHookLoadPackage
,它只有一个方法带有一个参数,这个参数带有上下文信息。这个例子里,我们记录下被加载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(tag是Xposed),同时输出到文件:/data/data/de.robv.android.xposed.installer/log/debug.log(可以通过Xposed Installer的日志页查看)。
assets/xposed_init
最后要做的一件事是告诉XposedBridge哪些类包含hook入口,这项工作通过一个叫 xposed_init
的文件完成。在assets目录下新建这个文件,该文件每行包含一个类的全名,在这里是:de.robv.android.xposed.mods.tutorial.Tutorial
。
编译运行
保存上述修改,运行这个application,因为是首次安装这个module,需要首先打开Xposed Installer激活。在Modules 页找到刚安装的module,勾选激活,重启设备, 通过logcat
观察输出,或者通过Xposed Installer的日志页,应该看到如下输出:
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模块。 它可能比写日志更有用…
探索你的目标并寻找修改它的方式
好了,下面要开始讲的部分也许会非常不同,这取决于你想做什么。如果你之前修改过apk,也许你会知道在这里应当如何思考。总的来说,你需要了解目标的一些实现细节。在本教程中,目标选定为状态栏的时钟。这有助于了解到状态栏以及系统UI的一部分的其他一些东西。现在让我们开始探索。
方法一:反编译,能获取到精确信息,但不易读,因为反编译后是smali格式。 方法二:获得AOSP源代码。比如这里,这里。这可能和你的ROM完全不同,但在本例中他们的实现是相似的甚至是相同的。我会先看AOSP,然后看看这么做够不够。如果不够,再观察反编译代码。
你可以找找名称中有“clock
”的类。其他需要找的是用到的资源和布局。如果你下载官方的AOSP代码,你可以从 frameworks/base/packages/SystemUI
开始查找。你会找到好几处“clock
”出现的地方,这是正常的,实际上会有不同的method
来实现修改。记住我们只能hook method
,所以你必须找到一个能够插入代码实现功能的地方,要么在method
的前面要么在后面,或者是替换掉method
。应该尽量寻找比较特殊的method
,不要寻找那些被高频或多个地方调用的method
,以避免性能问题或其他非预期的结果。
在本例当中,你或许会发现布局 res/layout/status_bar.xml
包含对com.android.systemui.statusbar.policy.Clock
的自定义view的引用。现在你可能会有很多点子。文字的颜色是通过textAppearance
属性定义的,所以最简单的修改方法是修改外观定义。但是,对Xposed来说修改styles是无法办到的,替换状态栏的layout
倒是有可能。但对于你试图做出的小小修改来说是杀鸡用牛刀。观察这个class,有一个method
叫updateClock
,似乎每分钟都调用一次用于更新时间。
final void updateClock() {
mCalendar.setTimeInMillis(System.currentTimeMillis());
setText(getSmallTime());
}
这看起来很适合修改,这个method功能很单一,只用来更新时间,如果在它后边修改字体颜色,应该可以实现。
对于单独修改字体颜色部分,有一种更好的办法。参见“替换资源”中“修改布局”的例子。
使用反射寻找要hook的method
我们已经知道com.android.systemui.statusbar.policy.Clock
有一个我们想要拦截的方法updateClock
。同时发现这个class在SystemUI的源码中,因此这个类只存在于SystemUI进程中。因此我们在handleLoadPackage
中需要做一些条件判断:
public void handleLoadPackage(LoadPackageParam lpparam) throws Throwable {
if (!lpparam.packageName.equals("com.android.systemui"))
return;
XposedBridge.log("we are in SystemUI!");
}
使用lpparam
参数,我们可以轻松检查是否在正确的包中。。一旦我们验证了这一点,我们就可以使用lpparam.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;
XposedHelpers.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
}
});
}
}
如果带有参数,那么在XC_MethodHook
之前加上若干参数,比如hook AMS
中的processContentProviderPublishTimedOutLocked
,代码如下:
XposedHelpers.findAndHookMethod("com.android.server.am.ActivityManagerService",
lpparam.classLoader,
"processContentProviderPublishTimedOutLocked",
Class.forName("com.android.server.am.ProcessRecord", false, lpparam.classLoader),
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
param.setResult(null); // 在beforeHookedMethod调用setResult,会阻止调用原始函数
Log.e(TAG, "beforeHookedMethod:processContentProviderPublishTimedOutLocked");
}
});
findAndHookMethod
是一个Helper函数,注意静态导入部分,它使用SystemUI的classloader寻找Clock类,然后寻找其中的updateClock
方法,如果这个方法有多个参数,还需要列出参数的class类型。最后一个参数,需要提供一个XC_MethodHook的实现类。如果是很小的修改,可以直接使用匿名内部类,如果改动很大,建议单独创建一个实现类。
XC_MethodHook中有两个你可以重写的method
。beforeHookedMethod
和 afterHookedMethod
。根据名字很容易知道这两个method
在被hook的method
前后执行。你可以使用beforeHookedMethod
在方法调用前改变参数(通过param.args
)。如果在beforeHookedMethod
时调用了param.setResult
,原来的method
就不会被调用。afterHookedMethod
可以用来做一些基于原来method的result
的事。你也可以在这里操纵result
。
当然,你可以在method
调用的前/后添加你自己要执行的代码。
如果想完全替换一个method,使用XC_MethodReplacement,只需要重写方法:
replaceHookedMethod
XposedBridge
为每个被hook的method
维护一个已注册的回调列表。优先级(在hookMethod定义)最高的先被回调,原始method
优先级最低。所以,如果你对一个method
hook了两个回调A(高优先级)和B(默认优先级),当method
被执行时,回调流程如下:A.before -> B.before -> original method -> B.after -> A.after
。所以A可以影响B看到的参数,method
的结果会先被B处理,A决定最终的结果。
最后一步:执行hook代码
我们已经hook了updateClock
方法,现在开始修改写东西。
第一步:我们有具体的Clock类型的引用么?是的,我们有。这就是param.thisObject参数。所以如果方法通过myClock.updateClock()
被调用,那么param.thisObject 就是 myClock
。
第二步:我们能做什么?Clock这个类是不可用的,不能强转(想都不要想),然而它继承于TextView,所以一旦你把Clock的引用转换为TextView,你可以使用像setText
, getText
和 setTextColor
之类的方法。我们的改动应该在原有的方法设置了新的时间以后进行。因此beforeHookedMethod
留空。
所以以下是完整的源代码:
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
处理程序使用了默认的优先级。所以你不清楚哪个会胜出(实际上这依赖于处理方法的字符串表示形式,但不要依赖这个方式)。
参考:
https://github.com/rovo89/XposedBridge/wiki/Development-tutorial
http://yuanfentiank789.github.io/2017/04/01/xposeddev/