Analysis of Picasso Source Code

Keywords: network Java Android OkHttp

Analysis of Picasso Source Code

When I first came into contact with Android a few years ago, it was more difficult to load and display pictures. Although it was not difficult, you had to care about the network, caching, traffic, memory leaks, etc. At this time, Picasso appeared with an artistic name, which suddenly made me realize the importance and power of so-called object-oriented encapsulation.Where, simple interface, don't care about internal implementation, so it took so long to understand its implementation. Although you don't need to repeat wheel building, you know how the wheel works.

Picasso.java

First, let's look at the basic usage of Picasso, starting with how it works, and getting closer to the truth step by step.

Picasso
    .with(context)
    .load("http://i.imgur.com/DvpvklR.png")
    .into(imageView);

WTF, so simple?This Nima is 189,000 kilometers from the truth. Please note that it is not a mile away, but Fuck belongs to Fuck. You still have to walk on your own way and cough.

1.Picasso.with()

This is the legendary singleton pattern. Let's first take a look at Picasso.with(context), what does this do?

  // Picasso is a global static property.
  static volatile Picasso singleton = null;
  public static Picasso with(@NonNull Context context) {
    if (context == null) {
      throw new IllegalArgumentException("context == null");
    }
    if (singleton == null) {
      synchronized (Picasso.class) {
        if (singleton == null) {
          singleton = new Builder(context).build();
        }
      }
    }
    return singleton;
  }

Pretend to explain this volatile wave first. Note that I'm going to start trying to force it. We all know that when multiple threads access the same variable, they copy it to their own thread. After using and modifying it, they assign it to the original thread. This process is not done in one step, it's not an atomic operation, if multiple threads access the change at the same time.Volatile member variables are forced to re-read from shared memory each time they are accessed by threads, while variable changes force threads to write back the changed values to shared memory so as to maximize the likelihood that two different threads will see the same member variable.One value to note here is that although it guarantees maximum data consistency, errors may occur in some cases because it is still not an atomic operation and there is no mechanism for using locks.

"Stop!!!"- shouted the director

"Teacher, let me say another word?"- Me

"No more, no more drumsticks tonight" - Director

"/(愒o愒)/~~"

Okay, that's the end. If you want to know more, please move here. Java concurrent programming: volatile keyword resolution See how this God perfectly confuses you.

Then let's see why the with method says that?

First of all, this is a single case, which I used to write like this (wrong example):

  static Picasso singleton = null;
  public static Picasso with(@NonNull Context context) {
    if (singleton == null) {
        singleton = new Builder(context).build();
    }
    return singleton;
  }

There's nothing wrong with single threading, but if it's multi-threaded, it's a big problem, and it can cause multiple Picasso to be created over and over again. Well, then you would say, improve it, add a lock:

  static Picasso singleton = null;
  public static synchronized Picasso with(@NonNull Context context) {
    if (singleton == null) {
        singleton = new Builder(context).build();
    }
    return singleton;
  }

Please allow the old man to spit, this sync...synx...syndkfddf...f**k How do you write this? It's so long and irregular. Did the people you designed java take into account our feelings from the lowercase hieroglyphs? How do you remember that you would die with sync?

To put it right, this doesn't solve the problem of multithreaded access "perfect", does it?Yes, but it's not perfect. The singleton only needs to be initialized once. Because synchronization locks are expensive on the system, we get singletons very often. For example, if I use Picasso and have 1,000 pictures to display, don't we want to lock them a thousand times?This?...I don't like this

Well, how can we, as programmers with code cleanliness plus obsessive-compulsive disorder, receive such things?So continue to improve:

  static Picasso singleton = null;
  public static Picasso with(@NonNull Context context) {
    if (singleton == null) {
        synchronized{
            if(singleton == null){
                singleton = new Builder(context).build();
            }
        }
    }
    return singleton;
  }

OK, now that we're using the handsome DoubleCheck, should we be satisfied?This is a classic single implementation, The answer is no.What?No more?O_u O "...why?

However, the foreign gods really say that this is not applicable in Java, why?And listen to me in detail.

First, let's assume two threads called the with method at the same time

  • Thread-1.with()
  • Thread-2.with()

  • At this point, they all found the painful fact that singleton == null, so Thread-1 first got the key, entered the synchronization lock, and decided if(singleton ==null) was a fact, so it executed singleton = new Builder(context).build(), the healthy old fellow, and it assigned this value to main memory.

  • Thread-2 then enters the synchronization lock, judging if(singleton ==null) finds this a painful fact

  • "Why?"
  • "Who asked?So many white words sprayed from the spitting stars in front?'
  • Because the singletons in Thread-1 and Thread-2 are copies of main memory, although Thread-1 assigns memory to main memory, in Thread-2, the singleton is null, so be careful, old fellow
  • So at this point Thread will still execute singleton = new Builder(context).build(), which will then repeat the creation of the singleton. In this case, it is not as slow as the one above. People are slow, but the good things won't go wrong.

Ayama, it's too heartfelt, Nima, give up?Do you want to go back to that?

No! There must be another way to solve this problem. Just as you scratch your ears and scratch your cheek, a flash flashes through your brain. Isn't volatile mentioned above just solving this problem?So every time I decide if(singleton == null) is copied from main memory, it's just tailored, oh, tailored, so we continue to improve to the following code:

  static volatile Picasso singleton = null;
  public static Picasso with(@NonNull Context context) {
    if (context == null) {
      throw new IllegalArgumentException("context == null");
    }
    if (singleton == null) {
      synchronized (Picasso.class) {
        if (singleton == null) {
          singleton = new Builder(context).build();
        }
      }
    }
    return singleton;
  }

Isn't that what people write in their Picasso source code?

2.Picasso.load()

Again, look at this fucking code first:

  public RequestCreator load(@Nullable String path) {
    if (path == null) {
      return new RequestCreator(this, null, 0);
    }
    if (path.trim().length() == 0) {
      throw new IllegalArgumentException("Path must not be empty.");
    }
    return load(Uri.parse(path));
  }

First of all, I make some judgments about whether path is null or path is ". If it is an empty string, it crashes me. Here, I want to write a story. Many people are always used to wrapping up libraries in the way of try catch where problems may occur. Especially when writing libraries, this is really a bad way of writing. If we find problems, we should expose it and let it go.Exposed to the sun, tell the people who use us that I've crashed here, how?Am I proud?If you like to try catch it, it's easy to hide the problem.

Okay, move on without looking back

  /**
   * Start an image request using the specified URI.
   * <p>
   * Passing {@code null} as a {@code uri} will not trigger any request but will set a placeholder,
   * if one is specified.
   *
   * @see #load(File)
   * @see #load(String)
   * @see #load(int)
   */
  public RequestCreator load(@Nullable Uri uri) {
    return new RequestCreator(this, uri, 0);
  }

Look, it will eventually create a class called RequestCreator. The programmer's name is still very real. Generally, anything is called. Unlike a person's name, it is not honest at all. For example, a person with a "handsome" name may not be handsome, like a person with a "fly" in my name, and I dare not ride on a rollercoaster.CHINA

So let's go into this category:

public class RequestCreator {
  private static final AtomicInteger nextId = new AtomicInteger();

  private final Picasso picasso;
  // Configuration holding some parameters in Request.Builder
  private final Request.Builder data;
  // Below are some configuration properties
  // Is it a smooth transition
  private boolean noFade;
  private boolean deferred;
  // Is a placeholder set
  private boolean setPlaceholder = true;
  // Placeholder ID
  private int placeholderResId;
  // Load Error Picture to Display
  private int errorResId;
  // Memory
  private int memoryPolicy;
  // network
  private int networkPolicy;
  // Placeholder Drawable
  private Drawable placeholderDrawable;
  // Error Picture Drawable
  private Drawable errorDrawable;
  // A Tag set by the user that can store anything
  private Object tag;
  RequestCreator(Picasso picasso, Uri uri, int resourceId) {
    if (picasso.shutdown) {
      throw new IllegalStateException(
          "Picasso instance already shut down. Cannot submit new requests.");
    }
    this.picasso = picasso;
    this.data = new Request.Builder(uri, resourceId, picasso.defaultBitmapConfig);
  }
}

Looking at its properties, we can probably see what it can do, such as, it can set a placeholder, load the wrong placeholder, whether to show the over-animation during picture switching, etc. So its role in Picasso from loading pictures to displaying should be preparation stage, and it should set a loading placeholder for ImageView at the same time, at the same time, itThere's also something called Request.Builder that this brother knows by name is a class that uses the Builder pattern, and there must be a lot of configured attributes in it too. Take a look:

  public static final class Builder {
    private Uri uri;
    private int resourceId;
    private String stableKey;
    private int targetWidth;
    private int targetHeight;
    private boolean centerCrop;
    private int centerCropGravity;
    private boolean centerInside;
    private boolean onlyScaleDown;
    private float rotationDegrees;
    private float rotationPivotX;
    private float rotationPivotY;
    private boolean hasRotationPivot;
    private boolean purgeable;
    private List<Transformation> transformations;
    private Bitmap.Config config;
    private Priority priority;

As expected by the old man, these are all very dense configurations. The old man will not go into details. Picture-related configurations, there must be a bunch of Setter methods underneath. As for the benefits of Builder mode, look at what this child shoe says, I am also lazy to say: Builder mode for design mode

Let's follow one of the methods in RequestCreator as an example to see how it does:

  /** Resize the image to the specified size in pixels. */
  public RequestCreator resize(int targetWidth, int targetHeight) {
    data.resize(targetWidth, targetHeight);
    return this;
  }

If I wanted to limit the size of this picture to the size I specified, it would be 1024*1024, but on different devices, it doesn't really need such a large picture at all, and it would eat a lot of memory, so I'm going to crop the picture according to the actual size of the device, so we use the resize() method, so we see it's actually saving this information to Reque before we talked about itIn st.Builder.

So that's all about preparation. All of the information, although we've given Picasso (in fact, a lot of information we haven't given Picasso, such as image clipping), is that it doesn't initiate a network request, it just saves the information to RequestCreater or Request.Builder, that is, the load() method is just preparation, and it's trueThe positive network request was triggered by the next method.

3.Picasso.into()

Picasso.in() has eight overload methods, but they can't change from one another. Find the most basic way to see it rattle:

  public void into(ImageView target, Callback callback) {
    long started = System.nanoTime();
    // Check to see if it is on the main thread
    checkMain();

    // ...delete some checking code here

    // Is the execution postponed to ensure that ImageView is laid out?
    // Already has size, if this state is true, Picasso will automatically
    // Picture clipped to match target size
    if (deferred) {
      // Users cannot customize size if matching ImageView size is set
      if (data.hasSize()) {
        throw new IllegalStateException("Fit cannot be used with resize.");
      }
      int width = target.getWidth();
      int height = target.getHeight();
      // Delay execution of this method if ImageView has a width and height of 0
      if (width == 0 || height == 0 || target.isLayoutRequested()) {
        if (setPlaceholder) {
          setPlaceholder(target, getPlaceholderDrawable());
        }
        picasso.defer(target, new DeferredRequestCreator(this, target, callback));
        return;
      }
      data.resize(width, height);
    }
    // This call to createRequest() will take the data we mentioned above
    // To convert to Request is to call build()
    Request request = createRequest(started);
    String requestKey = createKey(request);

    // Check user configuration to see if Bitmap is read from memory
    // If the user has configured Picasso to support caching to memory,
    // Read data from memory first, if it is read from memory then
    // Set the Bitmap directly to ImageView and prompt for successful loading
    // Return
    if (shouldReadFromMemoryCache(memoryPolicy)) {
      Bitmap bitmap = picasso.quickMemoryCacheCheck(requestKey);
      if (bitmap != null) {
        // Cancel Request
        picasso.cancelRequest(target);
        setBitmap(target, picasso.context, bitmap, MEMORY, noFade, picasso.indicatorsEnabled);
        if (picasso.loggingEnabled) {
          log(OWNER_MAIN, VERB_COMPLETED, request.plainId(), "from " + MEMORY);
        }
        if (callback != null) {
          callback.onSuccess();
        }
        return;
      }
    }
    // If there is no data in memory, go down
    // Set Placeholder Map
    if (setPlaceholder) {
      setPlaceholder(target, getPlaceholderDrawable());
    }

    // Build an ImageViewAction, such an Action in Picasso
    // There are several, all inherited from Action, Action is an abstract class that defines
    // Some common functions are implemented, and those that need to be differentiated are classes that inherit them to implement them
    // Differentiation operates primarily on the complete() and error() methods
    // Here the ImageViewAction needs to display the Bitmap obtained by seeding
    // On the ImageView, and TargetAction will get the Bitmap
    // Delivery, that's the subtlety of inheritance, so don't be foolish about it
    // Reuse inherited
    Action action =
        new ImageViewAction(picasso, target, request, memoryPolicy, networkPolicy, errorResId,
            errorDrawable, requestKey, tag, callback, noFade);
    picasso.enqueueAndSubmit(action);
  }

I'm a little confused about the code above. Picasso should have three levels of caching, memory, local, network. Why do you just see to check memory now, and then nothing?As we'll come to the end of this, let's move on

Picasso.enqueueAndSubmit()

  void enqueueAndSubmit(Action action) {
    Object target = action.getTarget();
    // Check to see if this object currently has an Action in progress
    // If there is one, cancel it before saving the upcoming request
    if (target != null && targetToAction.get(target) != action) {
      // This will also check we are on the main thread.
      cancelExistingRequest(target);
      targetToAction.put(target, action);
    }
    submit(action);
  }

  // submit is easy to pass through something called dispatcher
  // The word dispatch has been seen at the touch time and means to distribute events
  // Is it also distributed here?
  void submit(Action action) {
    dispatcher.dispatchSubmit(action);
  }

4.Dispatcher

The code above is simple. The only interesting thing is this dispatcher. It looks like it handles our Action. Let's just look at this Dispatcher.

class Dispatcher {
  final DispatcherThread dispatcherThread;
  final Handler handler;
  Dispatcher(...) {
    ...
    // This is a HandlerThread
    this.dispatcherThread = new DispatcherThread();
    // This Handler is a custom Handler
    this.handler = new DispatcherHandler(dispatcherThread.getLooper(), this);
    // This Handler was passed in by Picasso
    this.mainThreadHandler = mainThreadHandler;
    ...
  }

  void dispatchSubmit(Action action) {
    handler.sendMessage(handler.obtainMessage(REQUEST_SUBMIT, action));
  }
}

I've omitted a lot of code from it, we're just looking at the key parts.

  • First, why can an attribute modified as final be reassigned?Well, after checking the data, it turned out that I read a lot, and if final's properties have not been initialized, they can be initialized once in the construction method or code block.

  • Second, this code uses a combination of HandlerThread and Handler.We all know that Handler is used for inter-thread communication, and Handler Thread is obviously an asynchronous thread. That is, Handler Thread receives our messages through Handler and processes them on its thread. The advantage of Handler Thread is that it can always receive messages from Handler on an asynchronous thread, and its usage allows you to see what others have writtenFor analysis, I freely found an article on the Internet. Detailed HandlerThread in Android - Technical Blackhouse It's a huge collection of things, of course, if you can read English, it's great to suggest reading the official introductory documents.

  • So let's see that the dispatchSubmit() method is the handleMessage() method that sends a REQUEST_SUBMIT message to Dispatcher Handler, and note that the handleMessage() is on the Dispatcher Thread thread

  private static class DispatcherHandler extends Handler {
    private final Dispatcher dispatcher;
    @Override public void handleMessage(final Message msg) {
      switch (msg.what) {
        ...
        case REQUEST_SUBMIT: {
          Action action = (Action) msg.obj;
          dispatcher.performSubmit(action);
          break;
        }
        ...
    }
  }

As you can see above, Handler executes performSubmit() after receiving the REQUEST_SUBMIT message

  void performSubmit(Action action, boolean dismissFailed) {
    ...
    // BitmapHunter inherits from Runnable
    // Check the existence of this BitmapHunter in the hunterMap first
    BitmapHunter hunter = hunterMap.get(action.getKey());
    if (hunter != null) {
        // If so, simply replace the incoming action
      hunter.attach(action);
      return;
    }
    ...
    // Create a BitmapHunter
    hunter = forRequest(action.getPicasso(), this, cache, stats, action);
    // Execute BitmapHunter through ExecutorService thread pool
    // Notice that future.get() is not called here, so it is asynchronous
    hunter.future = service.submit(hunter);
    // Cache hunter
    hunterMap.put(action.getKey(), hunter);
    ...
  }

5.BitmapHunter

It's not easy to walk around here. One of the key classes used in the code above is BitmapHunter. Let's see

  // First, forRequest is a static method that returns a BitmapHunter
  static BitmapHunter forRequest(Picasso picasso, Dispatcher dispatcher, Cache cache, Stats stats,
      Action action) {
    Request request = action.getRequest();
    // This RequestHandler is also a base class, and the classes that implement it are:
    // AssetRequestHandler,FileRequestHandler,
    // MediaStoreRequestHandler,NetworkRequestHandler
    // And so on, there are a lot of, just by name, we guess it might be a specific class that gets the data
    // FileRequestHandler is obtained from the file system.
    // NetworkRequestHandler is acquired from the network
    List<RequestHandler> requestHandlers = picasso.getRequestHandlers();

    // for loop checks all RequestHandler s until it is found
    // RequestHandler that supports parsing (canHandleRequest()) request
    for (int i = 0, count = requestHandlers.size(); i < count; i++) {
      RequestHandler requestHandler = requestHandlers.get(i);
      if (requestHandler.canHandleRequest(request)) {
        return new BitmapHunter(picasso, dispatcher, cache, stats, action, requestHandler);
      }
    }
    // If not found, construct a default BitmapHunter
    return new BitmapHunter(picasso, dispatcher, cache, stats, action, ERRORING_HANDLER);
  }

Where we found the place to initialize all RequestHandler s, in Picasso's construction method, here's what we experienced

    allRequestHandlers.add(new ContactsPhotoRequestHandler(context));
    allRequestHandlers.add(new MediaStoreRequestHandler(context));
    allRequestHandlers.add(new ContentStreamRequestHandler(context));
    allRequestHandlers.add(new AssetRequestHandler(context));
    allRequestHandlers.add(new FileRequestHandler(context));
    allRequestHandlers.add(new NetworkRequestHandler(dispatcher.downloader, stats));
    requestHandlers = Collections.unmodifiableList(allRequestHandlers);

Later on, let's move on to BitmapHunter, since it's inherited from Runnable, there must be a run() method for all of them. Let's see what it does in the new thread:

  @Override public void run() {
    try {
      ...
      // Call the hunt() method and determine the return value
      result = hunt();
      // Processing callbacks based on return values
      if (result == null) {
        dispatcher.dispatchFailed(this);
      } else {
        dispatcher.dispatchComplete(this);
      }
    }  catch (Exception e) {
      exception = e;
      dispatcher.dispatchFailed(this);
    }
    ...
  }

OK, it looks like everything has been done in hunt. Why do you write that here?Can the methods in hunt() be written in run()?I think there are two reasons:

  1. First, if you put all the implementations in hunt() here, it would be very large and try catch would make the code hard to read
  2. Secondly, and most importantly, I think it's for a method to do only one event as much as possible for unit testing
  Bitmap hunt() throws IOException {
    Bitmap bitmap = null;
    // First check to see if this picture exists in memory
    if (shouldReadFromMemoryCache(memoryPolicy)) {
      bitmap = cache.get(key);
      if (bitmap != null) {
        stats.dispatchCacheHit();
        loadedFrom = MEMORY;
        if (picasso.loggingEnabled) {
          log(OWNER_HUNTER, VERB_DECODED, data.logId(), "from cache");
        }
        return bitmap;
      }
    }

    networkPolicy = retryCount == 0 ? NetworkPolicy.OFFLINE.index : networkPolicy;
    // Start calling the corresponding RequestHandler to load the picture
    RequestHandler.Result result = requestHandler.load(data, networkPolicy);
    if (result != null) {
      loadedFrom = result.getLoadedFrom();
      // Get Extended Information for Pictures
      exifOrientation = result.getExifOrientation();
      bitmap = result.getBitmap();

      // If there was no Bitmap then we need to decode it from the stream.
      if (bitmap == null) {
        Source source = result.getSource();
        try {
          bitmap = decodeStream(source, data);
        } finally {
          try {
            //noinspection ConstantConditions If bitmap is null then source is guranteed non-null.
            source.close();
          } catch (IOException ignored) {
          }
        }
      }
    }

    // stats is a class for statistics, let's leave it alone
    if (bitmap != null) {
      if (picasso.loggingEnabled) {
        log(OWNER_HUNTER, VERB_DECODED, data.logId());
      }
      stats.dispatchBitmapDecoded(bitmap);
      if (data.needsTransformation() || exifOrientation != 0) {
        // A synchronization lock is added to ensure that only one thread can process Bitmap at the same time
        // Minimizes OOM and reduces performance impact on mobile phones
        synchronized (DECODE_LOCK) {
          if (data.needsMatrixTransform() || exifOrientation != 0) {
          // Sometimes you may need to rotate or shift the picture
            bitmap = transformResult(data, bitmap, exifOrientation);
            if (picasso.loggingEnabled) {
              log(OWNER_HUNTER, VERB_TRANSFORMED, data.logId());
            }
          }
          // Is there a custom picture conversion, such as cropping into rounded rectangles?
          // This requires user customization and is easy to use
          // Pass in an original Bitmap and return a new Bitmap
          if (data.hasCustomTransformations()) {
            bitmap = applyCustomTransformations(data.transformations, bitmap);
            if (picasso.loggingEnabled) {
              log(OWNER_HUNTER, VERB_TRANSFORMED, data.logId(), "from custom transformations");
            }
          }
        }
        if (bitmap != null) {
          stats.dispatchBitmapTransformed(bitmap);
        }
      }
    }

    return bitmap;
  }

6.RequestHandler

Now let's look back at RequestHandler, taking NetworkRequestHandler as an example:

class NetworkRequestHandler extends RequestHandler {
  private static final String SCHEME_HTTP = "http";
  private static final String SCHEME_HTTPS = "https";

  @Override public boolean canHandleRequest(Request data) {
    String scheme = data.uri.getScheme();
    return (SCHEME_HTTP.equals(scheme) || SCHEME_HTTPS.equals(scheme));
  }

  @Override public Result load(Request request, int networkPolicy) throws IOException {
    // Loading data using OKHttp
    okhttp3.Request downloaderRequest = createRequest(request, networkPolicy);
    Response response = downloader.load(downloaderRequest);
    ResponseBody body = response.body();

    if (!response.isSuccessful()) {
      body.close();
      throw new ResponseException(response.code(), request.networkPolicy);
    }

    // Check whether the data source is network or local storage
    Picasso.LoadedFrom loadedFrom = response.cacheResponse() == null ? NETWORK : DISK;
    return new Result(body.source(), loadedFrom);
  }
}

We mentioned earlier why Picasso only has memory and network caches. From here, we can see that it also has local caches, but it uses OkHttp for local caches, all of which do not need to be written again by itself. This is convenient, but if you need to replace a network load library, if the network library does not support caching, you need toI wrote one by hand.

summary

By analyzing Picasso's source code, we can gain insight into how Picasso works and some of its code techniques:
-The correct posture of the singleton
- How volatile keywords work and use scenarios
- Use of HandlerThread
-Use of thread pool ExecutorService and use of synchronized keywords to limit thread consumption
-Reasonable packaging
- Weak reference and cache usage

Posted by JonathanS on Sat, 15 Jun 2019 12:17:48 -0700