码个蛋(codeegg) 第 989 次推文
作者:ZhouZhengyi
链接:https://juejin.im/post/5ea1c46851882573a25f3ec3
1.背景
最近看滴滴开源的Dokit框架中有一个大图监控的功能,可以对图片的文件大小和所占用的内存大小设置一个阈值,当图片超过该值的时候进行提示。这个功能对于我们在做APK体积压缩,内存管理的时候还是很有用的,比如当我们要从后台返回的连接中加载一张图片,这张图片的大小我们是不知道的,虽然现在大家都使用Glide等三方 图片加载框架,框架会自动对图片进行压缩,但是依然会出现压缩后所占内存超过预期的情况。这时候我们可以在开发、测试和预生产阶段使用大图监控来识别出那些超标的图片。
2.需求
在讨论如何做之前,我们必须明确我们要做什么。该大图监控框架我觉得应该实现以下功能:
能对图片的文件大小和所占用的内存大小设置阈值,超过其中之一则报警。
能够得到超标图片的详细信息,包括当前文件大小,所占用内存,图片分辨率,图片的略缩图,图片的加载地址,view的尺寸。
能够通过弹窗或者列表的方式查看当前超标的图片信息。
不论是本地加载图片还是网络加载图片都能够进行监控。
3.实现思路
要实现对图片文件大小和所占内存的监控,那么我们就得先知道图片的文件大小和加载该图片所耗费的内存。目前加载图片一般都使用第三方框架,所以可以对常用的图片加载框架进行Hook,这里主要对主流的四种图片加载框架进行Hook操作。
Glide
Picasso
Fresco
Image Loader
以从网络加载一张图片举例,当使用图片框架加载一张网络图片时,会使用OkHttp或者是HttpUrlconnection去下载该图片,这时候我们就能得到图片文件的大小。当图片框架将图片文件构造成Bitmap对象以后,我们又能得到其所占用的内存,这样我们就同时的得到了图片的文件大小和所占用的内存。那么这里我们也必须对OkHttp和HttpUrlconnection进行Hook。
既然要对三方框架进行Hook操作,那么我们如何进行Hook呢?在选择Hook的实现方案时,我对以下几种方案进行了调研。
反射+动态代理
ASM
AspectJ
ByteBuddy
首先反射+动态代理 只能在程序运行时进行,这样会影响效率,所以暂不考虑。其他三种方案都能够在编译期进行字节码插桩,ASM直接操纵字节码,阅读起来不那么友好。AspectJ以前用过,经常出一些莫名其妙的问题,体验不是很好。ByteBuddy 封装了ASM,据说效率很高,而且使用JAVA编写,代码可读性好,只是网上的资料太少了,大部分都是那么几篇文章再转发。所以这里最后选择了ASM实现。
有了ASM进行字节码插入,那什么时候将我们编写好的字节码插入到第三方框架中呢?
![9ca85412716302def59cc324d7217490.png](https://i-blog.csdnimg.cn/blog_migrate/40c6728bffa908c8e0ee87ae1d122f5e.jpeg)
我们从Apk打包流程图中可以看到,在生成dex文件之前,我们可以获取到本项目和第三方库的class文件,那么我们是否可以在此处将我们编写的字节码插入呢?答案是肯定的,我们在谷歌官网上找到这么一个界面-Transform Api。
![bfd1b77f9e9ba91a3723be1377339da4.png](https://i-blog.csdnimg.cn/blog_migrate/90251a5fdfe788de979fe484e4fd6bf8.jpeg)
网页上讲从Android Gralde插件1.5.0版本开始,添加了Transform API,来允许第三方插件在经过编译的class文件转换为dex文件之前对其进行操作。Gradle会按照以下顺序执行转换:JaCoCo->第三方插件->ProGuard。其中第三方插件的执行顺序与第三方插件添加顺序一致,并且第三方插件无法通过Api控制转换的执行顺序。
![20c1fdb9a82a8ec5647f05f2a3366d92.png](https://i-blog.csdnimg.cn/blog_migrate/898b46768987bd5d0da287ea19eca82d.jpeg)
有了Transform API +ASM我们就能够将我们自己编写的字节码插入到第三方框架的class文件中,从而在编译器完成插桩。
现在我们已经决定了用ASM在编译期通过Transform API进行插桩。那么具体该怎么实现呢?我们回想一下我们需要实现的功能,我们要对图片进行监控,为了监控我们要获取图片的数据,得到数据后发现超标图片我们要给与提示。这意味着有两部分功能,一部分负责通过插桩获取数据,另外一部分负责显示超标数据。于是整个大图监控项目我们采用Gradle自定义插件+Android Library的形式。
![dc1ee367f7e3ee426897a9c1f6a93928.png](https://i-blog.csdnimg.cn/blog_migrate/3d33990391fd03607438e4280ff27ace.jpeg)
largeimage-plugin:自定义Gradle插件,主要负责将我们编写的字节码插入到class文件。
largeimage:Andriod Library,主要负责将获取到的图片数据进行过滤,保存超标图片并且以弹窗或者列表的形式呈现给用户。
如何创建Gralde插件项目在这里就不多说了,网上有很多教程。网上的大多数教程会告诉你把插件项目名称改为buildSrc,这样做有很多好处,尤其是在代码编写阶段,可以采用以下这种形式进行测试
apply plugin:org.zzy.largeimage.LargeImageMonitorPlugin
不需要每次编写完成以后发布到maven仓库,插件项目修改以后,会直接在使用模块体现出来。
在这里笔者自建了本地maven库,并且为了名称上的统一,并没有将插件项目的名称改为buildSrc,这两种形式都可以,大家可以根据自身的情况来使用。
4.1 插件端
如果在编译期存在很多Transform那么肯定会对编译速度有一定的影响,那么有没有什么方式可以减少这种影响?有!并发+增量编译。
在这里推荐一个开源库Hunter,它能够帮助你快速的开发插件,并且支持并发+增量编译,笔者在这里就使用了该开源库。
![41b5cece4622d2f12f6735d31dad11b9.png](https://i-blog.csdnimg.cn/blog_migrate/ccb70721f98a0e38276d3c12a063277c.jpeg)
使用该开源库很简单,只需要在插件项目的build.gradle中引入依赖就行。
接下来为了创建我们的Transform并且将其注册到整个Transform队列中,我们需要创建一个类实现Plugin接口。
public class LargeImageMonitorPlugin implements Plugin {
@Override
public void apply(Project project) {
List taskNames = project.getGradle.getStartParameter.getTaskNames;
//如果是Release版本,则不进行字节码替换
for(String taskName : taskNames){
if(taskName.contains("Release")){
return;
}
}
AppExtension appExtension = (AppExtension)project.getProperties.get("android");
//创建自定义扩展
project.getExtensions.create("largeImageMonitor