Epic是一个框架,可实现基于ART虚拟机的hook方案,最近在网上看到不少的博客介绍用其进行大图检测,于是便想尝试一下。结果发现很多博客上的方案实施起来或多或少有些问题,导致不能正常运行。本文记录自己的操作步骤。
我们来看Epic在github的说明, 其readme中写的是添加入下依赖
dependencies {
compile 'com.github.tiann:epic:0.11.2'
}
直接使用示例
DexposedBridge.hookAllConstructors(Thread.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
Thread thread = (Thread) param.thisObject;
Class<?> clazz = thread.getClass();
if (clazz != Thread.class) {
Log.d(TAG, "found class extend Thread:" + clazz);
DexposedBridge.findAndHookMethod(clazz, "run", new ThreadMethodHook());
}
Log.d(TAG, "Thread: " + thread.getName() + " class:" + thread.getClass() + " is created.");
}
});
DexposedBridge.findAndHookMethod(Thread.class, "run", new ThreadMethodHook());
按照官方的说明,我们来操作发现 DexposedBridge这个类都找不到。去论坛里面找,发现也有人碰到这个问题,作者应该是很久没有维护了。
于是我直接将代码库下载下来,将其中的
library目录下的代码作为一个子module添加到项目中。记录一下自己的操作步骤:
操作
1. 集成library目录
将Epic工程中的library目录下的代码作为一个子module添加到项目中, 这里需要注意以下几点:
- 其
build.gradle中的部份代码可以注释掉(这是用来打包上传aar的):
apply plugin: 'com.github.panpf.bintray-publish'
publish {
userOrg = 'twsxtd'//
groupId = 'me.weishu'
artifactId = 'epic'
publishVersion = '0.11.1'
desc = 'Android Java AOP Method Hook (Dexposed on ART)'
website = 'https://github.com/tiann/epic'
}
- 需要配置NDk的环境,我这边是之前已经配置好了,所以这一步省了
Offset.java中的Build.VERSION_CODES.S编译报错,可直接将其改成 31. 需要注意Epic官方提到不支持android 12, 此处改成31是为了编译通过。
2.使用
直接在Application的onCreate方法中添加如下代码就可以了。
DexposedBridge.findAndHookMethod(ImageView.class, "setImageBitmap", Bitmap.class, new ImageHook());
错误方案
网上的不少博客上是这样使用
DexposedBridge.hookAllConstructors(ImageView.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
DexposedBridge.findAndHookMethod(ImageView.class, "setImageBitmap", Bitmap.class,
new ImageHook());
}
})
其中的ImageHook实例是我们具体的hook处理策略,可以先放一下,后面会把代码贴上。这个方案首先会hook掉ImageView的构造方法,它会造成 afterHookedMethod方法回调多次,然后下面的代码也会执行多次。
DexposedBridge.findAndHookMethod(ImageView.class, "setImageBitmap", Bitmap.class,new ImageHook());
所带来的影响就是,当我们调用一次ImageView的setImageBitmap方法时,ImageHook中的处理方法也就会执行多次。
其根本原因在于,我们虽然在xml中写了一个ImageView, 但其在初始化时,会先调用
public ImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)
然后该方法又会去调用
public ImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
int defStyleRes)
所以用 DexposedBridge.hookAllConstructors时,它会回调多次。
3. 业务调用
在测试的页面TestActivity.java的布局文件中添加一个ImageView
<ImageView
android:id="@+id/iv_test"
android:layout_width="120dp"
android:layout_height="120dp"
/>
为其设置bitmap
binding.ivTest.setImageBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.splash_bg));
测试的时候会发现当我们调用了setImageBitmap时,对应的hook方法便会执行。
ImageHook类的源码
通过比较bitmap和ImageView的尺寸来判断图片是否合规。这个是直接用的网上的方案
public class ImageHook extends XC_MethodHook {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
ImageView imageView = (ImageView)param.thisObject;
checkBitmap(imageView, imageView.getDrawable());
}
private void checkBitmap(ImageView imageView, Drawable drawable) {
if (drawable instanceof BitmapDrawable) {
final Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
if (bitmap != null) {
int width = imageView.getWidth();
int height = imageView.getHeight();
if (width > 0 && height > 0) {
if (bitmap.getWidth()>= width * 2 && bitmap.getHeight() >= height * 2) {
warn(bitmap.getWidth(), bitmap.getHeight(), width, height, new RuntimeException("Bitmap size too large"));
}
//宽高为0
} else {
final Throwable exception = new RuntimeException("Bitmap size too large");
imageView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
int width = imageView.getWidth();
int height = imageView.getHeight();
if (width > 0 && height > 0) {
if (bitmap.getWidth()>= width * 2 && bitmap.getHeight() >= height * 2) {
warn(bitmap.getWidth(), bitmap.getHeight(), width, height, exception);
}
imageView.getViewTreeObserver().removeOnPreDrawListener(this);
}
return true;
}
});
}
}
}
}
private static void warn(int bitmapWidth, int bitmapHeight, int viewWidth, int viewHeight, Throwable t) {
String warnInfo = "Bitmap size too large: real size: ( " + bitmapWidth + " * " + bitmapHeight
+ "), desire size:" + viewWidth + " x " + viewHeight + "\n call stackTrace:" + formatStackTrace(t);
Log.e("ImageHook", warnInfo);
}
private static String formatStackTrace(Throwable t) {
StringBuffer sbf = new StringBuffer();
for (StackTraceElement e: t.getStackTrace()) {
sbf.append(e.toString() + "\n");
}
return sbf.toString();
}
}
其它
- Epic是基于 dexposed作了一些改动。 dexposed 只支持Dalvik虚拟机, 也就是5.0以下的android系统。Epic支持android 5.0 - 11。
- 这种方案也可以用Frida,aspectjx去写。其实我们在用图片加载器的时候,比如glide就已经会去根据ImageView容器的大小decode出合适的bitmap, 所以这种方案其实收益还是有限的。