Xposed 开发教程(翻译自官方)

CodeingBoy 6月 19, 2015

本教程转载自官方,并由本人(CodeingBoy)翻译。非经允许禁止转载。


开发教程

好吧 …… 你打算学习怎么建立一个新的 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源代码就像下面那样:

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 :

首先,我们需要输出一些日志以表明这个模块已被加载。一个模块只有几种入口点。从哪一个进入取决于你想要修改什么。例如你可以在 Android 系统启动的时候让 Xposed 调用你的函数,或者在一个应用即将被加载的时候,或者在一个应用的资源文件加载的时候,等等。

在本教程中,你将会学习到在一个特定的应用中必须做出的必要更改,所以现在我们使用“一个应用被加载时提示我”的入口点。所有的入口点被一个 IXposedMod的 sub-interface 所标记。在本例中,它是 IXposedHookLoadPackage which you need to implement. 实际上它只是一个方法,带有一个参数,这个参数可以带给你更多别的信息,例如导入模块的上下文。实际上它是只有一个参数的一个函数,这个传入的参数可以告诉你更多信息that gives more information about the context to the implementing module. 现在让我们输出被加载应用的信息吧,就像下面一样:

这个方法会在标准的 logcat 中输出信息,tag 为 Xposed ,并且保存在 /data/xposed/debug.log。

assets/xposed_init

现在你不知道的唯一一件事就是到底入口点存在于 XposedBridge 的哪个类里面。其实它通过调用 xposed_init 来实现的。 在 assets 文件夹建立一个以前面的名字命名的文件。在这个文件中,在一行里面写上你想要做入口点的类名。本例中,它就是 de.robv.android.xposed.mods.tutorial.Tutorial

试一试

保存你的文件。然后以安卓应用的方式编译并运行你的项目。如果你是第一次安装,你需要在安装完毕后到 Xposed 安装器启用它,这样它才能工作。先在 Xposed 安装器中核实你是否安装了 Xposed 安装文件。然后前往 “模块” 页面。你应该在这个页面找到你的模块。把对应的方框打上勾以启用它。然后重启。这时候你会发现系统并没有什么不一样,但是只要检查一下日志文件,你应该会看到和下面类似的内容:

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 的函数,在每分钟要更新时间的时候,它会被调用来更新时间:

看上去这是一个做修改的好地方,它是一个非常具体的方法,它只会将时钟的文字设置一下,不会做别的什么事情。如果我们在它调用之后加一些可以修改颜色和文本的修改代码,那应该就能达成我们的目的了。开始干吧!

如果你只想修改文本的颜色,有一个更好的办法。你可以查看“替换资源”的“修改布局”章,那里说明了如何用反射机制来寻找和挂钩一个函数。

那么现在我们来总结一下我们得到的信息。我们找到在 com.android.systemui.statusbar.policy.Clock 找到了一个叫 updateClock 的函数,我们将在这个函数进行注入修改。而我们是在 SystemUI 的源代码中找到它的,所以它只会对 SystemUI 这个进程起作用,一些框架下的类也会起作用。如果我们尝试在 handleLoadPackage 函数中直接取得这个类一些信息和引用,很可能会因为进程不符而失败。所以现在我们先开始让代码只在对的包里面运行:

使用传入的参数,我们可以很容易地检查我们是否正在正确的包中运行。只要我们确认是正确的包,我们就使用 ClassLoader (this 变量中有引用)来获取访问包中的这个类的权限。现在我们就寻找 com.android.systemui.statusbar.policy.Clock 这个类的 updateClock 函数,并且告诉 XposedBridge 去做一个挂钩:

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.

下面是完整的源代码:

令人满意的结果

现在重新安装 / 启动你的应用。由于你已经在第一次打开时启用了模块,所以你就不用再启用模块了,只需要重启一次。然而,如果你正在使用 red clock 示例模块,你最好去禁用掉。如果两个都启用,它们都会使用默认优先级来注入 updateClock ,这样你就不知道哪个模块在工作。 (it actually depends on the string representation of the handler method, but don't rely on that).

总结

我知道这个教程非常冗长。但我希望你现在不仅可以实现一个"green clock",更可以完成一些完全不同的事情。寻找一个绝佳的挂钩原函数需要一定的经验,所以先从比较简单的事情开始把。在初期,建议你多尝试使用 log 函数,以确保所有函数按你期望的方式来调用。现在,祝你玩得开心!

本文采用 CC BY-NC-SA 3.0 协议进行许可,在您遵循此协议的情况下,可以自由共享与演绎本文章。
本文链接:https://blog.codeingboy.me/xposed-development-tutorial-official/

发表评论

电子邮件地址不会被公开。 必填项已用*标注