Tutorial de desarrollo Xposed (traducido del oficial)

Texto original oficial: https://github.com/rovo89/XposedBridge/wiki/Development-tutorial

Tutorial de desarrollo

Ok ... ¿Estás planeando aprender a crear un nuevo módulo Xposed? Luego lea este tutorial (o llámelo "charla general") y aprenda cómo lograr este objetivo paso a paso. Esto no solo incluye contenido técnico como "crear e insertar", sino que también contiene algunas ideas entre bastidores que pueden hacerte consciente gradualmente de lo que estás haciendo, por qué estás creando este gadget y el valor de hacer este gadget. . Si cree que "El artículo es tan largo que no quiero leerlo", puede echar un vistazo al código fuente final y al capítulo "Creación del proyecto del módulo Xposed". Dado que no es necesario que comprenda todo a fondo, puede ahorrar el tiempo de leer este tutorial. Pero aún se recomienda que lea este tutorial en su totalidad, que le dará una mejor comprensión del desarrollo de los módulos Xposed.

Objetivo del tutorial

Volverá a escribir el ejemplo del "reloj rojo", que puede descargarse del primer artículo o encontrarse en Github. Puede cambiar el color del reloj en la barra de estado a rojo y agregar una pequeña carita sonriente. Debido a que este proyecto es muy pequeño pero se pueden ver los cambios de manera muy obvia y utiliza algunos métodos básicos del marco Xposed, elegí este proyecto como ejemplo.

Cómo funciona Xposed

Antes de comenzar el trabajo de modificación, debe tener una comprensión aproximada de cómo funciona el marco Xposed (si lo encuentra aburrido, puede omitirlo). Entonces, cómo funciona:

Existe un proceso llamado "Zygote", que es el corazón del tiempo de ejecución de Android. Cada aplicación se inicia y aloja servicios del sistema. Este proceso lo inicia el script /init.rc cuando se inicia el teléfono. Este proceso comenzará con / system / bin / app_process, el tipo que carga las clases necesarias y llama a la función de inicialización.

Ahora es el turno de Xposed. Cuando instala el marco, un proceso de aplicación extensible y ejecutable copiado de system / bin. Esta extensión cargará un jar adicional en la ruta de clases cuando se inicie el proceso y llamará a algunas funciones en otro lugar aquí. Por ejemplo, cuando la máquina virtual recién se inicia y se llama a la función principal de Zygote, se harán las cosas anteriores. En él, Xposed es parte de Zygote y se puede usar dentro de él.

El archivo jar es /data/xposed/XposedBridge.jar y su código fuente se puede encontrar aquí. Observe la clase XposedBridge, puede encontrar la función principal. Esto es lo que mencioné anteriormente, esta función se llamará en la etapa muy temprana del proceso. Cuando se ha completado una carga y el módulo está cargado (hablaré sobre la carga del módulo más adelante).

Función de enganche / sustitución

La implementación de Xposed se basa en la llamada de función "hooks". Cuando modifica el archivo APK y toca el código pequeño, puede insertar / modificar directamente el código. Si no desea modificar el APK pero desea lograr el mismo efecto, puede modificar el código binario o el código compilado, pero no es recomendable. Porque eso requiere exactamente el mismo código para mostrar los cambios que realizó. Incluso si lo descompila mientras se está ejecutando e intenta realizar algunas modificaciones en el código pequeño obtenido en función de la búsqueda del patrón, esto puede provocar que el resultado esté sesgado debido al uso de diferentes números de variable (declarados). Así que decidí modificar la unidad más pequeña en Java que se puede definir claramente: función.

La clase XposedBridge tiene una función local privada hookMethodNative. Esta función

XposedBridge tiene un método privado y nativo hookMethodNative. Este método también se implementa en el app_process extendido. Se necesita un objeto de método que puede obtener a través de la reflexión de Java y cambiar la definición interna de la máquina virtual del método. Cambiará el tipo de método a "nativo" y vinculará la implementación del método a su propio método genérico nativo. Eso significa que cada vez que se llama al método hooked, se llamará al método genérico sin que la persona que llama lo sepa. En este método, se llama al método handleHookedMethod en XposedBridge, pasando los argumentos a la llamada al método, la referencia this, etc. Y este método se encarga de llamar a los métodos que se han registrado para esta llamada al método. Esos pueden cambiar los argumentos de la llamada, luego llamar al método original y luego hacer algo con el resultado. O salte algo de eso. Es muy flexible.

Bueno, el curso de teoría ha terminado, ¡construyamos un módulo Xposed!

Nuevo proyecto

Un módulo Xposed es una aplicación estándar. Solo hay algunos metadatos y archivos especiales. Primero crea un nuevo proyecto de Android. Supongo que ha creado un nuevo proyecto de Android. Si no es así, hay muchos pasos detallados e información en la documentación oficial de desarrollo. Al elegir la versión SDK, elegí 4.0.3 (API 15) porque esta es la versión que está ejecutando mi teléfono. Le sugiero que elija este SDK también, no sea un conejillo de indias por el momento. No es necesario crear una actividad, porque la modificación no requiere ninguna interfaz de usuario. Después de configurar el proyecto, debería obtener un proyecto en blanco.

Convierta su proyecto en un módulo Xposed

Ahora convierta este proyecto en algo que Xposed pueda cargar: un módulo. Esto requiere varios pasos.

AndroidManifest.xml

La lista de módulos del instalador Xposed buscará aplicaciones con metadatos específicos. Puede crearlo a través de AndroidManifest.xml => Aplicación => Nodos de aplicación (en la parte inferior) => Agregar => Metadatos. El nombre debe ser xposedmodule y el valor debe ser verdadero. Mantenga el recurso en blanco. Debe repetir este paso para modificar xposedminversion y luego establecer el valor en la versión de API que está utilizando (por ejemplo, como se muestra a continuación). En este momento, el código fuente XML es el siguiente:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

dieciséis

<?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

A continuación, declare la API XposedBridge. Puede importar el proyecto XposedBridge y agregarlo por referencia, pero luego Eclipse intentará instalarlo (en una ubicación incorrecta) cuando pruebe la aplicación. Entonces, una mejor manera es descargar XposedBridgeApi.jar desde aquí y copiarlo en la carpeta raíz de su proyecto. Luego haga clic derecho sobre él y seleccione Build Path => Add to Build Path.

Una mejor alternativa es descargar el proyecto XposedLibrary e importarlo a su banco de trabajo Eclipse. Para que pueda hacer referencia a XposedBridgeApi.jar en su proyecto: En la pestaña "Bibliotecas" de la configuración de la ruta de compilación de su proyecto, haga clic en "Agregar JAR" y luego seleccione "XposedBridgeApi.jar". La ventaja de esto es que solo necesita mantener una copia de la API que usa, por lo que puede actualizar su módulo lo antes posible al verificar la nueva versión de la API (es mejor verificar el repositorio con Git). Si usa este método, puede usar algunas clases de IU de preferencia al anular la configuración del usuario. En el futuro, se agregarán más cosas. Puede averiguar cómo solucionarlo aquí.

Para saber qué versión de API está utilizando, abra el Explorador de paquetes en su proyecto, está en proyecto => Bibliotecas referenciadas => XposedBridgeApi.jar => assets => VERSION.
Implementación del módulo

Ahora puede crear una nueva clase para su módulo. Lo llamé "Tutorial" y el nombre del paquete es de.robv.android.xposed.mods.tutorial:

1

2

3

4

5

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

 

public class Tutorial {

 

}

Primero, necesitamos generar algunos registros para mostrar que este módulo se ha cargado. Solo hay varios puntos de entrada para un módulo. De cuál ingresar depende de lo que desee modificar. Por ejemplo, puede hacer que Xposed llame a su función cuando se inicia el sistema Android, o cuando una aplicación está a punto de cargarse, o cuando se carga el archivo de recursos de una aplicación, etc.

En este tutorial, aprenderá los cambios necesarios que se deben realizar en una aplicación en particular, por lo que por ahora usamos el punto de entrada "Avisarme cuando se cargue una aplicación". Todos los puntos de entrada están marcados por una subinterfaz de IXposedMod. En este caso, es IXposedHookLoadPackage el que necesita implementar, de hecho, es solo un método, con un parámetro, este parámetro puede traerle más información, como el contexto del módulo importado. De hecho, es una función con un solo parámetro. El parámetro entrante puede brindarle más información que le brinda más información sobre el contexto del módulo de implementación. Ahora, generemos la información de la aplicación cargada, como se muestra a continuación:

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

    }

}

Este método generará información en el logcat estándar, la etiqueta es Xposed y se guardará en /data/xposed/debug.log.

activos / xposed_init

Lo único que no sabe ahora es en qué clase de XposedBridge existe el punto de entrada. De hecho, se logra llamando a xposed_init. Cree un archivo con el nombre del nombre anterior en la carpeta de activos. En este archivo, escriba el nombre de la clase que desea que sea el punto de entrada en una línea. En este caso, es de.robv.android.xposed.mods.tutorial.Tutorial

Darle una oportunidad

Guarde su archivo. Luego compile y ejecute su proyecto como una aplicación de Android. Si está instalando por primera vez, debe habilitarlo en el instalador de Xposed después de que se complete la instalación, para que pueda funcionar. Primero verifique en el instalador de Xposed si ha instalado los archivos de instalación de Xposed. Luego vaya a la página "Módulos". Debería encontrar su módulo en esta página. Marque la casilla correspondiente para habilitarlo. Luego reinicie. En este momento, encontrará que el sistema no es diferente, pero solo verifique el archivo de registro, debería ver algo similar a lo siguiente:

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

Supongo que te gusta

Origin blog.csdn.net/THMAIL/article/details/112061434
Recomendado
Clasificación