前陣子定了個小目标,打算來深入了解下幾個常用的開源庫,看下其源碼和實作原理,進行總結并輸出成文章。初定的目标是 EventBus、ARouter、LeakCanary、Retrofit、Glide、Coil、OkHttp 等七個。 目前已經完成了九篇關于 EventBus、ARouter、LeakCanary、Retrofit、Glide 的文章 ,本篇是第十篇,來對 Glide 的一些擴充知識點進行講解,希望對你有所幫助
一、利用 AppGlideModule 實作預設配置
在大多數情況下 Glide 的預設配置就已經能夠滿足我們的需求了,像緩存池大小,磁盤緩存政策等都不需要我們主動去設定,但 Glide 也提供了 AppGlideModule 讓開發者可以去實作自定義配置。對于一個 App 來說,在加載圖檔的時候一般都是使用同一張 placeholder,如果每次加載圖檔時都需要來手動設定一遍的話就顯得很多餘了,此時就可以通過 AppGlideModule 來設定預設的 placeholder
首先需要繼承于 AppGlideModule,在
applyOptions
方法中設定配置參數,然後為實作類添加 @GlideModule 注解,這樣在編譯階段 Glide 就可以通過 APT 解析到我們的這一個實作類,然後将我們的配置參數設定為預設值
/**
* 作者:leavesC
* 時間:2020/11/5 23:16
* 描述:
* GitHub:https://github.com/leavesC
*/
@GlideModule
class MyAppGlideModule : AppGlideModule() {
//用于控制是否需要從 Manifest 檔案中解析配置檔案
override fun isManifestParsingEnabled(): Boolean {
return false
}
override fun applyOptions(context: Context, builder: GlideBuilder) {
builder.setDiskCache(
//配置磁盤緩存目錄和最大緩存
DiskLruCacheFactory(
(context.externalCacheDir ?: context.cacheDir).absolutePath,
"imageCache",
1024 * 1024 * 50
)
)
builder.setDefaultRequestOptions {
[email protected] 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) {
}
}
在編譯後,我們的工程目錄中就會自動生成 GeneratedAppGlideModuleImpl 這個類,該類就包含了 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() {
return appGlideModule.isManifestParsingEnabled();
}
@Override
@NonNull
public Set<Class<?>> getExcludedModuleClasses() {
return Collections.emptySet();
}
@Override
@NonNull
GeneratedRequestManagerFactory getRequestManagerFactory() {
return new GeneratedRequestManagerFactory();
}
}
在運作階段,Glide 就會通過反射生成一個 GeneratedAppGlideModuleImpl 對象,然後根據我們的預設配置項來初始化 Glide 執行個體
@Nullable
@SuppressWarnings({"unchecked", "TryWithIdenticalCatches", "PMD.UnusedFormalParameter"})
private static GeneratedAppGlideModule getAnnotationGeneratedGlideModules(Context context) {
GeneratedAppGlideModule result = null;
try {
//通過反射來生成一個 GeneratedAppGlideModuleImpl 對象
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) {
//調用 MyAppGlideModule 的 applyOptions 方法,對 GlideBuilder 進行設定
annotationGeneratedModule.applyOptions(applicationContext, builder);
}
//根據 GlideBuilder 來生成 Glide 執行個體
Glide glide = builder.build(applicationContext);
···
if (annotationGeneratedModule != null) {
//配置自定義元件
annotationGeneratedModule.registerComponents(applicationContext, glide, glide.registry);
}
applicationContext.registerComponentCallbacks(glide);
Glide.glide = glide;
}
二、自定義網絡請求元件
預設情況下,Glide 是通過 HttpURLConnection 來進行聯網請求圖檔的,這個過程就由 HttpUrlFetcher 類來實作。HttpURLConnection 相對于我們常用的 OkHttp 來說比較原始低效,我們可以通過使用 Glide 官方提供的
okhttp3-integration
來将網絡請求交由 OkHttp 完成
dependencies {
implementation "com.github.bumptech.glide:okhttp3-integration:4.11.0"
}
如果想友善後續修改的話,我們也可以将
okhttp3-integration
内的代碼複制出來,通過 Glide 開放的 Registry 來注冊一個自定義的 OkHttpStreamFetcher,這裡我也提供一份 kotlin 版本的示例代碼
首先需要繼承于 DataFetcher,在拿到 GlideUrl 後完成網絡請求,并将請求結果通過 DataCallback 回調出去
/**
* 作者:leavesC
* 時間:2020/11/5 23:16
* 描述:
* 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()
val contentLength = Preconditions.checkNotNull(responseBody).contentLength()
stream = ContentLengthInputStream.obtain(responseBody!!.byteStream(), contentLength)
callback?.onDataReady(stream)
} else {
callback?.onLoadFailed(HttpException(response.message(), response.code()))
}
}
override fun cleanup() {
try {
stream?.close()
} catch (e: IOException) {
// Ignored
}
responseBody?.close()
callback = null
}
override fun cancel() {
call?.cancel()
}
override fun getDataClass(): Class<InputStream> {
return InputStream::class.java
}
override fun getDataSource(): DataSource {
return DataSource.REMOTE
}
}
之後還需要繼承于 ModelLoader,提供建構 OkHttpUrlLoader 的入口
/**
* 作者:leavesC
* 時間:2020/11/5 23:16
* 描述:
* 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() {
// Do nothing, this instance doesn't own the client.
}
}
}
最後注冊 OkHttpUrlLoader ,之後 GlideUrl 類型的請求都會交由其處理
/**
* 作者:leavesC
* 時間:2020/11/5 23:16
* 描述:
* GitHub:https://github.com/leavesC
*/
@GlideModule
class MyAppGlideModule : AppGlideModule() {
override fun isManifestParsingEnabled(): 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())
)
}
}
三、實作圖檔加載進度監聽
對于某些高清圖檔來說,可能一張就是十幾MB甚至上百MB大小了,如果沒有進度條的話使用者可能就會等得有點難受了,這裡我就提供一個基于
OkHttp 攔截器實作的監聽圖檔加載進度的方法
首先需要對 OkHttp 原始的 ResponseBody 進行一層包裝,在内部根據
contentLength和
已讀取到的流位元組數來計算目前進度值,然後向外部提供通過 imageUrl 來注冊 ProgressListener 的入口
/**
* 作者:leavesC
* 時間:2020/11/6 21:58
* 描述:
* 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>
val progressListener = progressMap[pair.first]?.get()
progressListener?.update(pair.second)
}
}
}
}
}
private var bufferedSource: BufferedSource? = null
override fun contentType(): MediaType? {
return responseBody?.contentType()
}
override fun contentLength(): Long {
return responseBody?.contentLength() ?: -1
}
override fun source(): 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
}
}
}
}
然後在 Interceptor 中使用 ProgressResponseBody 對原始的 ResponseBody 多進行一層包裝,将我們的 ProgressResponseBody 作為一個代理,之後再将 ProgressInterceptor 添加給 OkHttpClient 即可
/**
* 作者:leavesC
* 時間:2020/11/6 22:08
* 描述:
* 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()
}
}
最終實作的效果:
四、自定義磁盤緩存 key
在某些時候,我們拿到的圖檔 Url 可能是帶有時效性的,需要在 Url 的尾部加上一個 token 值,在指定時間後 token 就會失效,防止圖檔被盜鍊。這種類型的 Url 在一定時間内就需要更換 token 才能拿到圖檔,可是 Url 的變化就會導緻 Glide 的磁盤緩存機制完全失效
https://images.pexels.com/photos/1425174/pexels-photo-1425174.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260&token=tokenValue
從我的上篇文章内容可以知道,一張圖檔在進行磁盤緩存時必定會同時對應一個唯一 Key,這樣 Glide 在後續加載同樣的圖檔時才能複用已有的緩存檔案。對于一張網絡圖檔來說,其唯一 Key 的生成就依賴于 GlideUrl 類的
getCacheKey()
方法,該方法會直接傳回網絡圖檔的 Url 字元串。如果 Url 的 token 值會一直變化,那麼 Glide 就無法對應上同一張圖檔了,導緻磁盤緩存完全失效
/**
* @Author: leavesC
* @Date: 2020/11/6 15:13
* @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() {
return stringUrl != null ? stringUrl : Preconditions.checkNotNull(url).toString();
}
}
想要解決這個問題,就需要來手動定義磁盤緩存時的唯一 Key。這可以通過繼承 GlideUrl,修改
getCacheKey()
方法的傳回值來實作,将 Url 移除 token 鍵值對後的字元串作為緩存 Key 即可
/**
* @Author: leavesC
* @Date: 2020/11/6 15:13
* @Desc:
* GitHub:https://github.com/leavesC
*/
class TokenGlideUrl(private val selfUrl: String) : GlideUrl(selfUrl) {
override fun getCacheKey(): 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") {
//将包含 token 的鍵值對移除
return selfUrl.replace(it, "")
}
}
return selfUrl
}
}
然後在加載圖檔的時候使用 TokenGlideUrl 來傳遞圖檔 Url 即可
Glide.with(Context).load(TokenGlideUrl(ImageUrl)).into(ImageView)
五、如何直接拿到圖檔
如果想直接取得 Bitmap 而非顯示在 ImageView 上的話,可以用以下同步請求的方式來獲得 Bitmap。需要注意的是,
submit()
方法就會觸發 Glide 去請求圖檔,此時請求操作還是運作于 Glide 内部的線程池的,但
get()
操作就會直接阻塞所線上程,直到圖檔加載結束(不管成功與否)才會傳回
thread {
val futureTarget = Glide.with(this)
.asBitmap()
.load(url)
.submit()
val bitmap = futureTarget.get()
runOnUiThread {
iv_tokenUrl.setImageBitmap(bitmap)
}
}
也可以用類似的方式來拿到 File 或者 Drawable
thread {
val futureTarget = Glide.with(this)
.asFile()
.load(url)
.submit()
val file = futureTarget.get()
runOnUiThread {
showToast(file.absolutePath)
}
}
Glide 也提供了以下的異步加載方式
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)
}
})
六、Glide 如何實作網絡監聽
在上篇文章我有講到,RequestTracker 就用于存儲所有加載圖檔的任務,并提供了
開始、暫停和重新開機所有任務的方法,一個常見的需要重新開機任務的情形就是使用者的網絡
從無信号狀态恢複正常了,此時就應該自動重新開機所有未完成的任務
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) {
//重新開機未完成的任務
requestTracker.restartRequests();
}
}
}
}
可以看出來,RequestManagerConnectivityListener 本身就隻是一個回調函數,重點還需要看 ConnectivityMonitor 是如何實作的。ConnectivityMonitor 實作類就在 DefaultConnectivityMonitorFactory 中擷取,内部會判斷目前應用是否具有
NETWORK_PERMISSION
權限,如果沒有的話則傳回一個空實作 NullConnectivityMonitor,有權限的話就傳回 DefaultConnectivityMonitor,在内部根據 ConnectivityManager 來判斷目前的網絡連接配接狀态
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)
: new NullConnectivityMonitor();
}
}
DefaultConnectivityMonitor 的邏輯比較簡單,不過多贅述。我覺得比較有價值的一點是:Glide 由于使用人數衆多,有比較多的開發者會回報 issues,DefaultConnectivityMonitor 内部就對各種可能抛出 Exception 的情況進行了捕獲,這樣相對來說會比我們自己實作的邏輯要考慮周全得多,是以我就把 DefaultConnectivityMonitor 複制出來轉為 kotlin 以便後續自己複用了
/**
* @Author: leavesC
* @Date: 2020/11/7 14: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() {
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() {
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
}
return networkInfo != null && networkInfo.isConnected
}
fun onStart() {
register()
}
fun onStop() {
unregister()
}
}
七、結尾
關于 Glide 的知識點擴充也介紹完了,上述的所有示例代碼我也都放到 GitHub 了,歡迎 star:
AndroidOpenSourceDemogithub.com