ASM实战:Hook图片加载框架 预警大图 非法URL

想找出你所有的烦恼与不开心

作者 Moonshot 日期 2018-08-28
ASM实战:Hook图片加载框架 预警大图 非法URL

ASM实战:Hook图片加载框架 预警大图 非法URL

背景

图片作为Android内存消耗的大头。开发过程需要谨慎的去使用。然后历史证明,人是不值得相信的,只有机器不会骗人。(我也不会)如何利用工具去检测加载过大(非必要的)的Bitmap? 且使用CDN是优化静态图片资源的常用的手段,如何去预警未使用cdn的网络图片?

目标

  • 定位 加载的Bitmap大于View大小的情况
  • 定位 未使用的cdn地址的 情况
  • 进阶目标:定位到下载的图片大小 超过View大小 一定比例 (内存上没问题、影响速度等)

面向切面编程: ASM 字节码操作框架

一般下面向对方编程的话就需要 找到所有每个加载图片的地方进行相应的改动,添加相应的代码。
然而这样成本太高。而且不太可能。
那么这个时候就要说下面向切面编程了。

面向切面编程(AOP)是一种非侵入式扩充对象、方法和函数行为的技术。通过 AOP 可以从“外部”去增加一些行为,进而合并既有行为或修改既有行为。

举个栗子:有这么一天两个大猪蹄子,准备和喜欢的妹纸表白。怎么办呢?

学习了面向对象的大猪蹄子,经过了一系列的学习。

women-is-a-book
懂得迎合妹纸喜欢,分别到妹纸喜欢的地方进行了表白。
express

而学习的面向切面编程的大猪蹄子, 心想:
那么多妹纸,我一个个去研究她们喜欢的地方、去她们喜欢的地方,那得有多麻烦,多累人了啊。
脑子灵机一动。

hair-adult
我只要在学校门口,等着她们一个个过来, 再一个个表白 不就可以了。

所以,利用AOP的思想。我们只要找一个入口,找到我们关心的类或者方法,通过ASM去插入我们想要的代码。

需要Hook的核心类

首先看一段Glide加载图片的代码

GlideApp.with(context)
.load(url)
.into(imageView);

简单,不就是花十分钟看下源码的事情吗?(然后一个纪元过去了)

得出了几个重点:

  • into的其实是一个ViewTarget
public abstract class ImageViewTarget<Z> extends ViewTarget<ImageView, Z>
implements Transition.ViewAdapter {

//...省略非必要的

private void setResourceInternal(@Nullable Z resource) {
// Order matters here. Set the resource first to make sure that the Drawable has a valid and
// non-null Callback before starting it.
setResource(resource);
maybeUpdateAnimatable(resource);
}

//将最终加载的数据设置再ImageView中
protected abstract void setResource(@Nullable Z resource);

//相应的Request
public Request getRequest() {
//...
return request;
}
}
  • load的Url 最终传入Request中。 是Glide支持的一种model
public final class SingleRequest<R> implements Request,
SizeReadyCallback,
ResourceCallback,
FactoryPools.Poolable {

@Nullable
private Object model;
}

所以只要在类ImageViewTarget#setResource中添加检测代码,这样每个调用的地方就可以执行到

Hook准备 :ImageViewTarget中嵌入的检测代码

public class GlideHelpter {

/**
* 检测是否有异常数据
*/
public static void detect(ImageViewTarget imageViewTarget, Object resource) {

if (imageViewTarget == null || resource == null) {
return;
}
ImageView imageView = (ImageView) imageViewTarget.getView();
if(imageView==null){
return;
}
//View大小 与 加载的 资源大小
int viewWidth = imageView.getWidth();
int viewHeight = imageView.getHeight();
int resWidth=0,resHeight=0;

if(resource instanceof Bitmap){
Bitmap bitmap = (Bitmap)resource;
resWidth = bitmap.getWidth();
resHeight = bitmap.getHeight();
}else if(resource instanceof Drawable){
Drawable drawable= (Drawable )resource;
resWidth = drawable.getIntrinsicWidth();
resHeight =drawable.getIntrinsicHeight();
}
//
DetectorInfo detectorInfo = new DetectorInfo();
if(resHeight>viewWidth&&resWidth>viewWidth){
//加载资源超过视图大小。需要预警
detectorInfo.isTooLarge = true;
detectorInfo.mImgWidth = resWidth;
detectorInfo.mImgHeight = resHeight;
detectorInfo.mViewHeight = viewHeight;
detectorInfo.mViewWidth = viewWidth;
}

//获取加载的url
String url = getUrlFromRequest(imageViewTarget.getRequest());
detectorInfo.mUrl = url;
detectorInfo.isLegalUrl = isLegalUrl(url);

if(detectorInfo.isTooLarge||!detectorInfo.isLegalUrl){
//打开预警界面
AlertListActivity.laungh(imageView.getContext(),detectorInfo);
}


}

/**
* 获取请求的Url
* @param request
* @return
*/
public static String getUrlFromRequest(Request request){
if (request != null && request instanceof SingleRequest) {
SingleRequest singleRequest = (SingleRequest) request;
//反射获取加载的url
Class requestClass = singleRequest.getClass();
try {
Field modelFiled = requestClass.getDeclaredField("model");
modelFiled.setAccessible(true);
Object model = modelFiled.get(singleRequest);
String url = null;
if (model instanceof String) {
url = (String) model;
} else if (model instanceof Uri) {
url = ((Uri)model).toString();
}
return url;

} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}

}

return null;
}

/**
* 是否是合法的Url
* @param url
* @return
*/
public static boolean isLegalUrl(String url){
if(TextUtils.isEmpty(url)){
return false;
}
Pattern pattern = Pattern.compile(Constant.URL_REGEX_CDN);
Matcher matcher = pattern.matcher(url);
if(matcher!=null&&matcher.find()){
return true;
}
return false;

}

}

主要ASM 代码

class GlideClassVisitor extends ClassVisitor {


GlideClassVisitor(ClassVisitor cv) {
super(Opcodes.ASM5, cv)
}

@Override
MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions);
methodVisitor = new AdviceAdapter(Opcodes.ASM5, methodVisitor, access, name, desc) {

private boolean isInject() {
//只Hook setResource方法
if (name.equals("setResource")) {
return true
}
return false
}

@Override
protected void onMethodExit(int i) {
if(isInject()){
//setResource 执行结束后调用 GlideHelper.detect
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ALOAD, 1);
mv.visitMethodInsn(INVOKESTATIC, "com/moonshot/library/imagedetector/GlideHelper", "detect", "(Lcom/bumptech/glide/request/target/ImageViewTarget;Ljava/lang/Object;)V", false);

}

}
}
return methodVisitor
}
}

TODO

  • 定位到下载的图片大小 超过View大小 一定比例 (内存上没问题、影响速度等)
  • 适配 Picasso、Fresco 其他两大图片加载框架