Xposed development tutorial (translated from the official)

Official original text: https://github.com/rovo89/XposedBridge/wiki/Development-tutorial

Development tutorial

Ok... Are you planning to learn how to create a new Xposed module? Then read this tutorial (or call it "general talk") and learn how to achieve this goal step by step. This not only includes technical content such as "create and insert", but also contains some behind-the-scenes ideas that can gradually make you aware of what you are doing, why you are doing this thing, and the value of doing this thing. If you think "The article is so long I don't want to read", you can just look at the final source code and the chapter "Building Xposed Module Project". Since you don't have to understand everything thoroughly, you can save the time of reading this tutorial. But it is still recommended that you read this tutorial in its entirety, which will give you a better understanding of the development of Xposed modules.

Tutorial goal

You will rewrite the "red clock" example, which can be downloaded from the first article or found on Github. It can change the color of the clock in the status bar to red and add a small smiley face. Because this project is very small but you can see the changes very obviously, and uses some basic methods of the Xposed framework, I chose this project as an example.

How Xposed works

Before starting the modification work, you should have a rough understanding of how the Xposed framework works (if you find this boring, you can skip it). So, how does it work:

There is a process called "Zygote", which is the heart of the Android runtime. Every application is started by it and it hosts system services. This process is started by the script /init.rc when the phone boots. This process will start with /system/bin/app_process, the guy who loads the necessary classes and calls the initialization function.

Now it's Xposed's turn. When you install the framework, an extensible and executable app_process copied from system/bin. This extension will load an extra jar to the classpath when the process starts and call some functions elsewhere here. For example, when the virtual machine is just started and Zygote's main function is called, the above things will be done. In it, Xposed is part of Zygote and can be used inside it.

The jar file is /data/xposed/XposedBridge.jar and its source code can be found here. Observe the XposedBridge class, you can find the main function. This is what I mentioned above, this function will be called in the very early stage of the process. When some loading has been completed and the module is loaded (I will talk about module loading later).

Function hooking/replacement

The implementation of Xposed relies on function call "hooks". When you modify the APK file and touch the smali code, you can directly insert/modify the code. If you don't want to modify the APK but want to achieve the same effect, you can modify the binary code or the compiled code, but it is not recommended. Because that requires exactly the same code to show the changes you made. Even if you decompile it while it is running and try to make some modifications to the smali code obtained based on the pattern search, this may cause the result to be biased due to the use of different variable (declared) numbers. So I decided to modify the smallest unit in Java that can be clearly defined: function.

The XposedBridge class has a private, local function hookMethodNative. This function

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.

Well, the theory course is over here, let's build an Xposed module!

New Project

An Xposed module is a standard application. There are just some special metadata and files. So first create a new Android project. I assume you have created a new Android project. If not, there are many detailed steps and information in the official development documentation. When choosing the SDK version, I chose 4.0.3 (API 15) because this is the version that my phone is running. I suggest you choose this SDK too, don't be a guinea pig for the time being. You do not need to create an activity, because the modification does not require any user interface. After setting up the project, you should get a blank project.

Turn your project into an Xposed module

Now let us turn this project into something that Xposed can load-a module. This requires several steps.

AndroidManifest.xml

The module list of the Xposed installer will look for apps with specific metadata. You can create it via AndroidManifest.xml => Application => Application Nodes (at the bottom) => Add => Meta Data. The name should be xposedmodule and the value should be true. Keep resource blank. You should repeat this step to modify xposedminversion, and then set the value to the API version you are using (for example, as shown below). At this time, the XML source code is as follows:

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

Next, declare the XposedBridge API. You can import the XposedBridge project and add it by reference, but then Eclipse will try to install it (to a wrong location) when you test the application. So a better way is to download XposedBridgeApi.jar from here and copy it to the root folder of your project. Then right click on it and select Build Path => Add to Build Path.

A better alternative is to download the XposedLibrary project and import it into your Eclipse workbench. So you can reference XposedBridgeApi.jar in your project: In the "Libraries" tab of your project's build path configuration, click "Add JARs", and then select "XposedLibrary => XposedBridgeApi.jar". The advantage of this is that you only need to keep a copy of the API you are using, so that you can upgrade your module as soon as possible by checking the new version of the API (it is best to check the repository with Git). If you use this method, you can use some preference UI classes when overriding user settings. In the future, more things will be added. You can find out how to fix it here.

To find out which API version you are using, open the Package Explorer in your project, it is in project => Referenced Libraries => XposedBridgeApi.jar => assets => VERSION.
Module implementation

Now you can create a new class for your module. I named him "Tutorial" and the package name is de.robv.android.xposed.mods.tutorial:

1

2

3

4

5

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

 

public class Tutorial {

 

}

First, we need to output some logs to show that this module has been loaded. There are only several entry points for a module. Which one to enter from depends on what you want to modify. For example, you can make Xposed call your function when the Android system is started, or when an application is about to be loaded, or when an application's resource file is loaded, etc.

In this tutorial, you will learn the necessary changes that must be made in a particular application, so for now we use the entry point "Prompt me when an application is loaded". All entry points are marked by a sub-interface of IXposedMod. In this case, it is IXposedHookLoadPackage which you need to implement. In fact, it is just a method, with a parameter, this parameter can bring you more other information, such as the context of the imported module. In fact, it is a function with only one parameter. The incoming parameter can tell you more information that gives more information about the context to the implementing module. Now let's output the information of the loaded application, like the following:

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);

    }

}

This method will output information in the standard logcat, the tag is Xposed, and it will be saved in /data/xposed/debug.log.

assets/xposed_init

The only thing you don't know now is which class of XposedBridge the entry point exists in. In fact, it is achieved by calling xposed_init. Create a file named after the previous name in the assets folder. In this file, write the name of the class you want to be the entry point on one line. In this case, it is de.robv.android.xposed.mods.tutorial.Tutorial

Give it a try

Save your file. Then compile and run your project as an Android application. If you are installing for the first time, you need to enable it in the Xposed installer after the installation is complete, so that it can work. First verify in the Xposed installer whether you have installed the Xposed installation files. Then go to the "Modules" page. You should find your module on this page. Tick ​​the corresponding box to enable it. Then restart. At this time, you will find that the system is not different, but just check the log file, you should see something similar to the following:

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 函数,以确保所有函数按你期望的方式来调用。现在,祝你玩得开心!

Guess you like

Origin blog.csdn.net/THMAIL/article/details/112061434