For Android Developer, many open source libraries are essential knowledge points for development, from the use of ways to implementation principles to source code parsing, which require us to have a certain degree of understanding and application ability. So I’m going to write a series of articles about source code analysis and practice of open source libraries, the initial target is EventBus, ARouter, LeakCanary, Retrofit, Glide, OkHttp, Coil and other seven well-known open source libraries, hope to help you 😇😇

Official account: byte array

Article Series Navigation:

  • Tripartite library source notes (1) -EventBus source detailed explanation
  • Tripartite library source notes (2) -EventBus itself to implement one
  • Three party library source notes (3) -ARouter source detailed explanation
  • Third party library source notes (4) -ARouter own implementation
  • Three database source notes (5) -LeakCanary source detailed explanation
  • Tripartite Library source note (6) -LeakCanary Read on
  • Tripartite library source notes (7) -Retrofit source detailed explanation
  • Tripartite library source notes (8) -Retrofit in combination with LiveData
  • Three party library source notes (9) -Glide source detailed explanation
  • Tripartite library source notes (10) -Glide you may not know the knowledge point
  • Three party library source notes (11) -OkHttp source details
  • Tripartite library source notes (12) -OkHttp/Retrofit development debugger
  • Third party library source notes (13) – may be the first network Coil source analysis article

Use AppGlideModule to implement the default configuration

Glide’s default configuration is sufficient for most of the cases. We don’t need to set cache pool size, disk cache policy, etc., but Glide also provides AppGlideModule for developers to implement custom configurations. AppGlideModule (placeholder) is the default AppGlideModule for each image to be loaded

We need to inherit from AppGlideModule, set the configuration parameters in the applyOptions method, and add @glidemodule annotation to the implementation class so that Glide can be parsed to our implementation class at compile time. Then set our configuration parameters to the default values

/ * * * the author: leavesC * time: 2020/11/5 ephron; * description: * GitHub:https://github.com/leavesC * /
@GlideModule
class MyAppGlideModule : AppGlideModule() {

    // To control whether the configuration file needs to be parsed from the Manifest file
    override fun isManifestParsingEnabled(a): Boolean {
        return false
    }

    override fun applyOptions(context: Context, builder: GlideBuilder) {
        builder.setDiskCache(
            // Configure the disk cache directory and maximum cacheDiskLruCacheFactory( (context.externalCacheDir ? : context.cacheDir).absolutePath,"imageCache".1024 * 1024 * 50
            )
        )
        builder.setDefaultRequestOptions {
            return@setDefaultRequestOptions RequestOptions()
                .placeholder(android.R.drawable.ic_menu_upload_you_tube)
                .error(android.R.drawable.ic_menu_call)
                .diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
                .format(DecodeFormat.DEFAULT)
                .encodeQuality(90)}}override fun registerComponents(context: Context, glide: Glide, registry: Registry){}}Copy the code

After compiling, our project directory will automatically generate GeneratedAppGlideModuleImpl this class, the class contains MyAppGlideModule

@SuppressWarnings("deprecation")
final class GeneratedAppGlideModuleImpl extends GeneratedAppGlideModule {
  private final MyAppGlideModule appGlideModule;

  public GeneratedAppGlideModuleImpl(Context context) {
    appGlideModule = new MyAppGlideModule();
    if (Log.isLoggable("Glide", Log.DEBUG)) {
      Log.d("Glide"."Discovered AppGlideModule from annotation: github.leavesc.glide.MyAppGlideModule"); }}@Override
  public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) {
    appGlideModule.applyOptions(context, builder);
  }

  @Override
  public void registerComponents(@NonNull Context context, @NonNull Glide glide,
      @NonNull Registry registry) {
    appGlideModule.registerComponents(context, glide, registry);
  }

  @Override
  public boolean isManifestParsingEnabled(a) {
    return appGlideModule.isManifestParsingEnabled();
  }

  @Override
  @NonNull
  publicSet<Class<? >> getExcludedModuleClasses() {return Collections.emptySet();
  }

  @Override
  @NonNull
  GeneratedRequestManagerFactory getRequestManagerFactory(a) {
    return newGeneratedRequestManagerFactory(); }}Copy the code

In operation stage, the Glide through reflection generated a GeneratedAppGlideModuleImpl object, and then according to our default configuration items to initialize the Glide instance

 @Nullable
  @SuppressWarnings({"unchecked", "TryWithIdenticalCatches", "PMD.UnusedFormalParameter"})
  private static GeneratedAppGlideModule getAnnotationGeneratedGlideModules(Context context) {
    GeneratedAppGlideModule result = null;
    try {
      / / by reflection to generate a GeneratedAppGlideModuleImpl object
      Class<GeneratedAppGlideModule> clazz =
          (Class<GeneratedAppGlideModule>)
              Class.forName("com.bumptech.glide.GeneratedAppGlideModuleImpl");
      result =
          clazz.getDeclaredConstructor(Context.class).newInstance(context.getApplicationContext());
    } catch (ClassNotFoundException e) {
      if (Log.isLoggable(TAG, Log.WARN)) {
        Log.w(
            TAG,
            "Failed to find GeneratedAppGlideModule. You should include an"
                + " annotationProcessor compile dependency on com.github.bumptech.glide:compiler"
                + " in your application and a @GlideModule annotated AppGlideModule implementation"
                + " or LibraryGlideModules will be silently ignored");
      }
      // These exceptions can't be squashed across all versions of Android.
    } catch (InstantiationException e) {
      throwIncorrectGlideModule(e);
    } catch (IllegalAccessException e) {
      throwIncorrectGlideModule(e);
    } catch (NoSuchMethodException e) {
      throwIncorrectGlideModule(e);
    } catch (InvocationTargetException e) {
      throwIncorrectGlideModule(e);
    }
    return result;
  }


 private static void initializeGlide(
      @NonNull Context context,
      @NonNull GlideBuilder builder,
      @Nullable GeneratedAppGlideModule annotationGeneratedModule) { Context applicationContext = context.getApplicationContext(); ...if(annotationGeneratedModule ! =null) {
      // Set the GlideBuilder by calling the applyOptions method of MyAppGlideModule
      annotationGeneratedModule.applyOptions(applicationContext, builder);
    }
    // Generate a Glide instance based on GlideBuilderGlide glide = builder.build(applicationContext); ...if(annotationGeneratedModule ! =null) {
        // Configure custom components
        annotationGeneratedModule.registerComponents(applicationContext, glide, glide.registry);
    }
    applicationContext.registerComponentCallbacks(glide);
    Glide.glide = glide;
  }
Copy the code

Custom network request components

By default, Glide is made using HttpURLConnection for networking requests. This process is implemented by the HttpUrlFetcher class. HttpURLConnection is raw and inefficient compared to our usual OkHttp. We can use The okHttp3-Integration provided by Glide to make network requests to OkHttp

dependencies {
    implementation "Com. Making. Bumptech. Glide: okhttp3 - integration: 4.11.0"
}
Copy the code

To facilitate subsequent modifications, we can also copy the code in OkHttp3-Integration and register a custom OkHttpStreamFetcher through Glide’s open Registry. I also provide a kotlin version of the sample code here

The first step is to inherit from the DataFetcher, complete the network request after getting the GlideUrl, and call back the result of the request through DataCallback

/ * * * the author: leavesC * time: 2020/11/5 ephron; * description: * GitHub:https://github.com/leavesC * /
class OkHttpStreamFetcher(private val client: Call.Factory, private val url: GlideUrl) :
    DataFetcher<InputStream>, Callback {

    companion object {
        private const val TAG = "OkHttpFetcher"
    }

    private var stream: InputStream? = null

    private var responseBody: ResponseBody? = null

    private var callback: DataFetcher.DataCallback<in InputStream>? = null

    @Volatile
    private var call: Call? = null

    override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
        val requestBuilder = Request.Builder().url(url.toStringUrl())
        for ((key, value) in url.headers) {
            requestBuilder.addHeader(key, value)
        }
        val request = requestBuilder.build()
        this.callback = callback call = client.newCall(request) call? .enqueue(this)}override fun onFailure(call: Call, e: IOException) {
        if (Log.isLoggable(TAG, Log.DEBUG)) {
            Log.d(TAG, "OkHttp failed to obtain result", e) } callback? .onLoadFailed(e) }override fun onResponse(call: Call, response: Response) {
        if (response.isSuccessful) {
            responseBody = response.body()
            valcontentLength = Preconditions.checkNotNull(responseBody).contentLength() stream = ContentLengthInputStream.obtain(responseBody!! .byteStream(), contentLength) callback? .onDataReady(stream) }else{ callback? .onLoadFailed(HttpException(response.message(), response.code())) } }override fun cleanup(a) {
        try{ stream? .close() }catch (e: IOException) {
            // Ignored} responseBody? .close() callback =null
    }

    override fun cancel(a){ call? .cancel() }override fun getDataClass(a): Class<InputStream> {
        return InputStream::class.java
    }

    override fun getDataSource(a): DataSource {
        return DataSource.REMOTE
    }

}
Copy the code

Then you need to inherit from ModelLoader to provide an entry point for building OkHttpUrlLoader

/ * * * the author: leavesC * time: 2020/11/5 ephron; * description: * GitHub:https://github.com/leavesC * /
class OkHttpUrlLoader(private val client: Call.Factory) : ModelLoader<GlideUrl, InputStream> {

    override fun buildLoadData(
        model: GlideUrl,
        width: Int,
        height: Int,
        options: Options
    ): LoadData<InputStream> {
        return LoadData(
            model,
            OkHttpStreamFetcher(client, model)
        )
    }

    override fun handles(model: GlideUrl): Boolean {
        return true
    }

    class Factory(private val client: Call.Factory) : ModelLoaderFactory<GlideUrl, InputStream> {

        override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<GlideUrl, InputStream> {
            return OkHttpUrlLoader(client)
        }

        override fun teardown(a) {
            // Do nothing, this instance doesn't own the client.}}}Copy the code

OkHttpUrlLoader is registered, which handles requests of the GlideUrl type

/ * * * the author: leavesC * time: 2020/11/5 ephron; * description: * GitHub:https://github.com/leavesC * /
@GlideModule
class MyAppGlideModule : AppGlideModule() {

    override fun isManifestParsingEnabled(a): Boolean {
        return false
    }

    override fun applyOptions(context: Context, builder: GlideBuilder){}override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
        registry.replace(
            GlideUrl::class.java,
            InputStream::class.java,
            OkHttpUrlLoader.Factory(OkHttpClient())
        )
    }

}
Copy the code

Three, the realization of image loading progress monitoring

For some hd images, may be a dozen or even hundreds of MB size, if there is no progress bar users may be a little uncomfortable, here I provide a method based on OkHttp interceptor to monitor the image loading progress

We first wrap the original OkHttp ResponseBody, internally calculate the current progress based on the contentLength and the number of bytes read from the stream, and then provide an entry externally to register the ProgressListener via imageUrl

/ * * * the author: leavesC * time: 2020/11/6 21:58 * description: * GitHub:https://github.com/leavesC * /
internal class ProgressResponseBody constructor(
    private val imageUrl: String,
    private val responseBody: ResponseBody?
) : ResponseBody() {

    interface ProgressListener {

        fun update(progress: Int)

    }

    companion object {

        private val progressMap = mutableMapOf<String, WeakReference<ProgressListener>>()

        fun addProgressListener(url: String, listener: ProgressListener) {
            progressMap[url] = WeakReference(listener)
        }

        fun removeProgressListener(url: String) {
            progressMap.remove(url)
        }

        private const val CODE_PROGRESS = 100

        private val mainHandler by lazy {
            object : Handler(Looper.getMainLooper()) {
                override fun handleMessage(msg: Message) {
                    if (msg.what == CODE_PROGRESS) {
                        val pair = msg.obj as Pair<String, Int>
                        valprogressListener = progressMap[pair.first]? .get() progressListener? .update(pair.second) } } } } }private var bufferedSource: BufferedSource? = null

    override fun contentType(a): MediaType? {
        returnresponseBody? .contentType() }override fun contentLength(a): Long {
        returnresponseBody? .contentLength() ? : -1
    }

    override fun source(a): BufferedSource {
        if (bufferedSource == null) { bufferedSource = source(responseBody!! .source()).buffer() }return bufferedSource!!
    }

    private fun source(source: Source): Source {
        return object : ForwardingSource(source) {

            var totalBytesRead = 0L

            @Throws(IOException::class)
            override fun read(sink: Buffer, byteCount: Long): Long {
                val bytesRead = super.read(sink, byteCount)
                totalBytesRead += if(bytesRead ! = -1L) {
                    bytesRead
                } else {
                    0
                }
                val contentLength = contentLength()
                val progress = when {
                    bytesRead == -1L- > {100} contentLength ! = -1L -> {
                        ((totalBytesRead * 1.0 / contentLength) * 100).toInt()
                    }
                    else- > {0
                    }
                }
                mainHandler.sendMessage(Message().apply {
                    what = CODE_PROGRESS
                    obj = Pair(imageUrl, progress)
                })
                return bytesRead
            }
        }
    }

}
Copy the code

We then use the ProgressResponseBody in the Interceptor to wrap the original ResponseBody in an extra layer, using our ProgressResponseBody as a proxy, You can then add the ProgressInterceptor to OkHttpClient

/ * * * the author: leavesC * time: 2020/11/6 22:08 * description: * GitHub:https://github.com/leavesC * /
class ProgressInterceptor : Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        val originalResponse = chain.proceed(request)
        val url = request.url.toString()
        return originalResponse.newBuilder()
            .body(ProgressResponseBody(url, originalResponse.body))
            .build()
    }

}
Copy the code

The result achieved:

Customize the disk cache key

In some cases, the image Url we get may be timeliness, we need to add a token value at the end of the Url, the token will be invalid after the specified time, to prevent the image from being linked. This type of Url needs to change the token in a certain period of time to get the image, but the Url change will lead to Glide disk cache mechanism completely invalid

https://images.pexels.com/photos/1425174/pexels-photo-1425174.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260&token=tokenVal ue
Copy the code

As you can see from my previous article, every image that is cached on disk must also have a unique Key, so Glide can reuse the existing cached file when loading the same image later. For a network image, the generation of a unique Key depends on the getCacheKey() method of the GlideUrl class, which returns the Url string of the network image directly. If the TOKEN value of the Url changes all the time, Glide will not correspond to the same image, resulting in a complete failure of the disk cache

/ * * *@Author: leavesC
 * @Date: 2020/11/6 * hast@Desc: * GitHub:https://github.com/leavesC * /
public class GlideUrl implements Key {
    
  @Nullable private final String stringUrl;
    
  public GlideUrl(String url) {
    this(url, Headers.DEFAULT);
  }

  public GlideUrl(String url, Headers headers) {
    this.url = null;
    this.stringUrl = Preconditions.checkNotEmpty(url);
    this.headers = Preconditions.checkNotNull(headers);
  }
    
  public String getCacheKey(a) {
    returnstringUrl ! =null? stringUrl : Preconditions.checkNotNull(url).toString(); }}Copy the code

To solve this problem, you need to manually define the unique Key for the disk cache. This can be done by inheriting GlideUrl and modifying the return value of getCacheKey() to use the string after the TOKEN Key pair is removed from the Url as the cache Key

/ * * *@Author: leavesC
 * @Date: 2020/11/6 * hast@Desc: * GitHub:https://github.com/leavesC * /
class TokenGlideUrl(private val selfUrl: String) : GlideUrl(selfUrl) {

    override fun getCacheKey(a): String {
        val uri = URI(selfUrl)
        val querySplit = uri.query.split("&".toRegex())
        querySplit.forEach {
            val kv = it.split("=".toRegex())
            if (kv.size == 2 && kv[0] = ="token") {
                // Remove the key-value pair containing the token
                return selfUrl.replace(it, "")}}return selfUrl
    }

}
Copy the code

Then use TokenGlideUrl to pass the image Url when loading the image

      Glide.with(Context).load(TokenGlideUrl(ImageUrl)).into(ImageView)
Copy the code

How to get the picture directly

If you want to get the Bitmap directly instead of displaying it on the ImageView, you can get the Bitmap using the following synchronous request. Note that the submit() method triggers Glide to request the image. The request is still running in Glide’s internal thread pool, but the get() operation blocks the thread until the image is loaded (whether successfully or not)

            thread {
                val futureTarget = Glide.with(this)
                    .asBitmap()
                    .load(url)
                    .submit()
                val bitmap = futureTarget.get()
                runOnUiThread {
                    iv_tokenUrl.setImageBitmap(bitmap)
                }
            }
Copy the code

You can use a similar method to get a File or a Drawable

            thread {
                val futureTarget = Glide.with(this)
                    .asFile()
                    .load(url)
                    .submit()
                val file = futureTarget.get()
                runOnUiThread {
                    showToast(file.absolutePath)
                }
            }
Copy the code

Glide also provides the following asynchronous loading methods

            Glide.with(this)
                .asBitmap()
                .load(url)
                .into(object : CustomTarget<Bitmap>() {
                    override fun onLoadCleared(placeholder: Drawable?). {
                        showToast("onLoadCleared")}override fun onResourceReady(
                        resource: Bitmap,
                        transition: Transition<in Bitmap>? {
                        iv_tokenUrl.setImageBitmap(resource)
                    }
                })
Copy the code

Six, Glide how to achieve network monitoring

As I mentioned in the last article, the RequestTracker is used to store all the image-loading tasks and provides a way to start, pause, and restart all of them. A common case for restarting a task is when the user’s network returns to normal from an unsignaled state and all outstanding tasks should be automatically restarted

ConnectivityMonitor  connectivityMonitor =
        factory.build(
            context.getApplicationContext(),
            new RequestManagerConnectivityListener(requestTracker));  


private class RequestManagerConnectivityListener
      implements ConnectivityMonitor.ConnectivityListener {
    @GuardedBy("RequestManager.this")
    private final RequestTracker requestTracker;

    RequestManagerConnectivityListener(@NonNull RequestTracker requestTracker) {
      this.requestTracker = requestTracker;
    }

    @Override
    public void onConnectivityChanged(boolean isConnected) {
      if (isConnected) {
        synchronized (RequestManager.this) {
          // Restart the unfinished taskrequestTracker.restartRequests(); }}}}Copy the code

Can see that RequestManagerConnectivityListener itself is a callback function, also need to see the ConnectivityMonitor focus on how to implement. ConnectivityMonitor implementation class in DefaultConnectivityMonitorFactory, internal will determine whether the current application has NETWORK_PERMISSION permissions, If not, return an empty implementation NullConnectivityMonitor, have the permissions is returned DefaultConnectivityMonitor, internally judging by the ConnectivityManager current network connection status

public class DefaultConnectivityMonitorFactory implements ConnectivityMonitorFactory {
  private static final String TAG = "ConnectivityMonitor";
  private static final String NETWORK_PERMISSION = "android.permission.ACCESS_NETWORK_STATE";

  @NonNull
  @Override
  public ConnectivityMonitor build(
      @NonNull Context context, @NonNull ConnectivityMonitor.ConnectivityListener listener) {
    int permissionResult = ContextCompat.checkSelfPermission(context, NETWORK_PERMISSION);
    boolean hasPermission = permissionResult == PackageManager.PERMISSION_GRANTED;
    if (Log.isLoggable(TAG, Log.DEBUG)) {
      Log.d(
          TAG,
          hasPermission
              ? "ACCESS_NETWORK_STATE permission granted, registering connectivity monitor"
              : "ACCESS_NETWORK_STATE permission missing, cannot register connectivity monitor");
    }
    return hasPermission
        ? new DefaultConnectivityMonitor(context, listener)
        : newNullConnectivityMonitor(); }}Copy the code

DefaultConnectivityMonitor logic is simple, but many more. What I find valuable is: Glide due to the use of large number, we’ll have more developers will feedback issues, DefaultConnectivityMonitor within the capture may throw an Exception for all kinds of situation, this relatively than our own implementation logic to consider more comprehensive, So I just copy DefaultConnectivityMonitor out into kotlin so that subsequent reuse it himself

/ * * *@Author: leavesC
 * @Date: 2020/11/7 40 *@Desc: * /
internal interface ConnectivityListener {
    fun onConnectivityChanged(isConnected: Boolean)
}

internal class DefaultConnectivityMonitor(
    context: Context,
    val listener: ConnectivityListener
) {

    private val appContext = context.applicationContext

    private var isConnected = false

    private var isRegistered = false

    private val connectivityReceiver: BroadcastReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            val wasConnected = isConnected
            isConnected = isConnected(context)
            if(wasConnected ! = isConnected) { listener.onConnectivityChanged(isConnected) } } }private fun register(a) {
        if (isRegistered) {
            return
        }
        // Initialize isConnected.
        isConnected = isConnected(appContext)
        try {
            appContext.registerReceiver(
                connectivityReceiver,
                IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
            )
            isRegistered = true
        } catch (e: SecurityException) {
            e.printStackTrace()
        }
    }

    private fun unregister(a) {
        if(! isRegistered) {return
        }
        appContext.unregisterReceiver(connectivityReceiver)
        isRegistered = false
    }

    @SuppressLint("MissingPermission")
    private fun isConnected(context: Context): Boolean {
        val connectivityManager =
            context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager ? :return true
        val networkInfo = try {
            connectivityManager.activeNetworkInfo
        } catch (e: RuntimeException) {
            return true
        }
        returnnetworkInfo ! =null && networkInfo.isConnected
    }

    fun onStart(a) {
        register()
    }

    fun onStop(a) {
        unregister()
    }

}
Copy the code

Seven, making

Glide knowledge about the extension is also introduced, all of the above sample code I also put on GitHub, welcome star: AndroidOpenSourceDemo