1肝断、背景
最近看滴滴開源的Dokit框架中有一個(gè)大圖監(jiān)控的功能,可以對(duì)圖片的文件大小和所占用的內(nèi)存大小設(shè)置一個(gè)閾值驰凛,當(dāng)圖片超過該值的時(shí)候進(jìn)行提示孝情。
這個(gè)功能對(duì)于我們?cè)谧鯝PK體積壓縮,內(nèi)存管理的時(shí)候還是很有用的,比如當(dāng)我們要從后臺(tái)返回的連接中加載一張圖片凳鬓,這張圖片的大小我們是不知道的锥忿,雖然現(xiàn)在大家都使用Glide等三方 圖片加載框架,框架會(huì)自動(dòng)對(duì)圖片進(jìn)行壓縮羔挡,但是依然會(huì)出現(xiàn)壓縮后所占內(nèi)存超過預(yù)期的情況洁奈。
這時(shí)候我們可以在開發(fā)、測試和預(yù)生產(chǎn)階段使用大圖監(jiān)控來識(shí)別出那些超標(biāo)的圖片绞灼。
2利术、需求
在討論如何做之前,我們必須明確我們要做什么低矮。該大圖監(jiān)控框架我覺得應(yīng)該實(shí)現(xiàn)以下功能:
能對(duì)圖片的文件大小和所占用的內(nèi)存大小設(shè)置閾值印叁,超過其中之一則報(bào)警。
能夠得到超標(biāo)圖片的詳細(xì)信息军掂,包括當(dāng)前文件大小轮蜕,所占用內(nèi)存,圖片分辨率蝗锥,圖片的略縮圖跃洛,圖片的加載地址,view的尺寸终议。
能夠通過彈窗或者列表的方式查看當(dāng)前超標(biāo)的圖片信息汇竭。
不論是本地加載圖片還是網(wǎng)絡(luò)加載圖片都能夠進(jìn)行監(jiān)控。
3穴张、實(shí)現(xiàn)思路
要實(shí)現(xiàn)對(duì)圖片文件大小和所占內(nèi)存的監(jiān)控细燎,那么我們就得先知道圖片的文件大小和加載該圖片所耗費(fèi)的內(nèi)存。
目前加載圖片一般都使用第三方框架皂甘,所以可以對(duì)常用的圖片加載框架進(jìn)行Hook,這里主要對(duì)主流的四種圖片加載框架進(jìn)行Hook操作玻驻。
Glide
Picasso
Fresco
Image Loader
以從網(wǎng)絡(luò)加載一張圖片舉例,當(dāng)使用圖片框架加載一張網(wǎng)絡(luò)圖片時(shí)叮贩,會(huì)使用OkHttp或者是HttpUrlconnection去下載該圖片,這時(shí)候我們就能得到圖片文件的大小击狮。當(dāng)圖片框架將圖片文件構(gòu)造成Bitmap對(duì)象以后,我們又能得到其所占用的內(nèi)存益老,這樣我們就同時(shí)的得到了圖片的文件大小和所占用的內(nèi)存彪蓬。那么這里我們也必須對(duì)OkHttp和HttpUrlconnection進(jìn)行Hook。
既然要對(duì)三方框架進(jìn)行Hook操作捺萌,那么我們?nèi)绾芜M(jìn)行Hook呢档冬?在選擇Hook的實(shí)現(xiàn)方案時(shí),我對(duì)以下幾種方案進(jìn)行了調(diào)研桃纯。
反射+動(dòng)態(tài)代理
ASM
AspectJ
ByteBuddy
首先反射+動(dòng)態(tài)代理 只能在程序運(yùn)行時(shí)進(jìn)行酷誓,這樣會(huì)影響效率,所以暫不考慮态坦。
其他三種方案都能夠在編譯期進(jìn)行字節(jié)碼插樁盐数,ASM直接操縱字節(jié)碼,閱讀起來不那么友好伞梯。
AspectJ以前用過玫氢,經(jīng)常出一些莫名其妙的問題帚屉,體驗(yàn)不是很好。ByteBuddy 封裝了ASM漾峡,據(jù)說效率很高攻旦,而且使用JAVA編寫,代碼可讀性好生逸,只是網(wǎng)上的資料太少了牢屋,大部分都是那么幾篇文章再轉(zhuǎn)發(fā)。
所以這里最后選擇了ASM實(shí)現(xiàn)槽袄。
有了ASM進(jìn)行字節(jié)碼插入烙无,那什么時(shí)候?qū)⑽覀兙帉懞玫淖止?jié)碼插入到第三方框架中呢?
我們從Apk打包流程圖中可以看到掰伸,在生成dex文件之前皱炉,我們可以獲取到本項(xiàng)目和第三方庫的class文件怀估,那么我們是否可以在此處將我們編寫的字節(jié)碼插入呢狮鸭?
答案是肯定的,我們?cè)诠雀韫倬W(wǎng)上找到這么一個(gè)界面-Transform Api
網(wǎng)頁上講從Android Gralde插件1.5.0版本開始多搀,添加了Transform API歧蕉,來允許第三方插件在經(jīng)過編譯的class文件轉(zhuǎn)換為dex文件之前對(duì)其進(jìn)行操作。
Gradle會(huì)按照以下順序執(zhí)行轉(zhuǎn)換:
JaCoCo->第三方插件->ProGuard
其中第三方插件的執(zhí)行順序與第三方插件添加順序一致康铭,并且第三方插件無法通過Api控制轉(zhuǎn)換的執(zhí)行順序惯退。
有了Transform API +ASM我們就能夠?qū)⑽覀冏约壕帉懙淖止?jié)碼插入到第三方框架的class文件中,從而在編譯器完成插樁从藤。
4催跪、具體實(shí)現(xiàn)
現(xiàn)在我們已經(jīng)決定了用ASM在編譯期通過Transform API進(jìn)行插樁。
那么具體該怎么實(shí)現(xiàn)呢夷野?
我們回想一下我們需要實(shí)現(xiàn)的功能懊蒸,我們要對(duì)圖片進(jìn)行監(jiān)控,為了監(jiān)控我們要獲取圖片的數(shù)據(jù)悯搔,得到數(shù)據(jù)后發(fā)現(xiàn)超標(biāo)圖片我們要給與提示骑丸。這意味著有兩部分功能,一部分負(fù)責(zé)通過插樁獲取數(shù)據(jù)妒貌,另外一部分負(fù)責(zé)顯示超標(biāo)數(shù)據(jù)通危。于是整個(gè)大圖監(jiān)控項(xiàng)目我們采用Gradle自定義插件+Android Library的形式。
largeimage-plugin:自定義Gradle插件灌曙,主要負(fù)責(zé)將我們編寫的字節(jié)碼插入到class文件菊碟。
largeimage:Andriod Library,主要負(fù)責(zé)將獲取到的圖片數(shù)據(jù)進(jìn)行過濾在刺,保存超標(biāo)圖片并且以彈窗或者列表的形式呈現(xiàn)給用戶逆害。
如何創(chuàng)建Gralde插件項(xiàng)目在這里就不多說了藏古,網(wǎng)上有很多教程。
網(wǎng)上的大多數(shù)教程會(huì)告訴你把插件項(xiàng)目名稱改為buildSrc忍燥,這樣做有很多好處拧晕,尤其是在代碼編寫階段,可以采用以下這種形式進(jìn)行測試
apply plugin:org.zzy.largeimage.LargeImageMonitorPlugin
不需要每次編寫完成以后發(fā)布到maven倉庫梅垄,插件項(xiàng)目修改以后厂捞,會(huì)直接在使用模塊體現(xiàn)出來。
在這里筆者自建了本地maven庫队丝,并且為了名稱上的統(tǒng)一靡馁,并沒有將插件項(xiàng)目的名稱改為buildSrc,這兩種形式都可以机久,大家可以根據(jù)自身的情況來使用臭墨。
4.1 插件端
如果在編譯期存在很多Transform那么肯定會(huì)對(duì)編譯速度有一定的影響,那么有沒有什么方式可以減少這種影響膘盖?有胧弛!并發(fā)+增量編譯。
在這里推薦一個(gè)開源庫Hunter,它能夠幫助你快速的開發(fā)插件侠畔,并且支持并發(fā)+增量編譯结缚,筆者在這里就使用了該開源庫
使用該開源庫很簡單,只需要在插件項(xiàng)目的build.gradle中引入依賴就行软棺。
接下來為了創(chuàng)建我們的Transform并且將其注冊(cè)到整個(gè)Transform隊(duì)列中红竭,我們需要?jiǎng)?chuàng)建一個(gè)類實(shí)現(xiàn)Plugin接口。
public class LargeImageMonitorPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
List<String> taskNames = project.getGradle().getStartParameter().getTaskNames();
//如果是Release版本喘落,則不進(jìn)行字節(jié)碼替換
for(String taskName : taskNames){
if(taskName.contains("Release")){
return;
}
}
AppExtension appExtension = (AppExtension)project.getProperties().get("android");
//創(chuàng)建自定義擴(kuò)展
project.getExtensions().create("largeImageMonitor",LargeImageExtension.class);
project.afterEvaluate(new Action<Project>() {
@Override
public void execute(Project project) {
LargeImageExtension extension = project.getExtensions().getByType(LargeImageExtension.class);
Config.getInstance().init(extension);
}
});
//將自定義Transform添加到編譯流程中
appExtension.registerTransform(new LargeImageTransform(project), Collections.EMPTY_LIST);
//添加OkHttp
appExtension.registerTransform(new OkHttpTransform(project),Collections.EMPTY_LIST);
//添加UrlConnection
appExtension.registerTransform(new UrlConnectionTransform(project),Collections.EMPTY_LIST);
}
}
該類主要做了三件事:
判斷當(dāng)前是否是Release變體茵宪,如果是的話就不進(jìn)行字節(jié)碼插樁。原因很簡單瘦棋,對(duì)超標(biāo)圖片的監(jiān)控盡量在開發(fā)和測試階段處理完稀火,不要帶到線上。
獲取自定義擴(kuò)展兽狭,比如我需要增加一個(gè)插樁開關(guān)標(biāo)識(shí)憾股,來控制是否進(jìn)行字節(jié)碼增強(qiáng)。
將自定義Transform進(jìn)行注冊(cè)箕慧。
在代碼中可以看見服球,我們注冊(cè)了三個(gè)自定義Transform,因?yàn)槲覀兺瑫r(shí)要對(duì)圖片加載框架和網(wǎng)絡(luò)請(qǐng)求庫進(jìn)行插樁颠焦。
LargeImageTransform:主要負(fù)責(zé)對(duì)Glide,Picasso,Fresco,ImageLoader進(jìn)行字節(jié)碼操作斩熊。
OkHttpTransform:主要負(fù)責(zé)對(duì)OkHttp進(jìn)行字節(jié)碼操作。
UrlConnectionTransform:主要負(fù)責(zé)對(duì)UrlConnection進(jìn)行字節(jié)碼操作伐庭。
4.1.1 Hook圖片加載庫
由于使用了Hunter框架粉渠,使得我們編寫Transform變得更加簡單分冈,不需要使用傳統(tǒng)的方式編寫Transform,我們主要來看關(guān)鍵代碼
public class LargeImageClassAdapter extends ClassVisitor {
private static final String IMAGELOADER_METHOD_NAME_DESC = "(Ljava/lang/String;Lcom/nostra13/universalimageloader/core/imageaware/ImageAware;Lcom/nostra13/universalimageloader/core/DisplayImageOptions;Lcom/nostra13/universalimageloader/core/assist/ImageSize;Lcom/nostra13/universalimageloader/core/listener/ImageLoadingListener;Lcom/nostra13/universalimageloader/core/listener/ImageLoadingProgressListener;)V";
/**
* 當(dāng)前類名
*/
private String className;
public LargeImageClassAdapter(ClassVisitor classWriter) {
super(Opcodes.ASM5, classWriter);
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
this.className = name;
}
@Override
public MethodVisitor visitMethod(int access, String methodName, String desc, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, methodName, desc, signature, exceptions);
//如果插件開關(guān)關(guān)閉霸株,則不插入字節(jié)碼
if(!Config.getInstance().largeImagePluginSwitch()) {
return mv;
}
// TODO: 2020/4/2 這里考慮做版本兼容
//對(duì)Glide4.11版本的SingleRequest類的構(gòu)造方法進(jìn)行字節(jié)碼修改
if(className.equals("com/bumptech/glide/request/SingleRequest") && methodName.equals("<init>") && desc!=null){
return mv == null ? null : new GlideMethodAdapter(mv,access,methodName,desc);
}
//對(duì)picasso的Request類的構(gòu)造方法進(jìn)行字節(jié)碼修改
if(className.equals("com/squareup/picasso/Request") && methodName.equals("<init>") && desc!=null){
return mv == null ? null : new PicassoMethodAdapter(mv,access,methodName,desc);
}
//對(duì)Fresco的ImageRequest類的構(gòu)造方法進(jìn)行字節(jié)碼修改
if(className.equals("com/facebook/imagepipeline/request/ImageRequest") && methodName.equals("<init>") && desc!=null){
return mv == null ? null : new FrescoMethodAdapter(mv,access,methodName,desc);
}
//對(duì)ImageLoader的ImageLoader類的displayImage方法進(jìn)行字節(jié)碼修改
if(className.equals("com/nostra13/universalimageloader/core/ImageLoader") && methodName.equals("displayImage") && desc.equals(IMAGELOADER_METHOD_NAME_DESC)){
return mv == null ? null : new ImageLoaderMethodAdapter(mv,access,methodName,desc);
}
return mv;
}
}
從繼承類的名字來看雕沉,這是一個(gè)類的訪問者,我們項(xiàng)目和第三方庫中的類都會(huì)經(jīng)過這去件。
我們?cè)趘isit方法中記錄下當(dāng)前經(jīng)過的類的名字坡椒。
并且在visitMethod方法中判斷當(dāng)前訪問的是否是某個(gè)類的某個(gè)方法,如果當(dāng)前訪問的方法是我們需要hook的方法尤溜,那么我們就執(zhí)行我們的字節(jié)碼插樁操作。
那么問題來了丈攒,我們?nèi)绾沃牢覀円猦ook哪個(gè)類的哪個(gè)方法呢授霸?
這就需要我們?nèi)ラ喿x需要hook框架的源碼了巡验。
在visitMethod方法中我們打算對(duì)Glide,Picasso,Fresco,ImageLoader四大圖片加載框架進(jìn)行hook。
那么我們就先需要知道這四大框架的Hook點(diǎn)在哪绝葡。那么如何尋找Hook點(diǎn)呢深碱?
雖然滴滴的Dokit項(xiàng)目中已經(jīng)給出了Hook點(diǎn)腹鹉,但是抱著學(xué)習(xí)的態(tài)度藏畅,我們可以試圖的分析一下,如何去尋找Hook點(diǎn)功咒?
我們對(duì)圖片加載框架進(jìn)行Hook,必須要滿足以下幾點(diǎn):
1.該Hook點(diǎn)是流程執(zhí)行的必經(jīng)之路愉阎。
2.在進(jìn)行Hook以后,我們能獲取到我們想要的數(shù)據(jù)力奋。
3.進(jìn)行Hook以后榜旦,不能影響正常的使用。
在經(jīng)過對(duì)四大圖片加載框架源碼的大致分析以后景殷,我發(fā)現(xiàn)大部分框架都在成功加載圖片后會(huì)對(duì)接口進(jìn)行回調(diào)溅呢,用來通知上層,圖片加載成功猿挚。那么我們是否有可能把圖片加載成功后回調(diào)的接口替換成我們的咐旧?
或者增加一個(gè)我們自定義的接口進(jìn)去,讓圖片加載成功以后也回調(diào)我們的接口绩蜻,這樣我們就能獲取到圖片的數(shù)據(jù)铣墨。
以Glide框架舉例,Glide在成功加載完圖片以后會(huì)在SingleRequest類的onResourceReady方法中對(duì)RequestListener接口進(jìn)行遍歷回調(diào)办绝。
private void onResourceReady(Resource<R> resource, R result, DataSource dataSource) {
...
try {
boolean anyListenerHandledUpdatingTarget = false;
if (requestListeners != null) {
for (RequestListener<R> listener : requestListeners) {
anyListenerHandledUpdatingTarget |=
listener.onResourceReady(result, model, target, dataSource, isFirstResource);
}
}
anyListenerHandledUpdatingTarget |=
targetListener != null
&& targetListener.onResourceReady(result, model, target, dataSource, isFirstResource);
if (!anyListenerHandledUpdatingTarget) {
Transition<? super R> animation = animationFactory.build(dataSource, isFirstResource);
target.onResourceReady(result, animation);
}
} finally {
isCallingCallbacks = false;
}
notifyLoadSuccess();
}
從這段代碼中我們可以知道幾點(diǎn):
requestListeners是一個(gè)List伊约。
回調(diào)方法onResourceReady中有我們所需要的所有數(shù)據(jù)腌逢。
這樣一來我們只需要在requestListeners中添加一個(gè)我們自定義的RequestListener。這樣在接口回調(diào)時(shí)上忍,我們也能獲取到圖片數(shù)據(jù)窍蓝。
那么在什么地方插入我們自定義的RequestListener呢吓笙?我們先來看requestListeners在SingleRequest中的定義。
@Nullable private final List<RequestListener<R>> requestListeners;
requestListeners被聲明成了final類型叁鉴,那么在編寫代碼的時(shí)候就只能夠賦值一次幌墓,如果是成員變量的話,則必須在構(gòu)造方法中進(jìn)行初始化
private SingleRequest(
Context context,
GlideContext glideContext,
@NonNull Object requestLock,
@Nullable Object model,
Class<R> transcodeClass,
BaseRequestOptions<?> requestOptions,
int overrideWidth,
int overrideHeight,
Priority priority,
Target<R> target,
@Nullable RequestListener<R> targetListener,
@Nullable List<RequestListener<R>> requestListeners,
RequestCoordinator requestCoordinator,
Engine engine,
TransitionFactory<? super R> animationFactory,
Executor callbackExecutor) {
this.requestLock = requestLock;
this.context = context;
this.glideContext = glideContext;
this.model = model;
this.transcodeClass = transcodeClass;
this.requestOptions = requestOptions;
this.overrideWidth = overrideWidth;
this.overrideHeight = overrideHeight;
this.priority = priority;
this.target = target;
this.targetListener = targetListener;
this.requestListeners = requestListeners;
this.requestCoordinator = requestCoordinator;
this.engine = engine;
this.animationFactory = animationFactory;
this.callbackExecutor = callbackExecutor;
status = Status.PENDING;
if (requestOrigin == null && glideContext.isLoggingRequestOriginsEnabled()) {
requestOrigin = new RuntimeException("Glide request origin trace");
}
}
如果我們?cè)赟ingleRequest的構(gòu)造方法中進(jìn)行Hook,把我們自定義的RequestListener添加進(jìn)requestListeners中,那么在圖片成功加載時(shí)舞肆,就會(huì)回調(diào)我們的方法椿胯,從而獲取到圖片數(shù)據(jù)。
這樣我們就找到了對(duì)Glide框架的Hook點(diǎn)种冬,也就有了visitMethod方法中下面這段代碼:
//對(duì)Glide4.11版本的SingleRequest類的構(gòu)造方法進(jìn)行字節(jié)碼修改
if(className.equals("com/bumptech/glide/request/SingleRequest") && methodName.equals("<init>") && desc!=null){
return mv == null ? null : new GlideMethodAdapter(mv,access,methodName,desc);
}
這段代碼就是用于判斷當(dāng)前訪問的是否是Glide框架中的SingleRequest類的構(gòu)造方法莺匠?如果是的話就進(jìn)行字節(jié)碼插入趣竣。
現(xiàn)在我們已經(jīng)有了Hook點(diǎn),我們要把自定義的RequestListener添加到requestListeners中单匣。那么現(xiàn)在有兩種選擇。
第一種鸡号,在SingleRequest類構(gòu)造方法進(jìn)入時(shí)鲸伴,得到傳入的參數(shù)requestListeners,將自定義RequestListener加入其中,接著再把參數(shù)requestListeners賦值給成員變量this.requestListeners捶朵。
第二種,讓參數(shù)requestListeners先賦值給成員變量this.requestListeners岖食,在方法退出之前拿到this.requestListeners析珊,將我們自定義的RequestListener加入其中忠寻。
兩種方法看似實(shí)現(xiàn)了相同的功能衷旅,但是字節(jié)碼卻不一樣。
第一種方法的語句與字節(jié)碼如下:
//語句
GlideHook.process(requestListeners);
//字節(jié)碼
mv.visitVarInsn(ALOAD, 12);
mv.visitMethodInsn(INVOKESTATIC, "org/zzy/lib/largeimage/aop/glide/GlideHook", "process", "(Ljava/util/List;)Ljava/util/List;", false);
第二種方法的語句與字節(jié)碼如下:
//語句
GlideHook.process(this.requestListeners);
//字節(jié)碼
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETFIELD, "com/bumptech/glide/request/SingleRequest", "requestListeners", "Ljava/util/List;");
mv.visitMethodInsn(INVOKESTATIC, "org/zzy/lib/largeimage/aop/glide/GlideHook", "process", "(Ljava/util/List;)Ljava/util/List;", false);
我們知道java在執(zhí)行一個(gè)方法的同時(shí)會(huì)創(chuàng)建一個(gè)棧幀,棧幀中包括局部變量表猪钮,操作數(shù)棧烤低,動(dòng)態(tài)鏈接,方法出口等腻要。其中局部變量表是在編譯期就已經(jīng)確定了,其索引是從0開始趟济,表示該對(duì)象的實(shí)例引用,你可以大體認(rèn)為就是this媳纬。
在第一種方法中茅糜,我們先是通過ALOAD指令將局部變量表中索引為12的引用型變量入棧(requestListeners)限匣,然后調(diào)用GlideHook的靜態(tài)方法process,將其傳入。
在第二種方法中峦筒,我們通過ALOAD指令將this入棧,然后訪問this對(duì)象的requestListeners字段峦失,將其傳入GlideHook的靜態(tài)方法process中。
從指令上來看隧魄,第一種方式的指令更少嘱么。
但是我們考慮一個(gè)問題辉川,第一種方式我們手動(dòng)的獲取了該方法局部變量表第12個(gè)索引的值府蛇。萬一哪一天Glide想在該構(gòu)造方法中增加或者刪除一個(gè)參數(shù)集索,那我們的代碼就不兼容了。
所以為了代碼的兼容性考慮,我們采用第二種方法务荆,起碼直接刪除一個(gè)成員變量的概率要小于對(duì)構(gòu)造方法入?yún)⒌男薷摹?/p>
在這里大家可以思考一下妆距,是否能直接在構(gòu)造方法中add我們的自定義RequestListener?
可以是可以函匕,但是如果下次要再增加一個(gè)自定義RequestListener娱据,我們又得在插件端修改字節(jié)碼指令结啼,太過于麻煩属铁,我們不如直接得到List,然后在GlideHook的process方法中add间学。
我們來看看具體的實(shí)現(xiàn)代碼:
public class GlideMethodAdapter extends AdviceAdapter {
/**
* 方法退出時(shí)
* 1.先拿到requestListeners
* 2.然后對(duì)其進(jìn)行修改
* GlideHook.process(requestListeners);
* 作者: ZhouZhengyi
* 創(chuàng)建時(shí)間: 2020/4/1 15:51
*/
@Override
protected void onMethodExit(int opcode) {
super.onMethodExit(opcode);
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETFIELD, "com/bumptech/glide/request/SingleRequest", "requestListeners", "Ljava/util/List;");
mv.visitMethodInsn(INVOKESTATIC, "org/zzy/lib/largeimage/aop/glide/GlideHook", "process", "(Ljava/util/List;)Ljava/util/List;", false);
}
}
onMethodExit表示在SingleRequest構(gòu)造方法退出前加入以下指令水泉。
這時(shí)候肯定有人會(huì)問了葡粒,字節(jié)碼指令這么麻煩我寫錯(cuò)了咋辦?
在這里推薦一款android studio插件ASM Bytecode Outline忿磅。安裝成功以后搓谆,用Java將代碼編寫完成屏轰,然后右鍵生成字節(jié)碼即可瘤睹。
例如我們可以創(chuàng)建一個(gè)測試類:
public class Test {
private List<RequestListener> requestListeners;
//模擬glide
private void init(){
GlideHook.process(requestListeners);
}
}
這樣我們就能得到我們想要的字節(jié)碼指令了,別忘了修改一下類的全限定名吊履。
該插件的詳細(xì)操作網(wǎng)上有很多教程居砖,這里就不多說了蕉世。
到此為止我們就成功將編寫好的字節(jié)碼插入到了Glide框架中查吊。
對(duì)其他三種圖片加載框架的Hook點(diǎn)尋找也是類似的思路逻卖,而且大部分也都是在某個(gè)類的構(gòu)造方法中進(jìn)行Hook灭返。
總結(jié)一下:
尋找到的Hook點(diǎn)可能不止一個(gè)怎静,大家根據(jù)自身情況進(jìn)行采用渣锦。
拿到Hook對(duì)象以后嫉称,要看看是否能得到我們想要的數(shù)據(jù)始藕,如果得不到需要重新尋找蒲稳。
構(gòu)造方法是一個(gè)好的Hook點(diǎn),因?yàn)樵谶@里一般都進(jìn)行初始化操作伍派。
在選擇Hook方式的時(shí)候一定要考慮到代碼兼容性問題。
在插入完字節(jié)碼以后剩胁,當(dāng)Glide執(zhí)行到SingleRequest的構(gòu)造方法時(shí)就會(huì)執(zhí)行我們插入的字節(jié)碼指令了诉植。在圖片成功加載后就會(huì)回調(diào)我們的自定義RequestListener,接著該怎么做昵观,我們后面再說晾腔,這部分的邏輯我們將它放到了largeimage 這個(gè)Library中。
4.1.2 Hook OkHttp
我們前面說到啊犬,當(dāng)我們使用圖片框架加載一張網(wǎng)絡(luò)圖片時(shí)灼擂,圖片框架會(huì)先從網(wǎng)絡(luò)將圖片下載,然后再加載觉至。以Glide為例剔应,Glide會(huì)將圖片下載存到本地,然后再把本地圖片讀入內(nèi)存構(gòu)建一個(gè)Resource,當(dāng)圖片加載成功的時(shí)候语御,就會(huì)回調(diào)我們自定義的監(jiān)聽器峻贮,但是這個(gè)時(shí)候我們只能獲取到圖片加載到內(nèi)存后的數(shù)據(jù),也就是說我們獲取不到圖片的文件大小应闯。
所以就考慮是否能再圖片下載成功后拿到圖片的文件大小呢纤控?
這就需要我們對(duì)網(wǎng)絡(luò)下載框架進(jìn)行Hook,每次得到Response時(shí)判斷Content-Type是否是image開頭碉纺,如果是的話我們就認(rèn)為本次請(qǐng)求的是圖片船万。
有了思路以后,我們就開始著手對(duì)OkHttp進(jìn)行Hook,OkHttp的Hook點(diǎn)很容易尋找骨田,一方面在于大家對(duì)OkHttp的源碼都比較熟悉耿导,另外一方面在于OkHttp的優(yōu)秀架構(gòu)。我們都知道OkHttp采用攔截鏈的方式來處理數(shù)據(jù)盛撑,并且作者預(yù)留了兩處可以添加攔截器的地方碎节,一處是應(yīng)用攔截器,一處是網(wǎng)絡(luò)攔截器抵卫。
只要我們?cè)谶@兩處添加我們自己的攔截器狮荔,那么請(qǐng)求和響應(yīng)數(shù)據(jù)都會(huì)經(jīng)過我們的攔截器胎撇。所以O(shè)kHttp的Hook點(diǎn)我們就放在OkHttpClient$Builder類的構(gòu)造方法中。
public class OkHttpClassAdapter extends ClassVisitor {
private String className;
public OkHttpClassAdapter(ClassVisitor cv) {
super(Opcodes.ASM5, cv);
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
this.className = name;
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions);
//如果插件開關(guān)關(guān)閉殖氏,則不插入字節(jié)碼
if(!Config.getInstance().largeImagePluginSwitch()) {
return methodVisitor;
}
if(className.equals("okhttp3/OkHttpClient$Builder") && name.equals("<init>") && desc.equals("()V")){
return methodVisitor == null ? null : new OkHttpMethodAdapter(methodVisitor,access,name,desc);
}
return methodVisitor;
}
}
而且這種攔截器的添加是全局性的晚树,以前你在項(xiàng)目中添加OkHttp的攔截器,只是你本項(xiàng)目的網(wǎng)絡(luò)請(qǐng)求會(huì)回調(diào)雅采。
但是通過這種方法添加的攔截器爵憎,本項(xiàng)目中和第三方庫中,只要使用了OkHttp框架都會(huì)添加相同的攔截器婚瓜。
說到這是不是想到了HttpDns宝鼓?
以前我們?yōu)榱朔乐笵NS劫持加快DNS解析速度,在OkHttp中通過自定義DNS的方式來實(shí)現(xiàn)HttpDns訪問巴刻,但是如果使用第三方圖片框架加載服務(wù)器上的圖片愚铡,還是走的53端口的UDP形式。
那么我們能不能順便把OkHttp中的Dns也Hook了胡陪?這樣就能全局添加我們自定義的Dns沥寥,實(shí)現(xiàn)整個(gè)項(xiàng)目都使用HttpDns來解析域名。
public class OkHttpMethodAdapter extends AdviceAdapter {
/**
* 方法退出時(shí)插入
* interceptors.addAll(LargeImage.getInstance().getOkHttpInterceptors());
* networkInterceptors.
* addAll(LargeImage.getInstance().getOkHttpNetworkInterceptors());
* dns = LargeImage.getInstance().getDns();
* 作者: ZhouZhengyi
* 創(chuàng)建時(shí)間: 2020/4/5 9:39
*/
@Override
protected void onMethodExit(int opcode) {
super.onMethodExit(opcode);
//添加應(yīng)用攔截器
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETFIELD, "okhttp3/OkHttpClient$Builder", "interceptors", "Ljava/util/List;");
mv.visitMethodInsn(INVOKESTATIC, "org/zzy/lib/largeimage/LargeImage", "getInstance", "()Lorg/zzy/lib/largeimage/LargeImage;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "org/zzy/lib/largeimage/LargeImage", "getOkHttpInterceptors", "()Ljava/util/List;", false);
mv.visitMethodInsn(INVOKEINTERFACE, "java/util/List", "addAll", "(Ljava/util/Collection;)Z", true);
mv.visitInsn(POP);
//添加網(wǎng)絡(luò)攔截器
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETFIELD, "okhttp3/OkHttpClient$Builder", "networkInterceptors", "Ljava/util/List;");
mv.visitMethodInsn(INVOKESTATIC, "org/zzy/lib/largeimage/LargeImage", "getInstance", "()Lorg/zzy/lib/largeimage/LargeImage;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "org/zzy/lib/largeimage/LargeImage", "getOkHttpNetworkInterceptors", "()Ljava/util/List;", false);
mv.visitMethodInsn(INVOKEINTERFACE, "java/util/List", "addAll", "(Ljava/util/Collection;)Z", true);
mv.visitInsn(POP);
//添加DNS
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESTATIC, "org/zzy/lib/largeimage/LargeImage", "getInstance", "()Lorg/zzy/lib/largeimage/LargeImage;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "org/zzy/lib/largeimage/LargeImage", "getDns", "()Lokhttp3/Dns;", false);
mv.visitFieldInsn(PUTFIELD, "okhttp3/OkHttpClient$Builder", "dns", "Lokhttp3/Dns;");
}
}
我們?cè)贠kHttpClient$Builder構(gòu)造方法退出之前柠座,將我們的攔截器和自定義dns插入邑雅。
同樣的,插件端只負(fù)責(zé)插入字節(jié)碼妈经,后續(xù)所有的邏輯都放在了Library中淮野。
4.1.3 Hook HttpUrlConnection
可能很多人會(huì)覺得,現(xiàn)在還有人用HttpUrlConnection嗎狂塘?還有必要對(duì)它進(jìn)行處理嗎录煤?雖然現(xiàn)在普遍使用OkHttp框架,但使用HttpUrlConnection的還很多荞胡,而且還得考慮兼容性不是嗎妈踊?像Glide框架使用的就是HttpUrlConnection請(qǐng)求網(wǎng)絡(luò),雖然Glide框架可以采用自定義ModelLoader的方式實(shí)現(xiàn)OkHttp請(qǐng)求網(wǎng)絡(luò)泪漂。
但是為了保險(xiǎn)起見廊营,我們統(tǒng)一進(jìn)行處理。那這里要怎么對(duì)HttpUrlConnection進(jìn)行Hook呢萝勤?HttpUrlConnection的源碼也沒看過呀露筒?
那我們能不能換一種思路,既然在前面我們已經(jīng)對(duì)OkHttp進(jìn)行了Hook敌卓,那么我們能不能將所有的HttpUrlConnection請(qǐng)求換成OkHttp來實(shí)現(xiàn)慎式?也就是將HttpUrlConnection請(qǐng)求導(dǎo)向OkHttp,這樣就可以在統(tǒng)一在OkHttp中對(duì)數(shù)據(jù)進(jìn)行處理。
那怎么才能將HttpUrlConnection換成OkHttp呢瘪吏?
我們以前在做Hook的時(shí)候癣防,通常的思路是,如果Hook的對(duì)象是接口掌眠,那么我們就使用動(dòng)態(tài)代理蕾盯,如果是類,那么我們就繼承它并且重寫其方法蓝丙。在這里我們也可以自定義一個(gè)類繼承HttpUrlConnection然后重寫它的方法级遭,方法里全部改用OkHttp來實(shí)現(xiàn)。那接下來的問題就是在什么地方將系統(tǒng)的HttpUrlConnection換成我們自定義的HttpUrlConnection渺尘。
HttpUrlConnection是一個(gè)抽象類挫鸽,不能直接用new來創(chuàng)建,要得到HttpUrlConnection對(duì)象沧烈,需要使用URL類的openConnection方法得到一個(gè)HttpURLConnection對(duì)象掠兄,那么我們就可以在所有調(diào)用openConnection方法的地方進(jìn)行Hook,將系統(tǒng)返回的HttpURLConnection對(duì)象替換成我們自定義的HttpURLConnection對(duì)象锌雀。
既然所有調(diào)用到openConnection方法的地方都要Hook,那么就沒用特定的類迅诬,所以這次我們不針對(duì)特定類腋逆。
public class UrlConnectionClassAdapter extends ClassVisitor {
/**
* 這個(gè)方法跟其他幾個(gè)methodAdapter不一樣
* 其他的methodAdapter是根據(jù)類名和方法名來進(jìn)行hook
* 也就是說當(dāng)訪問到某個(gè)類的某個(gè)方法時(shí)進(jìn)行
* 而這個(gè)方法是,所有的類和方法都有可能存在hook侈贷,
* 所以這里不做類和方法的判斷
* 作者: ZhouZhengyi
* 創(chuàng)建時(shí)間: 2020/4/5 17:25
*/
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions);
//如果插件開關(guān)關(guān)閉惩歉,則不插入字節(jié)碼
if (!Config.getInstance().largeImagePluginSwitch()) {
return methodVisitor;
}
return methodVisitor == null ? null : new UrlConnectionMethodAdapter(className, methodVisitor, access, name, desc);
}
}
URL類有兩個(gè)openConnection方法,都要進(jìn)行Hook俏蛮。
public class UrlConnectionMethodAdapter extends AdviceAdapter {
/**
* 這里復(fù)寫的方法與其他的methodAdapter也不同
* 其他的methodAdapter是在方法進(jìn)入或者退出時(shí)操作
* 而這個(gè)methodAdapter是根據(jù)指令比較的
* 這個(gè)方法的意思是當(dāng)方法被訪問時(shí)調(diào)用
* @param opcode 指令
* @param owner 操作的類
* @param name 方法名稱
* @param desc 方法描述 (參數(shù))返回值類型
* 作者: ZhouZhengyi
* 創(chuàng)建時(shí)間: 2020/4/5 17:29
*/
@Override
public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
//所有的類和方法撑蚌,只要存在調(diào)用openConnection方法的指令,就進(jìn)行hook
if(opcode == Opcodes.INVOKEVIRTUAL && owner.equals("java/net/URL")
&& name.equals("openConnection")&& desc.equals("()Ljava/net/URLConnection;")){
mv.visitMethodInsn(INVOKEVIRTUAL,"java/net/URL", "openConnection", "()Ljava/net/URLConnection;", false);
super.visitMethodInsn(INVOKESTATIC,"org/zzy/lib/largeimage/aop/urlconnection/UrlConnectionHook","process","(Ljava/net/URLConnection;)Ljava/net/URLConnection;",false);
}else if(opcode == Opcodes.INVOKEVIRTUAL && owner.equals("java/net/URL")
&& name.equals("openConnection")&& desc.equals("(Ljava/net/Proxy;)Ljava/net/URLConnection;")){
//public URLConnection openConnection(Proxy proxy)
mv.visitMethodInsn(INVOKEVIRTUAL,"java/net/URL", "openConnection", "(Ljava/net/Proxy;)Ljava/net/URLConnection;", false);
super.visitMethodInsn(INVOKESTATIC,"org/zzy/lib/largeimage/aop/urlconnection/UrlConnectionHook","process","(Ljava/net/URLConnection;)Ljava/net/URLConnection;",false);
}else{
super.visitMethodInsn(opcode, owner, name, desc, itf);
}
}
}
這樣我們就成功把由OkHttp實(shí)現(xiàn)的HttpURLConnection返回給使用者搏屑。
HttpUrlConnection字節(jié)碼插樁部分到這里就結(jié)束了争涌,剩下的邏輯也都在Library中。
4.2 Library端
Library端主要完成這么幾件事:
負(fù)責(zé)初始化并接收用戶的配置辣恋。
從框架的回調(diào)中得到所需的數(shù)據(jù)亮垫。
對(duì)超標(biāo)的圖片數(shù)據(jù)進(jìn)行保存。
對(duì)超標(biāo)的圖片進(jìn)行展示伟骨。
4.2.1 初始化與配置
LargeImage類負(fù)責(zé)初始化和接收用戶的配置饮潦,是用戶直接操作的類,該類被設(shè)置成了單例携狭,并且采用鏈?zhǔn)秸{(diào)用的方式接收用戶的配置继蜡。通過該類可以設(shè)置圖片的文件大小閾值,圖片所占內(nèi)存大小的閾值,OkHttp應(yīng)用攔截器的添加稀并,OkHttp網(wǎng)絡(luò)攔截器的添加等配置仅颇。
LargeImage.getInstance()
.install(this)//一定要調(diào)用該方法進(jìn)行初始化
.setFileSizeThreshold(400.0)//設(shè)置文件大小閾值單位為KB (可選)
.setMemorySizeThreshold(100)//設(shè)置內(nèi)存占用大小閾值單位為KB (可選)
.setLargeImageOpen(true)//是否開啟大圖監(jiān)控,默認(rèn)為開啟稻轨,如果false灵莲,則不會(huì)在大圖列表和彈窗顯示超標(biāo)圖片 (可選)
.addOkHttpInterceptor(new CustomGlobalInterceptor())//添加OKhttp自定義全局應(yīng)用監(jiān)聽器 (可選)
.addOkHttpNetworkInterceptor(new CustomGlobalNetworkInterceptor())//添加Okhttp值得你故意全局網(wǎng)絡(luò)監(jiān)聽器 (可選)
.setDns(new CustomHttpDns);//設(shè)置自定義的全局DNS,可以自己實(shí)現(xiàn)HttpDns (可選)
4.2.2 獲取數(shù)據(jù)
當(dāng)我們?cè)诓寮藢⒆止?jié)碼插入到框架以后殴俱,框架會(huì)自動(dòng)回調(diào)我們自定義的方法政冻,在這些方法中就可以獲取到圖片的數(shù)據(jù),所以關(guān)于這一塊沒什么好說的线欲,都比較簡單明场,無非就是獲取到數(shù)據(jù)以后調(diào)用相關(guān)類的方法保存數(shù)據(jù),并不做過多的業(yè)務(wù)處理李丰。
這里值得一說的是苦锨,在HttpUrlConnection進(jìn)行Hook時(shí),我們提到要自定義HttpUrlConnection并且使用OkHttp來實(shí)現(xiàn)趴泌,這部分的實(shí)現(xiàn)不用我們自己來完成舟舒,在OkHttp3.14版本之前有提供一個(gè)叫ObsoleteUrlFactory的類,已經(jīng)幫我們實(shí)現(xiàn)好了嗜憔,只是從3.14版本以后該類被去掉了秃励,我們只需要把這個(gè)類拷貝過來直接使用就行。
4.2.3 保存數(shù)據(jù)
獲取到圖片數(shù)據(jù)以后吉捶,我們就要進(jìn)行保存夺鲜,這部分的邏輯由LargeImageManager負(fù)責(zé),LargeImageManager類也被設(shè)計(jì)成了單例呐舔。既然是要對(duì)數(shù)據(jù)進(jìn)行保存币励,那么我們肯定是有選擇性的保存,也就是只保存超標(biāo)的圖片信息珊拼,沒有超標(biāo)的圖片食呻,我們就不管了。而保存的超標(biāo)信息是為了向用戶進(jìn)行報(bào)警杆麸。
在實(shí)現(xiàn)該類的時(shí)候遇到了這么幾個(gè)問題搁进,首先由于我們分別Hook了OkHttp和圖片框架,所以在加載一張網(wǎng)絡(luò)圖片的時(shí)候昔头,我們會(huì)先收到OkHttp的回調(diào)饼问,在這里我們可以得到圖片的文件大小信息,然后再收到圖片框架的回調(diào)揭斧,得到圖片所占用的內(nèi)存大小信息莱革。
我們前面提到我們需要保存超標(biāo)的圖片信息峻堰,而對(duì)超標(biāo)圖片的定義是文件大小超標(biāo)或者內(nèi)存占用超標(biāo),所以我們?cè)贠kHttp回調(diào)的時(shí)候是沒辦法知道內(nèi)存是否超標(biāo)的盅视,因?yàn)閳D片框架有可能會(huì)對(duì)圖片進(jìn)行壓縮捐名,那么我們?cè)贠kHttp回調(diào)時(shí)就不用判斷當(dāng)前圖片是否保存,而是一律保存下來闹击,將是否保存的判斷延遲到圖片框架回調(diào)時(shí)镶蹋。
在圖片框架回調(diào)時(shí),我們就能同時(shí)擁有文件大小和內(nèi)存占用的數(shù)據(jù)赏半,如果其中之一超標(biāo)我們則保存贺归,如果都不超標(biāo),我們?cè)賹?shù)據(jù)刪除断箫。
其次我們還遇到了這樣一個(gè)問題拂酣,當(dāng)我使用Glide框架加載一張網(wǎng)絡(luò)圖片時(shí),我們假設(shè)這張圖片文件大小超標(biāo)仲义,但是內(nèi)存不超標(biāo)婶熬,那么我們會(huì)記錄該圖片的所有信息。
但是在第二次啟動(dòng)APP時(shí)埃撵,由于Glide在磁盤中緩存了該圖片赵颅,就不會(huì)再次調(diào)用OkHttp去下載圖片,那么這時(shí)候我們只能收到圖片框架的回調(diào)暂刘,換句話說我們只能得到圖片所占用內(nèi)存的數(shù)據(jù)性含,如果這時(shí)候圖片內(nèi)存不超標(biāo),那么我們就會(huì)刪除此圖片的信息鸳惯,也就不會(huì)提示用戶。
為了解決這個(gè)問題叠萍,我們就必須在SD卡中保存超標(biāo)圖片的完整信息芝发,這樣就算圖片框架從緩存中加載圖片,我們也能得到圖片的文件大小信息苛谷。
我們應(yīng)該如何將超標(biāo)圖片的信息保存到本地呢辅鲸?用SharedPreferences?還是數(shù)據(jù)庫?因?yàn)槭褂脠鼍皶?huì)頻繁的增加腹殿,刪除和修改數(shù)據(jù)独悴,而SP每次都是全量寫入,也就是說SP在每次寫入數(shù)據(jù)之前都會(huì)把xml文件改名為備份文件锣尉,然后再從xml文件中讀取出數(shù)據(jù)與新增數(shù)據(jù)合并再寫入到新的xml文件中刻炒,如果執(zhí)行成功再將備份xml文件刪除,這樣效率太低了自沧。
至于數(shù)據(jù)庫的效率跟SP也差不了太多坟奥,而且還要防止突然間奔潰導(dǎo)致數(shù)據(jù)沒保存上的情況。
這就要求使用的組件具有實(shí)時(shí)寫入的能力,那么mmap內(nèi)存映射文件正好適合這種場景爱谁,通過mmap內(nèi)存映射文件晒喷,能夠提供一段可供隨時(shí)寫入的內(nèi)存塊,APP只管往里面寫數(shù)據(jù)访敌,由操作系統(tǒng)負(fù)責(zé)將內(nèi)存回寫到文件凉敲,而不必?fù)?dān)心crash導(dǎo)致數(shù)據(jù)丟失。
由微信開源的MMKV就是基于mmap內(nèi)存映射的key-value組件寺旺,它十分的高效爷抓,具有增量更新的能力。下面是微信團(tuán)隊(duì)對(duì)MMKV迅涮,SP废赞,SQlite的對(duì)比測試數(shù)據(jù)。
單進(jìn)程情況下叮姑,在華為 Mate 20 Pro 128G唉地,Android 10手機(jī)上,每組操作重復(fù) 1k 次传透,結(jié)果以ms為單位耘沼,可以看見MMKV的效率很高。
使用了MMKV朱盐,就解決了圖片框架從緩存加載數(shù)據(jù)時(shí)群嗤,得不到圖片文件大小的問題。但是另外一個(gè)問題出現(xiàn)了兵琳,使用MMKV以后狂秘,我們將超標(biāo)的圖片數(shù)據(jù)都保存到了本地,如果超標(biāo)圖片之后一直未使用躯肌,那么我們就要一直保存著嗎者春?
也就是說我們何時(shí)清理MMKV保存的數(shù)據(jù)?使用LRU算法清女?
也許可行钱烟,但是我這里使用了一個(gè)稍微簡單一點(diǎn)的實(shí)現(xiàn)方式,首先我們?cè)O(shè)置一個(gè)清理值嫡丙,達(dá)到該值就開始執(zhí)行清理操作拴袭,這里我將默認(rèn)值設(shè)置成了20,當(dāng)然這個(gè)值是可以通過我們提供的接口進(jìn)行修改的曙博。
在超標(biāo)圖片bean類中也增加一個(gè)記錄當(dāng)前圖片未使用次數(shù)的字段拥刻。然后程序每次啟動(dòng)時(shí)會(huì)對(duì)當(dāng)前啟動(dòng)次數(shù)加1,并且對(duì)MMKV中保存的超標(biāo)圖片未使用次數(shù)加1羊瘩,如果圖片被加載一次泰佳,超標(biāo)圖片中的未使用次數(shù)就重置為0盼砍。當(dāng)啟動(dòng)次數(shù)達(dá)到清理值,那么我們就遍歷MMKV逝她,將未使用次數(shù)到20的圖片信息進(jìn)行刪除浇坐,再重置當(dāng)前啟動(dòng)次數(shù)。
4.2.4 超標(biāo)圖片顯示
對(duì)于超標(biāo)圖片顯示黔宛,這里采取了兩種查看方式近刘,一種是通過彈窗提示,另外一種是通過列表展示臀晃。
這里沒什么好說的觉渴,主要注意一下懸浮窗權(quán)限的問題。
在實(shí)現(xiàn)列表展示的時(shí)候徽惋,我糾結(jié)過列表中的數(shù)據(jù)是展示所有的超標(biāo)圖片呢淤击?
還是本次啟動(dòng)加載到的超標(biāo)圖片裁眯?最后決定還是展示本次加載到的超標(biāo)圖片,主要有這么幾點(diǎn)考慮,首先如果加載所有超標(biāo)圖片多柑,那么勢必要從本地讀取超標(biāo)圖片的數(shù)據(jù)轻要,如果數(shù)據(jù)很多的話沾鳄,列表就會(huì)很長箕戳,如果用戶只是想看當(dāng)前頁面超標(biāo)的圖片信息,那么查找會(huì)很不方便代咸。
其次如果要加載歷史的超標(biāo)圖片信息蹈丸,涉及到一個(gè)問題,加載超標(biāo)圖片信息就要加載超標(biāo)圖片的略縮圖呐芥,那么問題來了逻杖,我們Hook了四大圖片加載框架,如果我們?cè)诩虞d略縮圖時(shí)采用了這四大圖片框架思瘟,那么就會(huì)再次收到圖片信息弧腥,由于加載的是略縮圖,所以圖片框架肯定會(huì)對(duì)圖片進(jìn)行壓縮潮太,那么就會(huì)更新超標(biāo)圖片的信息,這樣就會(huì)導(dǎo)致由于加載了一張超標(biāo)圖片的略縮圖導(dǎo)致超標(biāo)圖片信息被更新為未超標(biāo)虾攻,從而被刪除铡买。
這是我們不希望看見的,而只加載本次遇見的超標(biāo)圖片霎箍,我們可以將本次超標(biāo)的圖片緩存在內(nèi)存中奇钞,在列表展示的時(shí)候直接顯示緩存的Bitmap對(duì)象,這樣我們就不需要使用圖片加載框架漂坏,也就不存在這個(gè)問題景埃。
5媒至、寫在最后
到此大圖監(jiān)控的原理就講解的差不多了,大家可以到我的Github上結(jié)合源碼進(jìn)行分析谷徙。并且了解
https://github.com/AndroidCot/Android