ASM实战:Hook图片加载框架 预警大图 非法URL 背景 图片作为Android内存消耗的大头。开发过程需要谨慎的去使用。然后历史证明,人是不值得相信的,只有机器不会骗人。(我也不会)如何利用工具去检测加载过大(非必要的)的Bitmap? 且使用CDN是优化静态图片资源的常用的手段,如何去预警未使用cdn的网络图片?
目标
定位 加载的Bitmap大于View大小的情况
定位 未使用的cdn地址的 情况
进阶目标:定位到下载的图片大小 超过View大小 一定比例 (内存上没问题、影响速度等)
面向切面编程: ASM 字节码操作框架 一般下面向对方编程的话就需要 找到所有每个加载图片的地方进行相应的改动,添加相应的代码。 然而这样成本太高。而且不太可能。 那么这个时候就要说下面向切面编程了。
面向切面编程(AOP)是一种非侵入式扩充对象、方法和函数行为的技术。通过 AOP 可以从“外部”去增加一些行为,进而合并既有行为或修改既有行为。
举个栗子:有这么一天两个大猪蹄子 ,准备和喜欢的妹纸们 表白。怎么办呢?
学习了面向对象的大猪蹄子,经过了一系列的学习。
懂得迎合妹纸喜欢,分别到妹纸喜欢的地方进行了表白。
而学习的面向切面编程的大猪蹄子, 心想: 那么多妹纸,我一个个去研究她们喜欢的地方、去她们喜欢的地方,那得有多麻烦,多累人了啊。 脑子灵机一动。
我只要在学校门口,等着她们一个个过来, 再一个个表白 不就可以了。
所以,利用AOP的思想。我们只要找一个入口,找到我们关心的类或者方法,通过ASM去插入我们想要的代码。
需要Hook的核心类 首先看一段Glide加载图片的代码
GlideApp.with(context) .load(url) .into(imageView);
简单,不就是花十分钟看下源码的事情吗?(然后一个纪元过去了)
得出了几个重点:
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 其他两大图片加载框架