通常来讲,想要修改第三方安卓应用的代码需要反编译。然而,反编译的操作很复杂,不仅要面对被混淆得面目全非的类、方法、字段名,还要考虑重新打包后会不会被应用的签名校验识别到。而现在,出现了一些工具,使得修改应用变得更加简单。
如果你的设备已经Root,可以直接安装LSPosed模块(使用KernelSU或者Magisk安装)。
如果你的设备没有Root,那也可以使用Shizuku和LSPatch来代替,这两个工具便是今天要介绍的主角。
一、安装
Shizuku: https://github.com/RikkaApps/Shizuku/releases
LSPatch: https://github.com/LSPosed/LSPatch/release
安装后激活Shizuku,给LSPatch授予Shizuku权限,然后自行选定一个目录用于存放LSPatch生成的安装包。
二、添加想要修改的应用
点击加号,点击“选择已安装的应用程序”,找到自己想要修改的应用。
修补模式有两种,这里讲述一下区别。如果想要自由选择应用使用哪些模块,随时开启或关闭,建议选择“本地模式”,此模式需要LSPatch保持运行;如果已经确定使用哪些模块,并且后期无需做修改,建议使用“集成模式”,该模式下应用的运行无需依赖LSPatch。当然,在开发过程中建议选择“本地模式”,毕竟修改起来更方便。
这里我选择自己写一个简单的应用,作为需要修改的应用。(应用名:XposedTestApp,包名:com.magicianguo.xposedtestapp)
布局文件 activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
tools:context=".MainActivity">
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
android:textColor="@color/black"
android:textSize="24sp" />
<com.magicianguo.xposedtestapp.MySurfaceView
android:layout_width="150dp"
android:layout_height="150dp"
android:layout_marginTop="20dp" />
</LinearLayout>
MainActivity.java
public class MainActivity extends AppCompatActivity {
private TextView mTvTitle;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTvTitle = findViewById(R.id.tv_title);
mTvTitle.setText("你好,世界");
// 窗口禁止截屏
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
}
}
MySurfaceView.java
public class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback {
private final Paint mPaintBg = new Paint();
private final Paint mPaintLine = new Paint();
private final Path mPath = new Path();
public MySurfaceView(Context context) {
super(context);
init();
}
public MySurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public MySurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mPaintBg.setColor(Color.rgb(0x33, 0x88, 0xFF));
mPaintBg.setStyle(Paint.Style.FILL);
mPaintLine.setColor(Color.WHITE);
mPaintLine.setStyle(Paint.Style.STROKE);
mPaintLine.setStrokeWidth(getResources().getDimension(R.dimen.sv_line_width));
getHolder().addCallback(this);
// 禁止截屏
setSecure(true);
}
@Override
public void surfaceCreated(@NonNull SurfaceHolder holder) {
}
@Override
public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
Canvas canvas = holder.lockCanvas();
// 绘制整个背景
canvas.drawRect(new Rect(0, 0, width, height), mPaintBg);
// 绘制“Z”
mPath.reset();
mPath.moveTo(width * 0.25F, width * 0.25F);
mPath.lineTo(width * 0.75F, width * 0.25F);
mPath.lineTo(width * 0.25F, width * 0.75F);
mPath.lineTo(width * 0.75F, width * 0.75F);
canvas.drawPath(mPath, mPaintLine);
holder.unlockCanvasAndPost(canvas);
}
@Override
public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
}
}
运行效果如图。此时点击截图会提示无法截图,因为MainActivity中添加了相关代码。
打开LSPatch,点击加号,选择已安装的应用程序,选择想要修改的应用,选择本地模式,点击开始修补、安装,替换原有的应用。(注意安装后的签名和原本的应用会不一致)
三、添加模块并使用
添加模块前,要思考自己的目的,是要对应用做哪些修改。
3.1 解除应用的截屏限制
举个例子,假如我想要在此应用中截图,也就意味着需要让MainActivity的以下方法失效:
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
查看Window.java源码,发现最终需要对setFlags方法的参数值进行修改。目的是让传入的多个FLAG中,只有FLAG_SECURE失效,其他的FLAG不影响。
public void addFlags(int flags) {
setFlags(flags, flags);
}
明确思路就开始执行。新建模块“XposedAllowScreenshot”,添加如下依赖:
dependencies {
compileOnly 'de.robv.android.xposed:api:82'
// 这个注释掉,否则代码提示会消失!
// compileOnly 'de.robv.android.xposed:api:82:sources'
}
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round">
<!--是否为xposed模块,置为true生效-->
<meta-data
android:name="xposedmodule"
android:value="true" />
<!--xposed模块的描述-->
<meta-data
android:name="xposeddescription"
android:value="@string/xposed_description" />
<!--xposed模块最低API版本-->
<meta-data
android:name="xposedminversion"
android:value="30" />
</application>
</manifest>
strings.xml
<resources>
<string name="app_name">XposedAllowScreenshot</string>
<string name="xposed_description">用于解除应用的截屏限制</string>
</resources>
新建 HookAllowScreenshot 类,实现 IXposedHookLoadPackage 接口,在 handleLoadPackage 方法中添加相关逻辑。
package com.magicianguo.xposedallowscreenshot;
import android.view.SurfaceView;
import android.view.Window;
import android.view.WindowManager;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;
public class HookAllowScreenshot implements IXposedHookLoadPackage {
private final XC_MethodHook mHookDeleteWindowFlagSecure = new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
Integer flags = (Integer) param.args[0];
// 只把FLAG_SECURE移除,其他不受影响
param.args[0] = flags & (~WindowManager.LayoutParams.FLAG_SECURE);
}
};
private final XC_MethodHook mHookDeleteSurfaceSecure = new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
param.args[0] = false;
}
};
@Override
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) {
XposedHelpers.findAndHookMethod(Window.class, "setFlags", Integer.TYPE, Integer.TYPE, mHookDeleteWindowFlagSecure);
XposedHelpers.findAndHookMethod(SurfaceView.class, "setSecure", Boolean.TYPE, mHookDeleteSurfaceSecure);
}
}
findAndHookMethod 方法的作用是“追踪”某个特定的方法。当应用执行到此方法之前/之后你都可以执行一些额外的逻辑,或者对传入的参数值、返回值进行修改。
XC_MethodHook 对象中的 beforeHookedMethod 会在指定方法执行前回调( 同理, afterHookedMethod 对应执行后),param.args[0]代表指定方法中传入的第一个参数,param.args[1]代表第二个,依此类推。
如果想要修改返回值,可以在 afterHookedMethod 调用 param.setResult(object) 方法。
因此,以上的代码主要作用是在 Window 的 setFlags 方法执行前,将传入的 flags 参数进行处理,移除里面的 FLAG_SECURE ;在 SurfaceView 的 setSecure 方法执行前把布尔参数 true 改成 false。
之后,需要在模块的assets文件夹中新建“xposed_init”文件,里面写上以上类的包名加类名:
com.magicianguo.xposedallowscreenshot.HookAllowScreenshot
于是模块写完了。点击运行使其运行到设备中,在LSPatch中能够看到此模块:
点击之前的应用,选择模块作用域,选择刚才添加的模块,重新运行应用。会发现应用可以正常截屏了。
3.2 修改Activity的布局
假如,我想要在应用的布局中加入3个按钮,并且修改标题文字,就像如下效果。
并且点击第一个按钮跳转到系统设置页,点击第二个按钮重新绘制图中的SurfaceView,点击第三个按钮跳转应用内的其他Activity。该如何实现?
添加模块的操作和前面的类似。这里我们新建“XposedChangeView”模块。
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round">
<meta-data
android:name="xposedmodule"
android:value="true" />
<meta-data
android:name="xposeddescription"
android:value="@string/xposed_description" />
<meta-data
android:name="xposedminversion"
android:value="30" />
<!-- 用于说明此模块适合作用于哪些应用(包名) -->
<meta-data
android:name="xposedscope"
android:resource="@array/xposed_scope" />
</application>
</manifest>
strings.xml
<resources>
<string name="app_name">XposedChangeView</string>
<string name="xposed_description">修改应用XposedTestApp的布局(对其他应用无效)</string>
</resources>
arrays.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<array name="xposed_scope">
<item>com.magicianguo.xposedtestapp</item>
</array>
</resources>
新建“HookChangeView”类:
package com.magicianguo.xposedchangeview;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.view.SurfaceView;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;
public class HookChangeView implements IXposedHookLoadPackage {
@Override
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) {
if (TextUtils.equals(lpparam.packageName, "com.magicianguo.xposedtestapp")) {
Class<?> clsMainActivity = XposedHelpers.findClass("com.magicianguo.xposedtestapp.MainActivity", lpparam.classLoader);
XposedHelpers.findAndHookMethod(clsMainActivity, "onCreate", Bundle.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
Activity activity = (Activity) param.thisObject;
Toast.makeText(activity, "APP已被Xposed修改!", Toast.LENGTH_SHORT).show();
TextView mTvTitle = (TextView) XposedHelpers.getObjectField(activity, "mTvTitle");
mTvTitle.setText("你好,Xposed框架");
// 找到mTvTitle的父布局
LinearLayout parent = (LinearLayout) mTvTitle.getParent();
SurfaceView surfaceView = null;
for (int i = 0; i < parent.getChildCount(); i++) {
View v = parent.getChildAt(i);
if (v instanceof SurfaceView) {
surfaceView = (SurfaceView) v;
break;
}
}
Class<?> clsWebViewActivity = XposedHelpers.findClass("com.magicianguo.xposedtestapp.WebViewActivity", lpparam.classLoader);
parent.addView(ViewTools.createView(activity, surfaceView, clsWebViewActivity), 0);
}
});
} else if (lpparam.packageName.contains(".android.webview")) {
// ignore
} else {
new Handler(Looper.getMainLooper()).postDelayed(() -> System.exit(-1), 100L);
throw new RuntimeException("请不要将此插件集成在包名“com.magicianguo.xposedtestapp”以外的应用!packageName: "+lpparam.packageName);
}
}
}
以上代码中,我们根据类名找到了MainActivity的class对象,并且“追踪”它的 onCreate 方法。使用param.thisObject 获取到 MainActivity 对象,然后就可以逐渐找到 其包含的View对象,然后添加View。(对于一个没有源代码的应用,可以使用jadx查看安装包,查看里面的类名和资源文件)
ViewTools.java
package com.magicianguo.xposedchangeview;
import android.app.Activity;
import android.content.Intent;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.provider.Settings;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;
import org.jetbrains.annotations.Nullable;
public class ViewTools {
public static View createView(Activity activity, @Nullable SurfaceView surfaceView, Class<?> clsWebViewActivity) {
// 获取dpi,便于使用dp为单位的尺寸
int dpi = activity.getResources().getConfiguration().densityDpi;
LinearLayout linearLayout = new LinearLayout(activity);
LinearLayout.LayoutParams linearlayoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
linearlayoutParams.bottomMargin = 20 * dpi / 160;
linearLayout.setLayoutParams(linearlayoutParams);
linearLayout.setOrientation(LinearLayout.VERTICAL);
// 使用dp为单位
int padding = 10 * dpi / 160;
linearLayout.setPadding(padding, padding, padding, padding);
linearLayout.setBackgroundColor(Color.rgb(0xEB, 0xA8, 0xFF));
Button btn1 = new Button(activity);
btn1.setText("打开设置页");
btn1.setOnClickListener(v -> {
Intent intent = new Intent(Settings.ACTION_SETTINGS);
activity.startActivity(intent);
});
linearLayout.addView(btn1);
Button btn2 = new Button(activity);
btn2.setText("重新绘制SurfaceView");
btn2.setAllCaps(false);
btn2.setOnClickListener(v -> {
if (surfaceView != null) {
int width = surfaceView.getMeasuredWidth();
int height = surfaceView.getMeasuredHeight();
SurfaceHolder holder = surfaceView.getHolder();
Canvas canvas = holder.lockCanvas();
Paint paintBg = new Paint();
paintBg.setColor(Color.rgb(0x1D, 0xED, 0x99));
paintBg.setStyle(Paint.Style.FILL);
canvas.drawRect(new Rect(0, 0, width, height), paintBg);
// 绘制“A”
Path path = new Path();
path.moveTo(width * 0.5F, height * 0.2F);
path.lineTo(width * 0.3F, height * 0.8F);
path.moveTo(width * 0.5F, height * 0.2F);
path.lineTo(width * 0.7F, height * 0.8F);
path.moveTo(width * 0.4F, height * 0.5F);
path.lineTo(width * 0.6F, height * 0.5F);
Paint paintLine = new Paint();
paintLine.setColor(Color.WHITE);
paintLine.setStyle(Paint.Style.STROKE);
paintLine.setStrokeWidth((5 * dpi) / 160F);
canvas.drawPath(path, paintLine);
holder.unlockCanvasAndPost(canvas);
}
});
linearLayout.addView(btn2);
Button btn3 = new Button(activity);
btn3.setText("打开WebViewActivity");
btn3.setAllCaps(false);
btn3.setOnClickListener(v -> {
Intent intent = new Intent(activity, clsWebViewActivity);
activity.startActivity(intent);
});
linearLayout.addView(btn3);
return linearLayout;
}
}
安装模块并使其生效,效果如下:
四、总结
使用LSPatch和Shizuku可以对应用注入代码,修改方法的参数或返回值,但是它需要对应用重新打包,重新打包后,签名会发生改变。虽然它提供了跳过签名校验的选项,但是部分应用(例如MT管理器)仍然能够检测到应用被修改,导致应用无法正常运行。因此最靠谱的方案还是Root之后使用LSPosed模块,该方案无需对应用重新打包,可以直接将模块作用在应用上。
源代码: GitHub - MagicianGuo/Android-XposedTest: Xposed模块,实现修改应用内布局、解除应用截图限制等功能。