當(dāng)App運(yùn)行時(shí)大圖監(jiān)控,你又會(huì)去怎么做呢灶壶?

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末拒啰,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子完慧,更是在濱河造成了極大的恐慌谋旦,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,978評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件屈尼,死亡現(xiàn)場離奇詭異册着,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)脾歧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門甲捏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人鞭执,你說我怎么就攤上這事司顿。” “怎么了蚕冬?”我有些...
    開封第一講書人閱讀 156,623評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵免猾,是天一觀的道長。 經(jīng)常有香客問我囤热,道長猎提,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,324評(píng)論 1 282
  • 正文 為了忘掉前任旁蔼,我火速辦了婚禮锨苏,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘棺聊。我一直安慰自己伞租,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評(píng)論 5 384
  • 文/花漫 我一把揭開白布限佩。 她就那樣靜靜地躺著葵诈,像睡著了一般。 火紅的嫁衣襯著肌膚如雪祟同。 梳的紋絲不亂的頭發(fā)上作喘,一...
    開封第一講書人閱讀 49,741評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音晕城,去河邊找鬼泞坦。 笑死,一個(gè)胖子當(dāng)著我的面吹牛砖顷,可吹牛的內(nèi)容都是我干的贰锁。 我是一名探鬼主播赃梧,決...
    沈念sama閱讀 38,892評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼豌熄!你這毒婦竟也來了授嘀?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,655評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤房轿,失蹤者是張志新(化名)和其女友劉穎粤攒,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體囱持,經(jīng)...
    沈念sama閱讀 44,104評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡夯接,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了纷妆。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片盔几。...
    茶點(diǎn)故事閱讀 38,569評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖掩幢,靈堂內(nèi)的尸體忽然破棺而出逊拍,到底是詐尸還是另有隱情,我是刑警寧澤际邻,帶...
    沈念sama閱讀 34,254評(píng)論 4 328
  • 正文 年R本政府宣布芯丧,位于F島的核電站,受9級(jí)特大地震影響世曾,放射性物質(zhì)發(fā)生泄漏缨恒。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評(píng)論 3 312
  • 文/蒙蒙 一轮听、第九天 我趴在偏房一處隱蔽的房頂上張望骗露。 院中可真熱鬧,春花似錦血巍、人聲如沸萧锉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽柿隙。三九已至,卻和暖如春鲫凶,著一層夾襖步出監(jiān)牢的瞬間优俘,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評(píng)論 1 264
  • 我被黑心中介騙來泰國打工掀序, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人惭婿。 一個(gè)月前我還...
    沈念sama閱讀 46,260評(píng)論 2 360
  • 正文 我出身青樓不恭,卻偏偏與公主長得像叶雹,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子换吧,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評(píng)論 2 348