1. The background

Recently, Didi’s open source Dokit framework has a big picture monitoring function, which can set a threshold value for the file size and memory occupied by the picture, and prompt when the picture exceeds this value. This function is very useful when we do APK volume compression, memory management, for example, when we want to load an image from the background connection, the size of the image we do not know, although we now use Glide and other tripartite image loading framework, the framework will automatically compress the image. But it still takes up more memory than expected after compression. At this point we can use big picture monitoring during development, testing, and pre-production to identify images that are out of bounds.

2. Demand

Before discussing how to do it, we must define what we are going to do. The big picture monitoring framework I think should achieve the following functions:

  1. It can set thresholds for file size and memory occupied by pictures, and alarm if one of them exceeds.
  2. You can get detailed information about the image, including the current file size, memory occupied, image resolution, image miniaturization, image loading address, view size.
  3. The current picture information can be viewed through a popup window or a list.
  4. Both locally loaded images and network loaded images can be monitored.

3. Implementation idea

To monitor image file size and memory, we need to know the file size of the image and the memory used to load the image. At present, third-party frameworks are generally used to load pictures, so common picture loading frameworks can be hooked. Here, four mainstream picture loading frameworks are mainly hooked.

  1. Glide

  2. Picasso

  3. Fresco

  4. Image Loader

For example, when loading an image from the network using the image frame, we can use OkHttp or HttpUrlconnection to download the image and get the size of the image file. When the image frame constructs the image file into a Bitmap, we can get the memory used, so we can get both the file size and the memory used. We must Hook OkHttp and HttpUrlconnection as well.

Now that we’re going to Hook the tripartite framework, how do we Hook it? When choosing the implementation scheme of Hook, I conducted research on the following schemes.

  1. Reflection + dynamic proxy

  2. ASM

  3. AspectJ

  4. ByteBuddy

First of all, reflection + dynamic proxy can only be performed when the program is running, which will affect efficiency, so it is not considered for the time being. The other three are all capable of bytecode staking at compile time, and ASM manipulates the bytecode directly, making it less readable. AspectJ has been used in the past, and it often causes some puzzling problems, and the experience is not very good. ByteBuddy encapsulates ASM, which is said to be very efficient, and written in JAVA, with readable code, but there is very little information on the web, most of which are just a few articles and then forward. So the ASM implementation was chosen.

With ASM for bytecode insertion, when do we insert our bytecode into a third-party framework?

Transform Api

Starting with Android Gralde version 1.5.0, the Transform API has been added to allow third-party plug-ins to manipulate a compiled class file before converting it to a dex file. Gradle performs the transformations in the following order: JaCoCo-> third-party plug-ins ->ProGuard. The execution sequence of third-party plug-ins is the same as that of adding third-party plug-ins, and third-party plug-ins cannot control the execution sequence of transformations through APIS.

With the Transform API +ASM we can plug our own bytecode into a class file of a third party framework to do the piling in the compiler.

4. Implementation

We have now decided to use ASM to stake at compile time through the Transform API. So how do you do that? Let’s think about the functions we need to implement, we need to monitor the image, in order to monitor we need to get the data of the image, when the data is found, we need to give a hint. This means that there are two parts of the function, one is responsible for getting data through piling, and the other is responsible for displaying the exceeding data. Therefore, we adopted the form of Gradle custom plug-in +Android Library for the whole large image monitoring project.

  1. Largeimage-plugin: A custom Gradle plugin that inserts bytecodes written by us into class files.
  2. Largeimage: Android Library, which is mainly responsible for filtering the obtained image data, saving excessive images and presenting them to users in the form of popups or lists.

How to create a Gralde plug-in project won’t be covered here, there are many tutorials online. Most tutorials on the web will tell you to change the name of your plug-in project to buildSrc, which has many advantages, especially during code writing, and can be tested in this way

apply plugin:org.zzy.largeimage.LargeImageMonitorPlugin
Copy the code

Instead of publishing to the Maven repository every time you write it, changes to the plug-in project are directly reflected in the use module.

We’ve built our own Maven library and haven’t changed the name of the plugin project to buildSrc for consistency. You can use either of these to suit your needs.

4.1 the plug-in side

If you have a lot of transforms at compile time it will definitely affect the compile speed. Is there a way to reduce this impact? There are! Concurrent + incremental compilation.

Hunter is an open source library that helps you develop plug-ins quickly and supports concurrent + incremental compilation, which I used here.

Using this open source library is as simple as importing dependencies in your plug-in project’s build.gradle.

Next, in order to create our Transform and register it with the entire Transform queue, we need to create a class that implements the Plugin interface.

public class LargeImageMonitorPlugin implements Plugin<Project> {

    @Override
    public void apply(Project project) {
        List<String> taskNames = project.getGradle().getStartParameter().getTaskNames();
        // If it is a Release version, no bytecode replacement is performed
        for(String taskName : taskNames){
            if(taskName.contains("Release")) {return;
            }
        }

        AppExtension appExtension = (AppExtension)project.getProperties().get("android");
        // Create a custom extension
        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); }});// Add the custom Transform to the build process
        appExtension.registerTransform(new LargeImageTransform(project), Collections.EMPTY_LIST);
        / / add OkHttp
        appExtension.registerTransform(new OkHttpTransform(project),Collections.EMPTY_LIST);
        / / add a UrlConnection
        appExtension.registerTransform(newUrlConnectionTransform(project),Collections.EMPTY_LIST); }}Copy the code

This class does three things:

  1. Determine if the current version is a Release variant, if so no bytecode staking is performed. The reason is simple: the monitoring of excessive images should be done during the development and testing phase and not brought online.
  2. Get custom extensions, such as I need to add a pin switch flag to control whether bytecode enhancement is performed.
  3. Register the custom Transform.

As you can see in the code, we registered three custom Transforms because we were staking both the image loading frame and the network request library.

  1. LargeImageTransform: mainly responsible for Glide, Picasso was, Fresco, ImageLoader bytecode manipulation.
  2. OkHttpTransform: Performs bytecode operations on OkHttp.
  3. UrlConnectionTransform: Performs bytecode operations on UrlConnection.
4.1.1 Hook Image loading library

Using the Hunter framework makes it much easier to write a Transform without writing a Transform in the traditional way, so let’s focus on the key code.

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";
    /** * The current class name */
    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);
        // If the plug-in switch is off, no bytecode is inserted
        if(! Config.getInstance().largeImagePluginSwitch()) {return mv;
        }

        // TODO:2020/4/2 Consider doing version compatibility here
        // make bytecode modifications to the SingleRequest constructor for Glide4.11
        if(className.equals("com/bumptech/glide/request/SingleRequest") && methodName.equals("<init>") && desc! =null) {return mv == null ? null : new GlideMethodAdapter(mv,access,methodName,desc);
        }

        // Bytecode changes to Picasso's Request class constructor
        if(className.equals("com/squareup/picasso/Request") && methodName.equals("<init>") && desc! =null) {return mv == null ? null : new PicassoMethodAdapter(mv,access,methodName,desc);
        }

        // Make bytecode modifications to the constructor of Fresco's ImageRequest class
        if(className.equals("com/facebook/imagepipeline/request/ImageRequest") && methodName.equals("<init>") && desc! =null) {return mv == null ? null : new FrescoMethodAdapter(mv,access,methodName,desc);
        }

        // Make bytecode changes to the displayImage method of the ImageLoader class
        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);
        }
        returnmv; }}Copy the code

From the name of the inherited class, this is a visitor to the class that our project and third-party libraries pass through.

We record the name of the class we are currently passing through in the visit method. In the visitMethod method, we check whether we are currently accessing a method of a class. If the method we are currently accessing is the one we need to hook, then we perform our bytecode peg operation.

So how do we know which method of which class we want to hook? This requires us to read the source code need hook framework. We are going to Glide in the visitMethod method, Picasso was, Fresco, ImageLoader four pictures to hook load framework. So we need to know the Hook points of these four frameworks first. So how do you find Hook points? Although Didi’s Dokit project has already provided Hook points, with a learning attitude, we can try to analyze how to find Hook points.

When we Hook the image loading framework, the following points must be met:

1. The Hook point is the only way to execute the process.

2. After Hook, we can obtain the data we want.

3. After Hook, normal use cannot be affected.

After a rough analysis of the source code of the four picture loading frames, I found that most of the frames will call back to the interface after the successful loading of pictures, to inform the upper layer that the picture has been loaded successfully. Would it be possible to replace the interface that calls back after the image loads successfully with ours? Or we can add a custom interface, let the image loading success also call back our interface, so that we can get the data of the image.

Using the Glide framework as an example, Glide makes a traversal callback to the RequestListener interface in the onResourceReady method of the SingleRequest class after successfully loading the image.

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<?superR> animation = animationFactory.build(dataSource, isFirstResource); target.onResourceReady(result, animation); }}finally {
    isCallingCallbacks = false;
  }

  notifyLoadSuccess();
}
Copy the code

Several things can be learned from this code:

  1. RequestListeners are a List of listeners.
  2. The callback method onResourceReady has all the data we need.

All we need to do is add our own RequestListener to the Listeners. This way we can also get the image data when the interface calls back. So where do we insert our custom RequestListener? Let’s start with the definition of requestListeners in SingleRequest.

@Nullable private final List<RequestListener<R>> requestListeners;
Copy the code

RequestListeners are declared final, so they can only be assigned once at code time, and if they are member variables they must be initialized in the constructor.

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"); }}Copy the code

If we Hook our custom requestListeners into the Constructor of SingleRequest, the listeners will be called back to our methods to retrieve the image data when the image is successfully loaded. So we have our Hook to the Glide framework, and we have this code from the visitMethod method:

// make bytecode modifications to the SingleRequest constructor for Glide4.11
if(className.equals("com/bumptech/glide/request/SingleRequest") && methodName.equals("<init>") && desc! =null) {return mv == null ? null : new GlideMethodAdapter(mv,access,methodName,desc);
}
Copy the code

Is this the constructor of the SingleRequest class used by Glide? If so, bytecode insertion is performed.

Now that we have the Hook points, we need to add custom requestListeners to the requestListeners. So there are two options.

RequestListeners can be added to the listeners when the SingleRequest constructor comes in. RequestListeners are then assigned to the member variable this.requestlisteners.

RequestListeners can be assigned to the member variable this.requestListeners before the method exits, adding our own requestListeners to the process.

The two methods seem to do the same thing, but the bytecodes are different.

The statement and bytecode of the first method are as follows:

/ / statements
GlideHook.process(requestListeners);
/ / bytecode
mv.visitVarInsn(ALOAD, 12);
mv.visitMethodInsn(INVOKESTATIC, "org/zzy/lib/largeimage/aop/glide/GlideHook"."process"."(Ljava/util/List;) Ljava/util/List;".false);
Copy the code

The statement and bytecode of the second method are as follows:

/ / statements
GlideHook.process(this.requestListeners);
/ / bytecode
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);
Copy the code

We know that Java creates a stack frame when executing a method. The stack frame contains local variables, operand stacks, dynamic links, method exits, and so on. The local variable table is determined at compile time, and its index starts at 0, indicating the instance reference of the object, which you can roughly think of as this.

In the first method, we stack requestListeners with index 12 in the local variable table using ALOAD instructions, and then call GlideHook’s static process method to pass them in.

In the second method, we stack this via the ALOAD directive, then access the requestListeners field of this object and pass it to the GlideHook static process method.

In terms of instructions, the first way has fewer instructions. But let’s consider a problem. In the first way, we manually get the value of the 12th index of the local variable table. If Glide ever wanted to add or remove a parameter to this constructor, our code would be incompatible. So for code compatibility, we go with the second method, where the probability of deleting a member variable directly is less than the probability of modifying the constructor entry.

Can we add our custom RequestListener directly to the constructor? This is fine, but the next time we add a custom RequestListener, we’ll have to modify the bytecode instructions on the plugin side, which is too much trouble. We can just get the List and add it in the Process method of GlideHook.

Let’s look at the implementation code:

public class GlideMethodAdapter extends AdviceAdapter {

    /** * Notice * 2 And then make changes to them. * ZhouZhengyi * Created at: 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); }}Copy the code

OnMethodExit means to add the following directive before the SingleRequest constructor exits. At this point, some people will ask, what if I write the bytecode instruction wrong? ASM Bytecode Outline for Android Studio is recommended here. Once installed, write the code in Java and right-click to generate bytecode. For example, we can create a test class:

public class Test {
    private List<RequestListener> requestListeners;
    / / simulation glide
    private void init(a){ GlideHook.process(requestListeners); }}Copy the code

This way we can get the bytecode instructions we want, and don’t forget to change the fully qualified name of the class. There are many tutorials on how to use this plugin, but I won’t go into them here.

At this point we have successfully inserted our coded bytecode into the Glide framework. The Hook point search for the other three image-loading frameworks is a similar idea, and most of them Hook in a constructor of a class. We found an interface in Fresco that would call back the image after it was successfully loaded. Unfortunately, when we called back the interface, we couldn’t get the image data. Finally, the Bitmap is obtained through Hook Postprocessor. Specific we can be combined with my github source code to analyze.

To sum up:

1. There may be more than one Hook point, so we can adopt it according to our own situation.

2. After getting the Hook object, we need to see if we can get the data we want. If not, we need to find it again.

3. Constructors are a good Hook point because they are generally initialized.

4. Code compatibility must be taken into account when selecting Hook mode.

Glide executes our inserted bytecode instruction when it reaches the SingleRequest constructor after the bytecode is inserted. Our custom RequestListener is called after the image is successfully loaded, and we’ll talk about how to do that later. This part of the logic we put in the LargeImage Library.

4.1.2 Hook OkHttp

As we said earlier, when we load a web image using an image frame, the image frame downloads the image from the web and then loads it. Glide, for example, Glide will store images are downloaded to the local, and then the local image read into memory to build a Resource, when the image loaded successfully, will we custom callback listener, but this time we can only obtain the data to the image after loaded into memory, that is to say we can’t get the file size of the picture. So consider whether you can get the file size of the picture after successfully downloading the picture? This requires us to Hook the network download framework and determine whether the Content-type starts with image every time we get a Response. If so, we will consider the request as an image.

With the idea in mind, we started to Hook OkHttp. The Hook point of OkHttp is easy to find, on the one hand, everyone is familiar with OkHttp source code, on the other hand,OkHttp excellent architecture. We all know that OkHttp uses a chain of interceptors to process data, and the authors reserve two places to add interceptors, one application interceptor and one network interceptor. As soon as we add our own interceptors in both places, the request and response data will pass through our interceptors. So the Hook for OkHttp is in the constructor of the OkHttpClient$Builder class.

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);
        // If the plug-in switch is off, no bytecode is inserted
        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);
        }
        returnmethodVisitor; }}Copy the code

And the addition of interceptors is global. Before you added OkHttp interceptors to your project, only your project’s network requests would be called back. But interceptors added this way, in this project and in third-party libraries, will be the same interceptors added whenever the OkHttp framework is used. Does HttpDns come to mind? In order to prevent DNS hijacking and speed up DNS resolution, OkHttp uses a custom DNS to implement HttpDns access. However, if you use a third-party image framework to load images on the server, you still go through port 53 UDP. Can we Hook Dns in OkHttp as well? This way we can globally add our own custom Dns, implementing the entire project using HttpDns to resolve domain names.

public class OkHttpMethodAdapter extends AdviceAdapter {

   
    * interceptors.addall (largeImage.getInstance ().getokHttpInterceptors ())); * networkInterceptors. * addAll(LargeImage.getInstance().getOkHttpNetworkInterceptors()); * dns = LargeImage.getInstance().getDns(); * ZhouZhengyi * Created at: 2020/4/5 9:39 */
    @Override
    protected void onMethodExit(int opcode) {
        super.onMethodExit(opcode);
        // Add application interceptor
        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);
        // Add network interceptor
        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);
        / / add 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;"); }}Copy the code

We insert our interceptor and custom DNS before the OkHttpClient$Builder constructor exits.

Again, the plug-in side is only responsible for inserting the bytecode, and all subsequent logic is stored in the Library.

4.1.3 Hook HttpUrlConnection

A lot of people are probably thinking, does anyone use HttpUrlConnection anymore? Is it still necessary to deal with it? Although the OkHttp framework is widely used today, HttpUrlConnection is still widely used and compatibility needs to be considered, right? The Glide framework, for example, uses the HttpUrlConnection request network, although the Glide framework can implement the OkHttp request network using a custom ModelLoader. But just to be on the safe side, we’ll do it all together. How do I Hook HttpUrlConnection? HttpUrlConnection source code also not read? Now that we’ve already hooked OkHttp, can we replace all HttpUrlConnection requests with OkHttp? This is to direct HttpUrlConnection requests to OkHttp so that data can be processed uniformly in OkHttp.

How can I replace HttpUrlConnection with OkHttp? When we did hooks, the general idea was that if the Hook object was an interface, we would use a dynamic proxy, and if it was a class, we would inherit it and override its methods. We could also create a custom class that inherits HttpUrlConnection and override its methods using OkHttp instead. The next question is where to replace the system’s HttpUrlConnection with our custom HttpUrlConnection. HttpUrlConnection is an abstract class and cannot be created using new. To obtain an HttpUrlConnection object, use the openConnection method of the URL class. We can Hook in all the places where the openConnection method is called and replace the HttpURLConnection object returned by the system with our own HttpURLConnection object.

Since all calls to the openConnection method require a Hook, there is no specific class, so we will not target specific classes this time.

public class UrlConnectionClassAdapter extends ClassVisitor {

    /** * This method is different from the other methodAdapters, which hook by class name and method name, i.e. when accessing a method of a class. * Class and method judgment is not done here
    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions);
        // If the plug-in switch is off, no bytecode is inserted
        if(! Config.getInstance().largeImagePluginSwitch()) {return methodVisitor;
        }
        return methodVisitor == null ? null : newUrlConnectionMethodAdapter(className, methodVisitor, access, name, desc); }}Copy the code

The URL class has two openConnection methods that Hook.

public class UrlConnectionMethodAdapter extends AdviceAdapter {

    /** * The method overridden here is also different from the other methodAdapters, which operate on method entry and exit, whereas this methodAdapter compares methods by instruction@paramOpcode instruction *@paramClass * for the owner operation@paramName Method name *@paramDesc Method description (Parameter) Returned value type * author: ZhouZhengyi * Created at: 2020/4/5 17:29 */
    @Override
    public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
        // Hook all classes and methods as long as there is an instruction to call the openConnection method
        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); }}}Copy the code

We have successfully returned an HttpURLConnection implemented by OkHttp to the consumer.

The HttpUrlConnection bytecode staking part ends here, and the rest of the logic is in the Library.

4.2 the Library side

The Library side does several things:

1. Initializes and receives user configurations.

2. Get the data you need from the framework’s callback.

3. Save the picture data that exceeds the standard.

4. Display pictures that exceed the standard.

4.2.1 Initialization and Configuration

The LargeImage class initializes and receives the user’s configuration. It is a user-directed class that is set up as a singleton and receives the user’s configuration as a chain call. Through this class, you can set the file size threshold of the image, the memory size threshold of the image, the addition of the OkHttp application interceptor, the addition of the OkHttp network interceptor and other configurations.

 LargeImage.getInstance()
        .install(this)// Be sure to call this method for initialization
        .setFileSizeThreshold(400.0)// Set the file size threshold to KB (optional)
        .setMemorySizeThreshold(100)// Set the memory usage threshold in KB (optional).
        .setLargeImageOpen(true)// Whether to enable the large image monitoring function. This function is enabled by default. If false, the large image will not be displayed in the large image list and pop-up window (optional).
        .addOkHttpInterceptor(new CustomGlobalInterceptor())// Add OKhttp custom global application listener (optional)
        .addOkHttpNetworkInterceptor(new CustomGlobalNetworkInterceptor())// Add Okhttp worth your deliberate global network listener (optional)
        .setDns(new CustomHttpDns);// Set up a custom global DNS, can implement their own HttpDns (optional)
Copy the code
4.2.2 Obtaining Data

When we are in the plug-in side insert bytecode into the framework, the framework will automatically we custom callback methods, in these methods can access to the data of the picture, so on this piece of have nothing to say, is simple, just call the relevant classes method is access to the data after save the data, don’t do too much business processing. It is worth mentioning that we need to customize HttpUrlConnection and use OkHttp when we Hook HttpUrlConnection. We don’t need to do this ourselves. A ObsoleteUrlFactory class called ObsoleteUrlFactory has been provided prior to OkHttp3.14 and has been implemented for us, but the class has been deprecated since 3.14 so we should just copy the class and use it directly.

4.2.3 Saving Data

After obtaining the image data, we are going to save, this part of the logic is responsible for by LargeImageManager, LargeImageManager class is also designed as a singleton. Since it is to save the data, we must be selective, that is, only save the picture information exceeding the standard, we do not care about the picture information exceeding the standard. And the excess information saved is to report to the user.

First, since we Hook OkHttp and image frame respectively, when loading a network image, we will receive the callback from OkHttp first. Here we can get the file size information of the image, and then we will receive the callback from the image frame. Get information about how much memory the image occupies. We mentioned earlier that we need to save the image information that exceeds the standard, and the definition of an image that exceeds the standard is the file size or memory usage, so we have no way to know whether the memory is out of standard when we call back OkHttp, because the image frame may compress the image. So we don’t need to determine whether the current image is saved during the OkHttp callback, but save it all, postponing the decision until the image frame callback. In the image frame callback, we can have both file size and memory usage, save if one of them exceeds, and delete if neither exceeds.

Secondly, we also encountered such a problem. When I used Glide framework to load a network picture, we assumed that the file size of the picture was out of limit, but the memory was not out of limit, so we would record all the information of the picture. But when the second start the APP, because Glide in the disk cache the image, would not be called again OkHttp to download pictures, so this time we only received the picture frame callback, in other words, we can only get the image data memory, if by this time the picture memory not to exceed bid, then we will delete this picture information, The user will not be prompted. To solve this problem, we had to keep the full information of the offending image in the SD card so that we could get the file size of the image even if the image frame loaded the image from the cache.

How can we save the information of exceeding image locally? Use SharedPreferences? Or the database? Because of frequent usage scenarios will increase, delete and modify the data, and SP is full amount every time to write, that is to say, SP in every time before writing data XML file name to back up files, and then read out data from an XML file with the new combined data written to a new XML file, if you execute successfully remove the backup XML file again, It’s inefficient. As for the efficiency of the database with SP is not too bad, but also to prevent a sudden crash caused by data not saved on the situation. This requires the use of components with real-time writing ability, so the mmap memory mapping file just for this scenario, through mmap memory mapping file, can provide a available to write to the memory block of at any time, the APP just to write the data, the operating system is responsible for the memory back to a file, and don’t have to worry about crash leads to loss of data. MMKV, which is open source by wechat, is a key-value component based on MMAP memory mapping. It is very efficient and has the ability of incremental update. Below are the comparative test data of MMKV, SP and SQlite by wechat team.

In the case of single process, on Huawei Mate 20 Pro 128G and Android 10, each group of operations were repeated 1K times, and the results were measured in ms, indicating MMKV’s high efficiency.

Using MMKV, we solved the problem of image frame not getting image file size when loading data from cache. However, another problem arises. After using MMKV, we save all the exceeding picture data locally. If the exceeding picture has not been used, should we keep it all the time? That is to say, when do we clean the data stored by MMKV? Using the LRU algorithm? This may work, but I’m using a slightly simpler implementation here. First we set a cleanup value, and when we reach that value, we start the cleanup. Here I set the default value to 20, which can be changed through the interface we provide. Also add a field to the superstandard image bean class that records the number of times the current image is unused. Then, each time the program starts, the current startup times will be increased by 1, and the unused times of the exceeded picture saved in MMKV will be increased by 1. If the picture is loaded once, the unused times of the exceeded picture will be reset to 0. When the startup times reach the clean value, we traverse MMKV, delete the picture information that has not been used for 20 times, and reset the current startup times.

4.2.4 Display of pictures exceeding standards

There are two ways to view the exceeding picture display, one is through pop-up prompt, the other is through list display.

There’s nothing to say here, but focus on the hover window permissions.

When implementing the list display, I have struggled with whether the data in the list show all the pictures that exceed the standard. Or is this startup loaded to the excessive picture? Finally decided to show the loaded into the standard image, mainly has so time to consider, first of all, if excess load all images, so must be read from the local paint pictures, if a lot of data, the list will be very long, if the user just want to look at pictures of the current page to exceed bid information, then find it will be very inconvenient. Second if you want to load history of image information, involves a problem, excess load images will load overweight slightly thumbnail images, then the problem comes, we Hook up four images load framework, when load slightly thumbnail if we used the four big picture frame, will be to receive image information, due to the load is slightly thumbnail, Therefore, the picture frame will definitely compress the picture, and then the information of the picture exceeding the standard will be updated. In this way, the information of the picture exceeding the standard will be updated as not exceeding the standard due to the loading of a slightly scaled image of the picture exceeding the standard, so as to be deleted. This is what we do not want to see, and we only load the pictures that exceed the standard we meet this time. We can cache the pictures in memory and directly display the cached Bitmap object when the list is displayed, so we do not need to use the picture loading framework, and this problem does not exist.

5. Write at the end

To this big picture monitoring principle is almost explained, you can go to my Github combined with the source code for analysis, if you think it is useful, you can give me a Star, the project will continue to iterate. Thanks to didi’s open source Dokit framework and Hunter open source library. Finally, you can also take a look at ByteX’s open source ByteX library, which is a bytecode plug-in development platform that integrates many useful plug-ins. See the ByteX documentation for more details.