Android Protect-0.Xposed开发教程(官方Development tutorial)

你想要学习如何创建一个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,如果通过反编译来验证逻辑猜想的话,只能多次的反编译代码,修改代码,重新签名打包来完成。而使用Xposedhook功能,你不能修改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 version90也有可能正常工作。

我一直建议用户使用最新的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.

  1. name:xposedmodule,value:true:搜寻的特定标记,value必须为true。
  2. name:xposedminversion,value:上一节获得的API version
  3. 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,有一个methodupdateClock,似乎每分钟都调用一次用于更新时间。

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

findAndHookMethod是一个Helper函数,注意静态导入部分,它使用SystemUIclassloader寻找Clock类,然后寻找其中的updateClock方法,如果这个方法有多个参数,还需要列出参数的class类型。最后一个参数,需要提供一个XC_MethodHook的实现类。如果是很小的修改,可以直接使用匿名内部类,如果改动很大,建议单独创建一个实现类。

XC_MethodHook中有两个你可以重写的methodbeforeHookedMethodafterHookedMethod。根据名字很容易知道这两个method在被hook的method前后执行。你可以使用beforeHookedMethod在方法调用前改变参数(通过param.args)。甚至阻止调用原来的method(发送你自己的result)。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, getTextsetTextColor之类的方法。我们的改动应该在原有的方法设置了新的时间以后进行。因此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/

猜你喜欢

转载自blog.csdn.net/hgy413/article/details/86000300
今日推荐