Android | 用Kotlin仿寫Glide圖片加載框架

一椒功、前言

Glide是一個極其著名的Android的快速高效的開源媒體管理和圖像加載框架彭沼,它將媒體解碼廉侧、內(nèi)存和磁盤緩存以及資源池打包成一個簡單易用的界面肖方。源碼地址 https://github.com/bumptech/glide

Glide的設計十分巧妙,古人云:“紙上得來終覺淺祥绞,絕知此事要躬行”非洲。不動手寫只看代碼進行分析,印象始終不深刻,不能更進一步了解Glide框架設計精髓.

所以打算使用Kotlin語言進行仿寫,GitHub地址https://github.com/jiangpana/KGlide

二、關鍵類&作用

核心類
  • KGlide : 框架的入口類
  • Target: 圖片的請求的目標類,負責顯示圖片
  • BaseRequestOptions : 負責請求的相關基礎配置
  • Options : 負責請求過程配置的封裝,用map實現(xiàn)
  • Lifecycle: 基于觀察者設計模式,負責管理LifecycleListener,實現(xiàn)類有ActivityFragmentLifecycle,ApplicationLifecycle
  • FactoryPools : 對象池,復用對象
  • GlideExecutor : 線程池,主要有SourceExecutor ,DiskCacheExecutor ,AnimationExecutor ,只有SourceExecutor可以用來網(wǎng)絡請求
  • RequestManager: 負責管理圖片請求,實現(xiàn)LifecycleListener,在activity生命周期回調(diào)做相關處理
  • SingleRequest : 負責圖片請求成功失敗取消等相關操作
  • Engine : 負責啟動啟動圖片加載任務和內(nèi)存緩存
  • EngineJob : 負責啟動DecodeJob ,并處理資源成功失敗相關回調(diào)
  • DecodeJob : 負責從源獲取數(shù)據(jù),解碼,轉(zhuǎn)碼,變換,磁盤緩存相關
  • DataCacheGenerator : 負則從磁盤緩存獲取未解碼的data數(shù)據(jù)
  • ResourceCacheGenerator : 負則從磁盤緩存獲取已解碼可直接使用的resource數(shù)據(jù)
  • SourceGenerator : 負責用來獲取解碼的源數(shù)據(jù)
Registry
  • Registry:注冊表. 內(nèi)部主要有modelLoaderRegistry,decoderRegistry,resourceEncoderRegistry,encoderRegistry等相關注冊表,用來解碼轉(zhuǎn)碼編碼等相關功能
  • ModelLoaderRegistry : 模型加載表,用于通過model獲取data
  • ResourceDecoderRegistry : 資源解碼表,用于將獲取到的data 解碼成Resource
  • TranscoderRegistry: 轉(zhuǎn)碼表,用于將Resource轉(zhuǎn)碼,比如bitmap -> bitmapDrawable
  • DataRewinderRegistry: 數(shù)據(jù)回卷表,用于回卷流,便于inputStream重讀
  • EncoderRegistry: 編碼表,用于data磁盤緩存相關
  • ResourceEncoderRegistry: 資源編碼表,用于Resource磁盤緩存
  • DecodeHelper : 負責提供解碼編碼相關操作幫助類
ModelLoader 相關
  • ModelLoader : 接口,用于通過model獲取data
  • ModelLoaderFactory : 用于生產(chǎn)modelLoader
  • FileLoader : 處理model為file情況
  • StringLoader : 處理model為string的情況
緩存相關
  • Key : 負責磁盤緩存的key ,實現(xiàn)類有DataCacheKey,ResourceCacheKey 等
  • DiskLruCache : 負責磁盤緩存
  • LruArrayPool : 負責數(shù)組的緩存,防止內(nèi)存抖動
  • LruBitmapPool : 用來緩存bitmap,復用bitmap ,防止內(nèi)存抖動
  • LruResourceCache: 用來緩存resource ,大小根據(jù)cpu核心數(shù)計算
  • BitmapEncoder : 負責將bitmap編碼成file
  • StreamEncoder :負責將流編碼成file
  • StreamBitmapDecoder : 將流解碼成bitmap

三蜕径、流程&原理

處理生命周期&封裝參數(shù)

RequestManagerRetriever#supportFragmentGet方法中 , 構(gòu)建SupportRequestManagerFragment然后設置RequestManager,并添加到activity中用于監(jiān)聽生命周期

 private fun supportFragmentGet(
        context: Context,
        fm: FragmentManager,
        parentHint: Fragment?,
        isParentVisible: Boolean
    ): RequestManager {
       //構(gòu)建SupportRequestManagerFragment用于監(jiān)聽生命周期
        val current = getSupportRequestManagerFragment(fm, parentHint, isParentVisible);
        var requestManager = current.getRequestManager()
        if (requestManager == null) {
            val glide = KGlide.get(context)
            requestManager = factory.build(
                glide, current.getGlideLifecycle(), current.getRequestManagerTreeNode(), context
            )
            //設置RequestManager
            current.setRequestManager(requestManager)
        }
        return requestManager
    }

BaseRequestOptions#apply中,應用其他BaseRequestOptions的配置

 fun  apply(o: BaseRequestOptions<*>): T {
        val other = o
        other.fields.apply {
            if (isSet(SIZE_MULTIPLIER)) {
                sizeMultiplier = other.sizeMultiplier
            }
           // ... 省略大量類似代碼
            if (isSet(SIGNATURE)) {
                signature = other.signature
            }
            if (isSet(ONLY_RETRIEVE_FROM_CACHE)) {
                onlyRetrieveFromCache = other.onlyRetrieveFromCache
            }
            if (!isTransformationAllowed) {
                transformations.clear()
                fields.unSet(TRANSFORMATION)
                fields.unSet(TRANSFORMATION_REQUIRED)
                isTransformationRequired = false
                isScaleOnlyOrNoTransform = true
            }
        }
        fields = fields or other.fields
        options.putAll(other.options)
        return self()
    }

Options 設置相關option


  val CENTER_OUTSIDE: DownsampleStrategy = CenterOutside()
  val FIT_CENTER: DownsampleStrategy = FitCenter()
  val DEFAULT: DownsampleStrategy = CENTER_OUTSIDE
       
  fun downsample(strategy: DownsampleStrategy): T {
        return set(DownsampleStrategy.OPTION, strategy)
    }
    
  open operator fun <Y> set(option: Option<Y>, value: Y): T {
    options[option] = value
    return self()
}
構(gòu)建請求

SingleRequest#obtain方法中構(gòu)建request , 泛型R 默認為Drawable. 如果是asBitmap()則為Bitmap

  //callbackExecutor為在主線程執(zhí)行的Executor
  //target 默認為 DrawableImageViewTarget
  //model 為 string ,圖片請求地址
  //transcodeClass 為 Drawable.class
  //overrideWidth 解碼時候需要的圖片寬
  //overrideHeight 解碼時候需要的圖片高
  //priority 加載的優(yōu)先級
    fun <R> obtain(
            context: Context,
            glideContext: GlideContext,
            model: Any,
            transcodeClass: Class<R>,
            requestOptions: BaseRequestOptions<*>,
            overrideWidth: Int,
            overrideHeight: Int,
            priority: Priority,
            target: Target<R>,
            targetListener: RequestListener<R>? = null,
            requestListeners: List<RequestListener<R>>? = null,
            requestCoordinator: RequestCoordinator? = null,
            engine: Engine,
            animationFactory: TransitionFactory<R>? = null,
            callbackExecutor: Executor
        ): SingleRequest<R> {
         return SingleRequest()
         ...}
開始請求

Engine#waitForExistingOrStartNewJob 方法

 private fun <R> waitForExistingOrStartNewJob(
        glideContext: GlideContext,
        model: Any,
        signature: Key,
        width: Int,
        height: Int,
        resourceClass: Class<*>,
        transcodeClass: Class<R>,
        priority: Priority,
        diskCacheStrategy: DiskCacheStrategy,
        transformations: Map<Class<*>, Transformation<*>>,
        isTransformationRequired: Boolean,
        isScaleOnlyOrNoTransform: Boolean,
        options: Options,
        isMemoryCacheable: Boolean,
        useUnlimitedSourceExecutorPool: Boolean,
        useAnimationPool: Boolean,
        onlyRetrieveFromCache: Boolean,
        cb: ResourceCallback,
        callbackExecutor: Executor,
        key: EngineKey,
        startTime: Long
    ): LoadStatus? {
        //先從緩存中獲取EngineJob,如果當前任務還在執(zhí)行則添加回調(diào)
        val current: EngineJob<*>? = jobs.get(key, onlyRetrieveFromCache)
        current?.let {
            current.addCallback(cb, callbackExecutor)
            return LoadStatus(current, cb)
        }
        //構(gòu)建engineJob
        val engineJob = engineJobFactory!!.build<R>(
            key,
            isMemoryCacheable,
            useUnlimitedSourceExecutorPool,
            useAnimationPool,
            onlyRetrieveFromCache
        )
        //構(gòu)建decodeJob
        val decodeJob = decodeJobFactory!!.build(
            glideContext,
            model,
            key,
            signature,
            width,
            height,
            resourceClass,
            transcodeClass,
            priority,
            diskCacheStrategy,
            transformations,
            isTransformationRequired,
            isScaleOnlyOrNoTransform,
            onlyRetrieveFromCache,
            options,
            engineJob
        )
        //將engineJob緩存起來
        jobs.put(key, engineJob)
        engineJob.addCallback(cb, callbackExecutor)
        //啟動decodeJob
        engineJob.start(decodeJob)
        return LoadStatus(engineJob, cb)
    }
從源獲取數(shù)據(jù) data

SourceGenerator#startNext , 如果支持data緩存就處理緩存

 override fun startNext(): Boolean {
        printThis("startNext() " +Thread.currentThread().name)
        //緩存data
        if (dataToCache!=null){
            val data: Any = dataToCache!!
            dataToCache = null
            cacheData(data)
        }
        //緩存成功,從DataCacheGenerator進行處理,如果處理成功返回true
        if (sourceCacheGenerator != null && sourceCacheGenerator!!.startNext()) {
            return true
        }
        sourceCacheGenerator = null

        //從源獲取data
        loadData = null
        var started = false
        //遍歷modelLoader獲取data
        while (!started && hasNextModelLoader()) {
            loadData = helper.getLoadData()[loadDataListIndex++]
            loadData?.let {
                if (helper.getDiskCacheStrategy().isDataCacheable(it.fetcher.getDataSource())
                    || helper.hasLoadPath(it.fetcher.getDataClass())
                ) {
                    started = true
                    startNextLoad(it)
                }
            }
        }
        return started
    }

HttpUrlFetcher#loadData ,通過HttpURLConnection 下載圖片

  urlConnection.connectTimeout = DEFAULT_TIME_OUT
        urlConnection.readTimeout = DEFAULT_TIME_OUT
        urlConnection.useCaches = false
        urlConnection.doInput = true
        urlConnection.instanceFollowRedirects = false
        urlConnection.connect()
        stream = urlConnection.inputStream
        //下載圖片過程中取消則返回null
        if (isCancelled) {
            return null
        }
        val statusCode = urlConnection.responseCode;
        //請求成功則返回數(shù)據(jù)
        if (isHttpOk(statusCode)) {
            return getStreamForSuccessfulRequest(urlConnection)
        }else if (isHttpRedirect(statusCode)){
            println("$TAG  statusCode =300  ")
            //300 重定向
            val redirectUrlString = urlConnection.getHeaderField("Location")
            check(redirectUrlString.isNotBlank()){
                "Received empty or null redirect url"
            }
            val redirectUrl = URL(url, redirectUrlString)
            cleanup()
            return loadDataWithRedirects(redirectUrl, redirects + 1, url, headers)
        }else{
            throw Exception(urlConnection.responseMessage + "statusCode =$statusCode")
        }
對獲取到的data進行解碼

StreamBitmapDecoder#decode()

 override fun decode(
        source: InputStream,
        width: Int,
        height: Int,
        options: Options
    ): Resource<Bitmap>? {
        printThis(" decode -> width=$width , height=$height")
        var callbacks: Downsampler.DecodeCallbacks?=null
        return downsampler.decode(source  ,width,height,options,callbacks)
    }

Downsampler#decode() , 通過inTargetDensity和inDensity 方式減少內(nèi)存占用然后

  fun decode(
        ris: InputStream,
        width: Int,
        height: Int,
        options: Options,
        callbacks: DecodeCallbacks?
    ): Resource<Bitmap>? {
        var bitmap: Bitmap
        val options = BitmapFactory.Options()
        ris.reset()
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeStream(ris,null,options)
        //獲取源圖片的寬高
        options.inJustDecodeBounds = false;
        val sourceHeight =options.outHeight
        val sourceWidth =options.outWidth
        printThis("sourceHeight =$sourceHeight sourceWidth =$sourceWidth")
        //通過inTargetDensity,inDensity,inScaled方式優(yōu)化bitmap占用內(nèi)存大小
        options.inTargetDensity=width
        options.inDensity=sourceWidth
        options.inScaled=true
        //把流回到起點,重讀
        ris.reset()
        bitmap = BitmapFactory.decodeStream(ris,null,options)!!
        printThis("bitmap size = ${Util.getBitmapByteSize(bitmap)}")
        return BitmapResource.obtain(bitmap, bitmapPool);
    }
變換

DecodeJob#onResourceDecoded

 private fun <Z> onResourceDecoded(dataSource: DataSource, decoded: Resource<Z>?): Resource<Z>? {
       /.../
        var transformed = decoded
        var appliedTransformation: Transformation<Z>? = null
        if (dataSource != DataSource.RESOURCE_DISK_CACHE) {
            //RESOURCE_DISK_CACHE ,不需要 transformed
            appliedTransformation = decodeHelper.getTransformation(resourceSubClass as Class<Z>)
            transformed =
                appliedTransformation?.transform(glideContext!!, decoded, width, height) ?: decoded
        }
        /.../
        return result
    }
轉(zhuǎn)碼

bitmap 轉(zhuǎn)為bitmapDrawable

  //BitmapDrawableTranscoder#transcode
 override fun transcode(
        toTranscode: Resource<Bitmap>?,
        options: Options
    ): Resource<BitmapDrawable> {
        printThis("transcode")
        return LazyBitmapDrawableResource.obtain(resources, toTranscode)!!
    }
    
//LazyBitmapDrawableResource#get
 override fun get(): BitmapDrawable {
     return BitmapDrawable(resources, bitmapResource.get())
 }
顯示到imageview 中

EngineJob#CallResourceReady , 先調(diào)用 cb.onResourceReady(engineResource!!, dataSource)然后移除cb

  inner class CallResourceReady(val cb: ResourceCallback) : Runnable {
        override fun run() {
            synchronized(cb.getLock()) {
                synchronized(this@EngineJob) {
                    if (cbs.contains(cb)) {
                        // Acquire for this particular callback.
                        engineResource?.acquire()
                        callCallbackOnResourceReady(cb)
                        //移除監(jiān)聽,防止內(nèi)存泄漏
                        removeCallback(cb)
                    }
                }
            }
        }

    }
    
    // callCallbackOnResourceReady調(diào)用
      cb.onResourceReady(engineResource!!, dataSource)

SingleRequest實現(xiàn)ResourceCallback接口 onResourceReady方法中

    override fun onResourceReady(resource: Resource<*>, dataSource: DataSource?) {
        target.onResourceReady(resource.get() as R,null)
    }

DrawableImageViewTarget#setResource

 override fun setResource(resource: Drawable?) {
        view.setImageDrawable(resource)
    }
Resource緩存

將緩存策略設置為DiskCacheStrategy.RESOURCE

//如果dataSource 不等于RESOURCE_DISK_CACHE并且不等于MEMORY_CACHE則支持Resource緩存
override fun isResourceCacheable(
                isFromAlternateCacheKey: Boolean,
                dataSource: DataSource?,
                encodeStrategy: EncodeStrategy?
            ): Boolean {
                return dataSource!=DataSource.RESOURCE_DISK_CACHE && dataSource!=DataSource.MEMORY_CACHE
            }

DecodeJob#onResourceDecoded

   if (diskCacheStrategy.isResourceCacheable(
                isFromAlternateCacheKey,
                dataSource,
                encodeStrategy
            )
        ) {
            //構(gòu)建用于緩存的key
            val key: Key
            when (encodeStrategy) {
                EncodeStrategy.SOURCE -> key = DataCacheKey(currentSourceKey!!, signature!!);
                EncodeStrategy.TRANSFORMED -> key = ResourceCacheKey(
                    decodeHelper.getArrayPool(),
                    currentSourceKey!!,
                    signature!!,
                    width,
                    height,
                    appliedTransformation,
                    resourceSubClass,
                    options!!
                )
                else -> throw IllegalArgumentException("Unknown strategy: $encodeStrategy")
            }

            val lockedResult = LockedResource.obtain(transformed)
            //初始化deferredEncodeManager,用于待會緩存
            deferredEncodeManager.init(key, encoder!!, lockedResult)
            result = lockedResult
        }

DecodeJob#notifyEncodeAndRelease

    private fun notifyEncodeAndRelease(resource: Resource<R>, dataSource: DataSource) {
        printThis("notifyEncodeAndRelease")
        val result = resource
        notifyComplete(result, dataSource)
        stage = Stage.ENCODE
        try {
            if (deferredEncodeManager.hasResourceToEncode()) {
               //緩存資源
               deferredEncodeManager.encode(diskCacheProvider, options!!)
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
        onEncodeComplete()
    }

四两踏、總結(jié)

本文首先解讀各主要類的功能以及方法執(zhí)行流程,然后對框架進行解讀.

用kotlin語言精簡代碼進行仿寫, 希望能更加理解glide源碼設計精髓,但glide的源碼所能獲取的營養(yǎng)遠不止如此.

每看一遍又會有不一樣的理解, 讓人受益匪淺,在此對Glide開源工作者表示崇高的敬意和感謝。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末兜喻,一起剝皮案震驚了整個濱河市梦染,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌朴皆,老刑警劉巖帕识,帶你破解...
    沈念sama閱讀 217,657評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異遂铡,居然都是意外死亡肮疗,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評論 3 394
  • 文/潘曉璐 我一進店門扒接,熙熙樓的掌柜王于貴愁眉苦臉地迎上來伪货,“玉大人,你說我怎么就攤上這事钾怔〖詈簦” “怎么了?”我有些...
    開封第一講書人閱讀 164,057評論 0 354
  • 文/不壞的土叔 我叫張陵宗侦,是天一觀的道長巍举。 經(jīng)常有香客問我,道長凝垛,這世上最難降的妖魔是什么懊悯? 我笑而不...
    開封第一講書人閱讀 58,509評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮梦皮,結(jié)果婚禮上炭分,老公的妹妹穿的比我還像新娘。我一直安慰自己剑肯,他們只是感情好捧毛,可當我...
    茶點故事閱讀 67,562評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著让网,像睡著了一般呀忧。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上溃睹,一...
    開封第一講書人閱讀 51,443評論 1 302
  • 那天而账,我揣著相機與錄音,去河邊找鬼因篇。 笑死泞辐,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的竞滓。 我是一名探鬼主播咐吼,決...
    沈念sama閱讀 40,251評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼商佑!你這毒婦竟也來了锯茄?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,129評論 0 276
  • 序言:老撾萬榮一對情侶失蹤茶没,失蹤者是張志新(化名)和其女友劉穎肌幽,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體礁叔,經(jīng)...
    沈念sama閱讀 45,561評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡牍颈,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,779評論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了琅关。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片煮岁。...
    茶點故事閱讀 39,902評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖涣易,靈堂內(nèi)的尸體忽然破棺而出画机,到底是詐尸還是另有隱情,我是刑警寧澤新症,帶...
    沈念sama閱讀 35,621評論 5 345
  • 正文 年R本政府宣布步氏,位于F島的核電站,受9級特大地震影響徒爹,放射性物質(zhì)發(fā)生泄漏荚醒。R本人自食惡果不足惜芋类,卻給世界環(huán)境...
    茶點故事閱讀 41,220評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望界阁。 院中可真熱鬧侯繁,春花似錦、人聲如沸泡躯。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽较剃。三九已至咕别,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間写穴,已是汗流浹背惰拱。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留确垫,地道東北人弓颈。 一個月前我還...
    沈念sama閱讀 48,025評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像删掀,于是被迫代替她去往敵國和親翔冀。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,843評論 2 354

推薦閱讀更多精彩內(nèi)容