Xposed 简介 Xposed 框架是 Android 平台上一个非常著名且强大的开源框架,使用它能够对系统进程内运行的方法进行 hook,所以可以用它来做一些系统层面的工作,它拥有无限可能的灵活性,目前市面上基于 Xposed 框架下开发 Xposed 子模块已经数不胜数了。
原理简析 Android 系统运行的核心和起点是 Zygote 进程,所有应用都是从它 fork 子进程产生的,当系统开始运行时由 init.rc 脚本启动, 使用 /system/bin/app_process 程序完成启动,它加载所需的类并调用初始化方法。
Xposed 框架将在这个地方发挥作用,当 Xposed 框架被安装时,一个被扩展的 app_process 程序将被复制到 /system/bin/ 中,这个扩展的 app_process 将向类的路径附加一个 jar 文件,并在某些位置调用其方法,可能是虚拟机创建之后,或者在 Zygote 进程的 main 方法之前。在这个方法里,我们可以在其上下文中做插桩。
 
环境配置 Xposed 框架会替换系统的关键文件,所以需要 root 权限,获取 root 权限之后,安装 Xposed 框架。
Xposed 下载地址: http://repo.xposed.info/module/de.robv.android.xposed.installer 
安装 Xposed 框架的 APK 后,进入并点击 INSTALL/UPDATE 下面的版本号,即可开始安装。
提示:安装可能会导致设备无限重启或变砖,所以一定要确认适合自己的设备后再安装。 
模块开发 Xposed 框架安装完毕后即可进行 Xposed 模块的开发,我们自定义的功能都是在 Xposed 模块中实现的。
它是一个普通的 APK,包含一些实现 Xposed 依赖库提供的特定接口的类,当它被安装到设备后,被 Xposed 框架调起发挥作用,下面开始进行 Xposed 模块的开发。
项目依赖 首先添加项目依赖。
确认根 Project 或 Module 添加了 jcenter 的仓库渠道。 
 
1 2 3 repositories {     jcenter(); } 
 
添加 Xposed 库的编译支持。 
 
Android Studio 3.0+ 的版本
1 2 3 dependencies {     compileOnly 'de.robv.android.xposed:api:82'  } 
 
低于 3.0 的版本
1 2 3 dependencies {     provided 'de.robv.android.xposed:api:82'  } 
 
注意:这里的选项是指定只参与编译,不需要真正的导入它的类 ,因为系统的 Xposed 框架内已经提供了这些类,所以不要用 implementation(Studio 3.0+) 和 complie(lower than 3.0)。
方法钩子 方法钩子是 Xposed 的核心功能,一般通过对 APK 反编译后进行修改的方式,可以在任何位置插入和更改代码,但是必须重新编译打包整个包。使用 Xposed 可以放置方法钩子,不能修改方法代码,不过可以在方法调用的前后插入代码。
XposedBridge 中有一个私有的 hookMethod 本地方法,它在扩展后的 app_process 中实现,它可以将方法更改为本地方法,并链接到自己的泛型本地方法,每次调用钩子方法时,都将调用泛型方法,但是调用方无需知晓,在这个方法中,会调用 XposedBridge 的 handleHookedMethod 方法,handleHookedMethod 则会调用注册的回调方法。这里可以更改调用的参数、更改实例或静态变量、调用其他方法或对结果执行某些操作…或者跳过任何内容。 
下面在 Android Studio 中创建一个 Module,在 AndroidManifet.xml 的 application 标签中添加 3 个 meta-data 标签。 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 <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 ="82"  /> </application > 
 
xposedmodule 是固定的配置,说明自己是一个 Xposed 模块。 
xposeddescription 是 Xposed 模块的简要描述,在 Xposed 框架应用中会展示出来。 
xposedminversion 是 Xposed 模块的 API 版本,通常情况下,和使用的 Xposed API 版本应该一致。 
 
实现 IXposedHookLoadPackage 接口,这个是放置方法钩子的入口点,如果需要实现资源替换,需要实现另外的接口,当 Android 系统启动时,每一个应用加载启动时都会回调这个接口。 
 
1 2 3 4 5 6 7 8 package  io.l0neman.xposedproject;public  class  MyXposedStub  implements  IXposedHookLoadPackage   {  @Override    public  void  handleLoadPackage (final  XC_LoadPackage.LoadPackageParam lpparam)  throws  Throwable  {          XposedBridge.log("Loading app: "  + lpparam.packageName); } 
 
最后需要配置入口点,在 assets 目录下,建立一个 xposed_init 文件,它的每行都可以指定一个入口点的全类名。 
 
1 io.l0neman.xposedproject.MyXposedStub 
 
现在安装 APK,然后打开 Xposed 框架,勾选这个模块,重启手机,就能看到日志。 
 
1 2 3 4 5 6 Loading Xposed (for Zygote)... Loading modules from /data/app/io.l0neman.xposedproject-1.apk   Loading class io.l0neman.xposedproject Loaded app: com.android.systemui Loaded app: com.android.settings ... 
 
上面的基础工作做完了,现在就可以使用 XposedHelpers 的辅助方法来获取方法钩子了,下面对一个应用的 Application 类进行 hook。 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public  class  MyXposedClient  implements  IXposedHookLoadPackage   {  @Override    public  void  handleLoadPackage (final  XC_LoadPackage.LoadPackageParam lpparam)  throws  Throwable  {          XposedBridge.log("Loading app: "  + lpparam.packageName);     if  (!lpparam.packageName.equales("com.android.systemui" )) {        return ;      }     XposedHelpers.findAndHookMethod(Application.class, "attach" , Context.class,         new  XC_MethodHook() {           @Override            protected  void  beforeHookedMethod (MethodHookParam param)  throws  Throwable  {                                                  }           @Override  protected  void  afterHookedMethod (MethodHookParam param)  throws  Throwable  {                          Context context = (Context) param.args[0 ];           }     }); } 
 
除了以上的方法 hook 之外,也可对应用内的任意类型进行反射获取。 
 
资源替换 目前还没有使用资源替换的方法,后续会补充。
避免重启的方法 每次更改 Xposed 模块的代码后,都需要从新启动 Android 设备,非常不利于调试,所以可以用一种方法只在第一次重启,以后都不用重启了。
原理就是,首先编写一个 XposedStub APK,然后在其中通过包名的形式寻找另一个实现 Xposed 逻辑的 APK,通过动态加载的方式加载调用 APK 中的类和方法,这时只需要每次更改实现逻辑的 APK 即可,无需重启刷新 XposedStub 这个 APK 里面的逻辑了,相当于搭了一个桥梁。
首先实现一个实现逻辑的 APK,这里我需要一个能够 hook 类的方法,创建一个参数接收的类。 
 
1 2 3 4 5 6 7 8 9 10 11 package  io.l0neman.xposedproject;import  android.util.Log;import  de.robv.android.xposed.callbacks.XC_LoadPackage;public  class  MyXposedClient   {  private  static  final  String TAG = "MyXposedClient" ;   public  static  void  handle (String seflApkPath, XC_LoadPackage.LoadPackageParam llparam)   {     Log.d(TAG, "accept ok: "  + llparam.packageName);   } } 
 
然后编写 XposedStub APK,它将作为一个和 Xposed 框架通信的中间角色,接收 Xposed 框架的回调结果,并转交给目标 APK,下面是具体逻辑。 
 
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 public  class  MyXposedStub  implements  IXposedHookLoadPackage   {  private  static  final  String TAG = "MyXposedStub" ;      private  static  final  String TARGET_PACKAGE_NAME = "io.l0neman.xposedproject" ;      private  static  final  String TARGET_INJECT_CLASS_NAME =       "MyXposedClient" ;      private  static  final  String TARGET_INJECT_METHOD = "handleLoadPackage" ;   @Override    public  void  handleLoadPackage (final  XC_LoadPackage.LoadPackageParam lpparam)  throws  Throwable  {     log("loading app: "  + lpparam.packageName);          XposedHelpers.findAndHookMethod(Application.class, "attach" , Context.class,         new  XC_MethodHook() {           @Override  protected  void  afterHookedMethod (MethodHookParam param)  throws  Throwable  {             Context context = (Context) param.args[0 ];             callTarget(context, lpparam);           }         });   }      private  void  callTarget (Context context, XC_LoadPackage.LoadPackageParam lpparam)        throws  Exception  {    File apkPath = findApkPath(context, TARGET_PACKAGE_NAME);     if  (apkPath == null ) { return ; }     log("hit apk: "  + apkPath);     PathClassLoader pathClassLoader = new  PathClassLoader(         apkPath.getAbsolutePath(), ClassLoader.getSystemClassLoader()     );          Class<?> targetClass = Class.forName(TARGET_INJECT_CLASS_NAME, true , pathClassLoader);     Method handle = targetClass.getMethod(TARGET_INJECT_METHOD, String.class,         XC_LoadPackage.LoadPackageParam.class);     handle.invoke(null , apkPath.getPath(), lpparam);     log("inject ok" );   }      private  static  File findApkPath (Context context, String packaegName)   {     if  (context == null ) { throw  new  AssertionError("context is null" ); }     try  {       Context targetContext = context.createPackageContext(packaegName,           Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY       );       String apkPath = targetContext.getPackageCodePath();       return  new  File(apkPath);     } catch  (PackageManager.NameNotFoundException ignore) {}     return  null ;   }   private  void  log (String log)   { Log.d(TAG, log); } } 
 
提示 编写 Xposed 模块时需要清楚的知道自己的代码处于什么环境,当 handleLoadPackage 被回调时,说明一个新的代码包(常见为一个新的应用)被加载起来了,此时下面的代码将运行在目标应用的进程中,当模块具有 UI 界面时,模块的代码将分别运行在本身的应用进程和注入的目标应用进程中,如果需要进行设配置,可能需要进行进程间通信。
由于一些原因,Xposed 框架的作者不再提供 Android 9 系统之上的版本,如果需要在 Android 9 平台或以上版本使用 Xposed,推荐使用基于 Magisk(面具框架)的 EdXposed 替代,目前它的最新版本具有即时更新模块的功能,不用再重启设备了。
Magisk 地址:https://github.com/topjohnwu/Magisk 
Edxposed 地址:https://github.com/ElderDrivers/EdXposed 
参考