主頁 > 移動端開發 > Glide原始碼難看懂?用這個角度讓你事半功倍!

Glide原始碼難看懂?用這個角度讓你事半功倍!

2021-08-12 07:56:49 移動端開發

前言

一個功能強大的框架,其背后少不了各種各樣的封裝,當我們一頭扎進去看的時候,很容易被原始碼里各種各樣的跳轉,設計模式,封裝等等,搞的云里霧里;

在這種情況下,我們只能將大概流程給搞懂,但是卻很容易忘記,為什么呢?

因為我們還沒有真正的理解它!

我們沒有將其轉化為我們的知識點,所以隔一段時間就容易忘記了;

那么我們要怎么將其轉化為我們的知識點呢?

不知道你有沒有發現,我們人的大腦是很難一下子記住一個很長很長的東西,但是一個名詞或者一個事物是可以很輕松的就記住的;

下面我會將原始碼拆散成一個個的小部件,然后由簡入深,用這個角度來理解Glide原始碼,讓你如虎添翼!

文章篇幅較長,建議點贊收藏慢慢觀看!

implementation ‘com.github.bumptech.glide:glide:4.12.0’
annotationProcessor ‘com.github.bumptech.glide:compiler:4.12.0’

1、第一版本實作

某一天,小明想實作一個加載圖片的功能,于是三下五除二的就把功能給實作了;

功能很簡單;
第一步:下載圖片;
第二步:將圖片設定給imageView;

代碼也很簡單,短短幾行就實作了,請看下面的偽代碼:


public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ImageView image = findViewById(R.id.image);
        String url = "圖片鏈接";
        // 訪問網路,下載圖片
        Bitmap bitmap = downLoadImage(url);
        // 將圖片bitmap設定給ImageView
        image.setImageBitmap(bitmap);
    }

    public Bitmap downLoadImage(String url) {
        // 訪問網路,下載圖片
        ...
       return bitmap;

    }
}

這樣的功能看起來是不是一下子就能記住呢? 這個功能是在是太簡單了,甚至都不用思考就能記住它;

但是這樣的功能并不通用,于是小明決定將其抽出來,設計成一個框架,以后在其他頁面有用到的話,就可以直接拿過來使用;

2、第二版本實作

于是小明先做了第一步的改造,設計出一個RequestManager用于管理請求操作,具有開啟,暫停,關閉網路請求的操作,同時還要有生命周期監聽,在生命周期銷毀的時候,關閉網路請求;

于是封裝好之后,就有了下面這幾個方法:


public class RequestManager {

    public void startRequest() {
        // 開啟網路請求
    }

    public void paustRequest() {
        // 暫停網路請求
    }

    public void onStart() {
        // 生命周期onStart
    }

    public void onStop() {
        // 生命周期onStop
    }

    public void onDestroy() {
        // 生命周期onDestroy
    }
}

當然看到這里你就會有疑問了,RequestManager的生命周期是怎么回呼的?

實作方法有很多種,最簡單的方法是在Activity或者fragment里面手動回呼!

第二步則設計一個請求的構建者,用于構建請求;

一個復雜的請求,必定有著各種各樣的引數,回呼監聽,加載失敗圖片的設定等等;

那么設計出來差不多就是下面這個效果:


public class RequestBuilder {

    public RequestBuilder load(@Nullable Object model) {
        // 賦值加載的引數
        return this;
    }

    public RequestBuilder listener(@Nullable RequestListener requestListener) {
        // 添加監聽操作
        return this;
    }

    public RequestBuilder error(@DrawableRes int resourceId) {
        // 設定錯誤圖片
        return this;
    }
}

既然有了RequestBuilder,那么最終是為了構建請求,那么小明還得設計出一個用于獲取圖片的Request,設計出來后的效果如下:

public interface Request {
    // 開始請求
    void begin();

    // 清除請求
    void clear();

    // 暫停請求
    void pause();
}

請求類已經設計好了,那么在Request的begin的時候就開始獲取圖片;

當圖片請求完之后,或者加載失敗,加載展位圖,都需要對控制元件進行設定,因此還需要設計一個類,專門用于設定圖片控制元件;

于是小明設計出了一個ImageViewTarget,用于設定圖片控制元件,請看下面的偽代碼;

public class ImageViewTarget {

    public void onl oadStarted(@Nullable Drawable placeholder) {
        // 開始加載,設定圖片展位圖
    }

    public void onl oadFailed(@Nullable Drawable errorDrawable) {
        // 加載失敗,設定圖片加載失敗展位圖
    }

    public void onResourceReady(@NonNull Drawable resource) {
        // 加載成功,設定圖片
    }
}

那么將這個簡單組合一個,就成了一個小小的“框架”,有了獲取圖片進行加載的功能了;

當然,我這里省略了部分代碼,你只要知道大概的加載流程就可以了,不必深究細節;

(1)通過RequestBuilder構建請求Request;
(2)通過RequestManager管理Request;
(3)Request請求成功或者失敗回呼ImageViewTarget設定給圖片控制元件;

很簡單,三步實作了一個小小的圖片“框架”;

3、第三版本實作

第一步我們已經簡單實作了,通過網路請求,加載圖片,設定給控制元件;

但是每一次都從網路上獲取圖片,在現實情況下,是非常不合理的;

因為每次都從網路上獲取,不但會導致網路資源的浪費,并且還會影響加載速度,萬一遇到網路不好的情況下,就容易加載很久才出來;

那么我們再對上面這個框架進行進一步的改造;

怎么改造呢? 很簡單,引入快取機制;

眾所周知,Android的快取機制可以分為幾種,一種是網盤快取,一種是磁盤快取,一種是記憶體快取;

那么我們就可以根據這三種情況來設計出具有三級快取的圖片加載機制;

一個簡單的快取機制就設計完成了,接下來就是代碼實作了;

要加載圖片快取機制,我們可以先設計出一個引擎類Engine,用來加載圖片的三級快取;

那么設計出來后大概是這個樣子:

public class Engine {

    public void load() {
        // 從三級快取里面加載圖片

    }

    private Bitmap loadFromMemory() {
        // 從快取里獲取圖片
        return null;
    }

    private Bitmap loadFromCache() {
        // 從磁盤里獲取圖片
        return null;
    }

    private Bitmap loadFromNetWork() {
        // 從網路里獲取圖片
        return null;
    }
}

3.1、記憶體快取的設計

首先是記憶體快取,記憶體快取的設計,非常的重要,因為涉及到圖片的回收;

回收早了,那么記憶體的快取起不到實際作用,回收慢了,又會占用記憶體,浪費記憶體空間;

那么這個圖片快取的演算法要怎么設計呢?

有一個著名演算法,就是最近最少使用演算法,非常適合用來做記憶體快取的回收;

什么叫最近最少使用演算法,當然字如其名;

即:長期不被使用的資料,在未來被用到的幾率也不大,因此,當資料所占記憶體達到一定閾值時,要移除掉最近最少使用的資料,

具體的演算法邏輯可以看下這個:什么是LRU(最近最少使用)演算法?

那么我們這里就可以用這個演算法來設計圖片快取;

public class LruResourceCache<T, Y> {

    private final Map<T, Y> cache = new LinkedHashMap<>(100, 0.75f, true);

    public synchronized Y put(@NonNull T key, @Nullable Y item) {
        // 快取圖片
        return null;
    }

    public synchronized Y get(@NonNull T key) {
        // 獲取圖片
        return null;
    }

    public void clearMemory() {
        // 清除所有快取
        trimToSize(0);
    }

    protected synchronized void trimToSize(long size) {
        // 回收記憶體到指定大小,將最近最少使用的,移除到指定大小
    }
}

這里設計了幾個方法,存和取的操作,還有清除快取,回收圖片記憶體的操作;

這時候你是否有疑問,我存取圖片的key要用那個呢?

很簡單,用圖片的url,因為每一張圖片的url都是不一樣的,所以可以把這個作為唯一的key;

那么這個簡單的記憶體快取就設計好了;

接下來我們來看看磁盤快取要怎么設計;

3.2、磁盤快取的設計

磁盤快取的設計,其實沒有那么復雜,無非就是將圖片檔案存盤到手機存盤卡里,讀也是從存盤卡里讀;

和記憶體不同的是,從磁盤里讀寫的速度較慢,好處就是,磁盤可以存盤比記憶體更多更大的圖片檔案;

因為和記憶體相比,存盤卡的容量要遠遠高于記憶體的大小;

和記憶體意義,我們設計了一個讀,一個寫的操作; 讀:負責從磁盤讀取圖片檔案 寫:負責將圖片檔案寫入到磁盤

public class DiskLruCache {

    public synchronized <T> T get(String key){
        // 獲取磁盤的資料
        return null;
    }

    public synchronized <T> void put(String key, T value){
        // 存盤圖片資料到磁盤中
    }
}

3.3、網路快取的設計

網路快取的設計就很簡單了,也就是直接訪問獲取,獲取圖片檔案;

public class NetWorkLruCache {

    public synchronized <T> T request(String url){
        // 獲取網路的資料
        return null;
    }
}

那么到這里,一個簡單的快取機制就設計完成了;

3.4、總結

那么一個簡單的圖片框架就這樣實作了,相較于之前的框架,多了快取機制,對于圖片的利用有了很大的提升;

如果我告訴你,恭喜你,你已經成功掌握了Glide原始碼的實作,我想我可能會被一巴掌拍扁了;

但是我想要告訴你的是,上面的原理,就是一個Glide原始碼的簡化,看懂了上面那個邏輯,基本上Glide原始碼的基本流程,你就已經搞懂了;

剩下的基本上就是更加細節的實作;

事實上,一個圖片框架的實作基本上離不開這幾步,更細節的實作,無非就是基于這幾步來進行擴展,封裝;

基礎的原理搞明白了,再去深入研究原始碼,才會有意義;

4、Glide版本實作

我們上面通過抽取實作了一個簡單的圖片框架,雖然功能都有了,但是總是感覺缺少了點什么!

缺了什么呢?

缺了更多的細節,比如設計模式的封裝,怎樣解耦以及更好的復用!

還有就是性能優化,上面我們這個框架,雖然引入了快取機制,但是還有更多的性能優化的點待挖掘;

下面我將根據上面這個簡單的圖片框架,來講一講Glide框架的實作的細節,相較于我們這個框架,有了什么更進一步的優化點;

4.1、RequestManager

我們上面在實作RequestManager的時候,提到了RequestManager的實作思路,手動的在Activity或者Fragment里面進行回呼;

而Glide的實作更加的巧妙,通過創建一個不可見的Fragment來實作生命周期的回呼;

Glide在呼叫Glide.with(this)方法的時候,就會回傳一個創建好的RequestManager,在創建RequestManager會創建一個不可見的Fragment,并將其設定給RequestManager,讓其具有生命周期的監聽;

創建Fragment的實作邏輯是在RequestManagerRetriever的ragmentGet這個方法里面;

看一下大概的實作;

private RequestManager fragmentGet(
    @NonNull Context context,
    @NonNull android.app.FragmentManager fm,
    @Nullable android.app.Fragment parentHint,
    boolean isParentVisible) {
    // 通過傳進來的FragmentManager來創建一個不可見的RequestManagerFragment
  RequestManagerFragment current = getRequestManagerFragment(fm, parentHint);
  // 通過RequestManagerFragment獲取RequestManager
  RequestManager requestManager = current.getRequestManager();
  if (requestManager == null) {
    Glide glide = Glide.get(context);
    // 如果RequestManager為空,則通過抽象工廠來創建RequestManger
    requestManager =
        factory.build(
            glide, current.getGlideLifecycle(), current.getRequestManagerTreeNode(), context);
    // 判斷當前頁面是否是可見的,是的話則回呼開始方法;
    if (isParentVisible) {
      requestManager.onStart();
    }
    // 將RequestManager設定給了fragment
    current.setRequestManager(requestManager);
  }
  return requestManager;
}

這里面的實作沒有很復雜,就做了兩件事,創建不可見的RequestManagerFragment,創建RequestManager,并將RequestManger設定給RequestManagerFragment,而RequestManagerFragment里面生命周期的回呼,都會回呼到RequestManager里;

這樣就讓RequestManager有了生命周期的監聽;

這里的Lifecycle不是Jectpack的Lifecycle組件,而是自己定義的一個監聽,用于回呼生命周期;

這里有哪個優化細節值得探討的呢?

這個細節就在于RequestManagerFragment的創建;

RequestManagerFragment的創建,并不是每次獲取都會重新創建的;

總共有三步邏輯,請看下面的原始碼

private RequestManagerFragment getRequestManagerFragment(
    @NonNull final android.app.FragmentManager fm, @Nullable android.app.Fragment parentHint) {
    // 先通過FragmentManager來獲取對應的fragment
  RequestManagerFragment current = (RequestManagerFragment) fm.findFragmentByTag(FRAGMENT_TAG);
  // 如果獲取為空,則從一個HashMap集合中獲取;
  if (current == null) {
    current = pendingRequestManagerFragments.get(fm);
    // 如果從集合中獲取為空,那么就新建一個Fragment并添加到頁面去,然后再將其放到HashMap中,并發送訊息,將該Fragment從HashMap中移除掉;
    if (current == null) {
      current = new RequestManagerFragment();
      current.setParentFragmentHint(parentHint);
      ...
      fm.beginTransaction().add(current, FRAGMENT_TAG).commitAllowingStateLoss();
      ...
    }
  }
  return current;
}

這里主要做了兩步操作:

第一步:通過FragmentManager來查找Fragment,如果獲取到了則回傳;

第二步:第一步沒有獲取到Fragment,則新建一個Fragment,將其添加到頁面;

在RequestManagerFragment的生命周期方法里,通過lifecycle進行回呼,而RequestManger注冊了這個監聽,那么就可以獲取到生命周期;

最終在RequestManger的生命周期里,開啟了圖片的加載和停止的操作;

4.2、RequestBuilder

RequestBuilder的職責很明確,用于創建獲取圖片的請求;

這個類使用了建造者模式來構建引數,這樣有一個好處就是,可以很方便的添加各種各樣復雜的引數;

這個RequestBuilder沒有build方法,但是有into方法,原理其實一樣,沒有說一定要寫成build;

最終加載圖片的邏輯,就是在into方法里面實作的;

private <Y extends Target<TranscodeType>> Y into(
    @NonNull Y target,
    @Nullable RequestListener<TranscodeType> targetListener,
    BaseRequestOptions<?> options,
    Executor callbackExecutor) {
...
  // 創建圖片請求
  Request request = buildRequest(target, targetListener, options, callbackExecutor);

  // 獲取當前圖片控制元件是否已經有設定圖片請求了,如果有且還沒有加載完成,
  // 或者加載完成但是加載失敗了,那么就將這個請求再重新呼叫begin,再一次進行請求;
  Request previous = target.getRequest();
  if (request.isEquivalentTo(previous)
      && !isSkipMemoryCacheWithCompletePreviousRequest(options, previous)) {
   ...
    if (!Preconditions.checkNotNull(previous).isRunning()) {
      ...
      previous.begin();
    }
    return target;
  }

//清除當前圖片控制元件的圖片請求
  requestManager.clear(target);
  // 設定請求給控制元件
  target.setRequest(request);
  // 再將請求添加到requestManager中
  requestManager.track(target, request);

  return target;
}

這里有兩個細節:

第一個細節:

在開始請求之前,會先獲取之前的請求,如果之前的請求還沒有加載完成,或者加載完成了但是加載失敗了,那么則會再次重試請求;

第二個細節:

將請求設定給了封裝了圖片控制元件的target,這樣做有什么好處呢?

我們的頁面大多數都是串列頁,那么基本上會使用RecycleView這種串列控制元件來加載資料,而這種串列在加載圖片的時候,快速滑動時會出現加載錯亂的問題,其原因是RecycleView的Item復用的問題;

而Glide就是在這里通過這樣的操作來避免這樣的問題;

在呼叫setRequest的時候,將當前的Request作為tag設定給了View,那么在獲取Request進行加載的時候,就不會出現錯亂的問題;

private void setTag(@Nullable Object tag) {
  ...
  view.setTag(tagId, tag);
}

4.3、Engine

從上面我們知道,一切的請求都是在Request的begin里開始的,而其實作是在SingleRequest的begin里面,最侄訓走到SingleRequest的onSizeReady里,通過Engine的load來加載圖片資料;

這個load方法主要分為兩步:

第一步:從記憶體加載資料

第二步:從磁盤或者網路加載資料

public <R> LoadStatus load(...) {
  ...
  synchronized (this) {
  // 第一步
    memoryResource = loadFromMemory(key, isMemoryCacheable, startTime);

// 第二步
    if (memoryResource == null) {
      return waitForExistingOrStartNewJob(
          glideContext,
          model,
          signature,
          width,
          height,
          resourceClass,
          transcodeClass,
          priority,
          diskCacheStrategy,
          transformations,
          isTransformationRequired,
          isScaleOnlyOrNoTransform,
          options,
          isMemoryCacheable,
          useUnlimitedSourceExecutorPool,
          useAnimationPool,
          onlyRetrieveFromCache,
          cb,
          callbackExecutor,
          key,
          startTime);
    }
  }
...
  return null;
}

簡單回顧一些,我上面設計了一個LruResourceCache的類來做記憶體快取,里面使用了LRU演算法;

相較于我們上面設計的從記憶體加載資料的邏輯,Glide這里的細節體現在哪里呢?

private EngineResource<?> loadFromMemory(
    EngineKey key, boolean isMemoryCacheable, long startTime) {
  ...

  // 從弱參考里獲取圖片資源
  EngineResource<?> active = loadFromActiveResources(key);
  if (active != null) {
    ...
    return active;
  }

 // 從LRU快取里面獲取圖片資源
  EngineResource<?> cached = loadFromCache(key);
  if (cached != null) {
    ...
    return cached;
  }

  return null;
}

首先第一個細節:

Glide設計了一個弱參考快取,當從記憶體里面加載時,會先從弱參考里面獲取圖片資源;

為什么要多設計一層弱參考的快取呢?

這里要說一下弱參考在記憶體里面的機制,弱參考物件在Java虛擬機觸發GC時,會回收弱參考物件,不管此時記憶體是不是夠用!

那么設計了一個弱參考快取的好處在于,沒有觸發GC的這段時間,可以重復的利用圖片資源,減少從LruCache里的操作;

看一下原始碼最終呼叫的地方:

synchronized EngineResource<?> get(Key key) {
  // 根據Key獲取到了一個弱參考物件
  ResourceWeakReference activeRef = activeEngineResources.get(key);
  if (activeRef == null) {
    return null;
  }

  EngineResource<?> active = activeRef.get();
  ...
  return active;
}

第二個細節:

從LruCache里獲取圖片資源后,并將其存入到弱參考快取里;

private EngineResource<?> loadFromCache(Key key) {
 // 
  EngineResource<?> cached = getEngineResourceFromCache(key);
  if (cached != null) {
    cached.acquire();
    activeResources.activate(key, cached);
  }
  return cached;
}

還有一個操作就是在圖片加載完成之后,會將該圖片資源存入到弱參考快取里,以便后續復用;

其原始碼位置在這里呼叫:Engine的onEngineJobComplete;

而這個方法是在圖片加載的回呼里呼叫的,也就是EngineJob的onResourceReady

public synchronized void onEngineJobComplete(
    EngineJob<?> engineJob, Key key, EngineResource<?> resource) {
  ...
  if (resource != null && resource.isMemoryCacheable()) {
   // 存入到弱參考快取里
    activeResources.activate(key, resource);
  }

  ...
}

其實這兩個細節,都是由弱參考快取來引入的,正是因為多了一個弱參考快取,所以才能把圖片的加載性能壓榨到了極致;

有人會說,加了這個有啥用呢? 又不能快多少,

俗話說的好:性能都是一步步壓榨出來的,在能優化的地方優化一點點,漸漸的積累,也就成河流了!

如果上面都獲取不到圖片資源,那么接下來就會從磁盤或者網路加載資料了;

4.4、DecodeJob

從磁盤或者網路讀取,必然是一個耗時的任務,那么肯定是要放在子執行緒里面執行,而Glide里也正是這樣實作的;

來看下大致的原始碼:

最侄訓創建一個叫DecodeJob的異步任務,來看下這個DecordJob的run方法做了什么操作?

public void run() {

 ...
    runWrapped();
 ...
}

run方法里面主要有一個runWrapped方法,這個方法才是最終執行的地方;

在這個com.bumptech.glide.load.engine.DecodeJob#getNextGenerator方法里面,會獲取記憶體生產者Generator,這幾個內容生產者分別對應著不同的快取資料;

private DataFetcherGenerator getNextGenerator() {
  switch (stage) {
    case RESOURCE_CACHE:
      return new ResourceCacheGenerator(decodeHelper, this);
    case DATA_CACHE:
      return new DataCacheGenerator(decodeHelper, this);
    case SOURCE:
      return new SourceGenerator(decodeHelper, this);
    case FINISHED:
      return null;
    ...
  }
}

4.5、DataCacheGenerator

ResourceCacheGenerator:對應轉化后的圖片資源生產者;

DataCacheGenerator:對應沒有轉化的原生圖片資源生產者;

SourceGenerator:對應著網路資源內容生產者;

這里我們只關心從磁盤獲取的資料,那么則對應著這個ResourceCacheGenerator和這個DataCacheGenerator的生產者;

這兩個方法的實作差不多,都是通過獲取一個File物件,然后再根據File物件來加載對應的圖片資料;

從上面我們可以知道,Glide的磁盤快取,是從DiskLruCache里面獲取的;

下面我們來看一下這個DataCacheGenerator的startNext方法;

public boolean startNext() {
  while (modelLoaders == null || !hasNextModelLoader()) {
    sourceIdIndex++;
    ...
    Key originalKey = new DataCacheKey(sourceId, helper.getSignature());
    // 通過生成的key從DiskLruCache里面獲取File物件
    cacheFile = helper.getDiskCache().get(originalKey);
    if (cacheFile != null) {
      this.sourceKey = sourceId;
      modelLoaders = helper.getModelLoaders(cacheFile);
      modelLoaderIndex = 0;
    }
  }

 ...
  while (!started && hasNextModelLoader()) {
    ModelLoader<File, ?> modelLoader = modelLoaders.get(modelLoaderIndex++);
    loadData =
        modelLoader.buildLoadData(
            cacheFile, helper.getWidth(), helper.getHeight(), helper.getOptions());
    if (loadData != null && helper.hasLoadPath(loadData.fetcher.getDataClass())) {
      started = true;
      loadData.fetcher.loadData(helper.getPriority(), this);
    }
  }
  return started;
}

這里主要分為兩步:

第一步是通過生成的key從DiskLruCache里面獲取File物件;

第二步是將File物件,通過LoadData將File物件轉化為Bitmap物件;

第一步:獲取File物件

Glide在加載DiskLruCache的時候,會將所有圖片對應的路徑資訊加載到記憶體中,當呼叫DiskLruCache的get方法時,其實是從DiskLruCache里面維護的一個Lru記憶體快取里直接獲取的;

所以第一步的get方法,其實是從LruCache記憶體快取里面獲取File物件的;

這個處理邏輯是在DiskLruCache的open方法里面操作的,里面會觸發一個讀取本地檔案的操作,也就是DiskLruCache的readJournal方法;

最終走到了readJournalLine方法;

這個檔案主要存放了圖片資源的key,用于獲取本地圖片路徑的檔案;

最終是在DiskLruCache的readJournalLine方法里面,會創建一個Entry物件,在Entry物件的構造方法里面創建了File物件;

private Entry(String key) {
  ...
  for (int i = 0; i < valueCount; i++) {
      // 創建圖片檔案File物件
      cleanFiles[i] = new File(directory, fileBuilder.toString());
      fileBuilder.append(".tmp");
      dirtyFiles[i] = new File(directory, fileBuilder.toString());
      ...
  }
}

到這里你是否會有疑問了,如果我是在DiskLruCache初始化之后下載的圖片呢? 這時候DiskLruCache的Lru記憶體里面肯定沒有這個資料,那這個資料是哪來的?

相信聰明的你已經猜到了,就是圖片檔案在存入本地的時候也會將其加入到DiskLruCache的Lru記憶體里;

其實作是在DiskLruCache的edit()方法;

private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
  ...
  Entry entry = lruEntries.get(key);
  ...
  if (entry == null) {
    entry = new Entry(key);
    lruEntries.put(key, entry);
  } 
  ...
  Editor editor = new Editor(entry);
  entry.currentEditor = editor;
  ...
  return editor;
}

這里先把生成的Entry物件加入到記憶體中,然后再通過Editor將圖片檔案寫入到本地,通過IO操作,這里就不多贅述了;

存的方法有了,來看一下DiskLruCache的get方法吧;

public synchronized Value get(String key) throws IOException {
 ...
 // 直接通過記憶體獲取Entry物件;
  Entry entry = lruEntries.get(key);
  ...

  return new Value(key, entry.sequenceNumber, entry.cleanFiles, entry.lengths);
}

第二步:File物件轉化

當根據key拿到File物件時,那么接下來就是將File檔案轉化為bitmap資料了;

這一段代碼的設計非常有意思,下面我們來看一下具體是怎么實作的!

DataCacheGenerator的startNext方法:

public boolean startNext() {
  while (modelLoaders == null || !hasNextModelLoader()) {
      ...
      modelLoaders = helper.getModelLoaders(cacheFile);
      ...

  }

while (!started && hasNextModelLoader()) {
 // 遍歷獲取ModelLoader
  ModelLoader<File, ?> modelLoader = modelLoaders.get(modelLoaderIndex++);
  // 獲取LoadData
  loadData =
      modelLoader.buildLoadData(
          cacheFile, helper.getWidth(), helper.getHeight(), helper.getOptions());
  if (loadData != null && helper.hasLoadPath(loadData.fetcher.getDataClass())) {
   // 加載資料
    started = true;
    loadData.fetcher.loadData(helper.getPriority(), this);
  }
}
  return started;
}

4.6、ModelLoader

這里通過設計了ModelLoader類,來用于加載資料邏輯;

這一段代碼都是基于介面來實作的,沒有依賴具體實作,好處就是非常的解耦與靈活,壞處就是代碼閱讀性降低,因為你很一時半會很難找到這里的實作類到底是哪個!

這個ModelLoader類的職責具體是做什么的呢? 我們可以先來看看注釋;

翻譯過來的意思就是:將任意復雜的資料模型轉換為具體資料型別的工廠介面;

怎么理解這句話呢? 可以理解為配接器模式,將某一類資料轉化為另外一類資料;

public interface ModelLoader<Model, Data> {

  ...
  class LoadData<Data> {
    public final Key sourceKey;
    public final List<Key> alternateKeys;
    public final DataFetcher<Data> fetcher;

    public LoadData(@NonNull Key sourceKey, @NonNull DataFetcher<Data> fetcher) {
      this(sourceKey, Collections.<Key>emptyList(), fetcher);
    }

    public LoadData(
        @NonNull Key sourceKey,
        @NonNull List<Key> alternateKeys,
        @NonNull DataFetcher<Data> fetcher) {
      this.sourceKey = Preconditions.checkNotNull(sourceKey);
      this.alternateKeys = Preconditions.checkNotNull(alternateKeys);
      this.fetcher = Preconditions.checkNotNull(fetcher);
    }
  }

...
  @Nullable
  LoadData<Data> buildLoadData(
      @NonNull Model model, int width, int height, @NonNull Options options);

 ...
  boolean handles(@NonNull Model model);

這個類方法很少,里面持有內部類LoadData,LoadData里面又持有介面DataFetcher,DataFetcher的職責就是用于加載資料,里面持有介面方法loadData;

這是一個典型的組合模式,可以在不改變類結構的情況下,讓這個類具有更多的功能;

ModelLoader還有一個介面方法buildLoadData,用于構建LoadData物件;

看到這里,你是否感覺懂了,但是又沒完全懂!

是的,盡管我們知道了這里的實作邏輯,但是具體的實作我們并不知道在哪里!

這里使用了兩個介面,一個是ModelLoader,一個是DataFetcher,找完ModelLoader的實作,還得找DataFetcher的實作,看起來就非常的繞!

而這一切還是得從ModelLoader講起;

實作了ModelLoader,也就實作了構建LoadData的方法,在構建LoadData的時候也就構建了DataFetcher物件,而這里的LoadData物件,其實只負責維護DataFetcher等相關變數,最終加載資料的地方還是DataFetcher;

也就是說我們找到了ModelLoader,也就找到了相應的DataFetcher,也就知道了對應的加載邏輯;

這個在設計上叫做啥? 高內聚!同一類相關的實作放在一起,實作了高內聚的封裝;

短短幾行代碼,大牛詮釋了什么叫做高內聚,低耦合,先膜拜一下!

接下來我們來看看ModelLoader是怎么獲取的;

從上面的原始碼我們可以看到,ModelLoader是通過DecodeHelper的getModelLoaders方法來獲取的,通過傳進去的File物件;

最終的呼叫是通過Registry的getModelLoaders方法來獲取ModelLoader串列;

這里有必要來講一下這個Registry類;

4.7、Registry

這個類的主要職責就是用于保存各種各樣的資料,提供給外部使用,你這里可以理解為一個Map集合,存放著各種各樣的資料,通過Key值來獲取;

只是這個Registry封裝的更強大,里面能保存的內容更豐富;

看一下這個類的構造方法,創建了一堆的注冊類,用于存放對應的資料;

而這個類的初始化是在Glide的構造方法,注冊對應的資料也是在Glide的構造方法;

大致瞄一眼,不必太過深入去了解,只需要知道這個類是用于存放注冊的資料,提供給外部使用;

當我們通過Registry獲取到ModelLoader串列后,就會進行遍歷,判斷當前的LoadData是否有LoadPath,這個LoadPath我們下面再講;

這里只需要了解這個類是用來做資源轉換的,比如把資源檔案轉化為Bitmap或者Drawable等等;

當有一個符合的時候,就只會執行一次;

而我們這里最終執行的類是ByteBufferFileLoader;

這個類在loadData執行方法的時候將File物件轉化為位元組資料ByteBuffer;

看一下大致的實作,不必太過深入了解;

image.png

上面我們獲取到了檔案的流資料,那么接下來怎么將其轉化為能展示的bitmap資料呢?

這里就到了解碼的階段了;

上面獲取到的位元組資料ByteBuffer,最侄訓回呼到DecodeJob這個類,在這里面實作了解碼的邏輯;

看一下DecodeJob的onDataFetcherReady方法:

public void onDataFetcherReady(
    Key sourceKey, Object data, DataFetcher<?> fetcher, DataSource dataSource, Key attemptedKey) {
      ...
      decodeFromRetrievedData();
      ...
}

里面呼叫了decodeFromRetrievedData方法來解碼圖片流資料;

這里通過LoadPath類來實作解碼的功能,

4.8、LoadPath

這個LoadPath類里面實作其實并不復雜,里面持有各種抽象的變數,比如Pool,DecodePath,這個類也是用的組合的模式,里面不持有具體的實作;

而是通過組合的類來進行呼叫,將實作抽象出來,讓這個類可以更靈活的使用;

解碼的地方是在這個類的LoadPath的load方法,最終是通過DecodePath的decode方法,具體實作并不在這個類中;

這個DecodePath的設計和LoadPath其實差不多,都是不涉及具體實作,而是通過里面持有的介面來進行呼叫,進而將具體實作抽象到外部,由外部傳入來控制;

來大概看一下這個類的成員變數和構造方法:

在DecodePath的decode方法里面進行解碼,最終呼叫的地方是通過ResourceDecoder的decode方法 來進行解碼;

瞄一下大致的實作;

而這個ResourceDecoder是一個介面,具體實作有很多個類,具體看下面的截圖:

最終的實作是在這些類里面;

看一下大致流程圖:

這些類是在哪里創建的呢?

我們通過上面的構造方法可以知道,這個類是由外部一步步傳進來的;

首先創建LoadPath的時候會將DecodePath傳進來,創建DecodePath的時候,又會把ResourceDecoder傳進來,那么我們可以先從創建LoadPath的地方找起;

首先是在DecodeJob的decodeFromFetcher方法里,通過DecodeHelper的getLoadPath方法;

private <Data> Resource<R> decodeFromFetcher(Data data, DataSource dataSource)
    throws GlideException {
  LoadPath<Data, ?, R> path = decodeHelper.getLoadPath((Class<Data>) data.getClass());
  return runLoadPath(data, dataSource, path);
}

在DecodeHelper的getLoadPath里,可以看到,最終是通過Registry類來獲取LoadPath;

<Data> LoadPath<Data, ?, Transcode> getLoadPath(Class<Data> dataClass) {
  return glideContext.getRegistry().getLoadPath(dataClass, resourceClass, transcodeClass);
}

上面我們講過Registry類,這個類是用來保存資料,提供給外部呼叫的,添加資料的地方是在Glide的構造方法;

下面看一下Registry的getLoadPath方法;

public <Data, TResource, Transcode> LoadPath<Data, TResource, Transcode> getLoadPath(
    @NonNull Class<Data> dataClass,
    @NonNull Class<TResource> resourceClass,
    @NonNull Class<Transcode> transcodeClass) {
  // 從快取里面獲取LoadPath
  LoadPath<Data, TResource, Transcode> result =
      loadPathCache.get(dataClass, resourceClass, transcodeClass);
  if (loadPathCache.isEmptyLoadPath(result)) {
    return null;
  } else if (result == null) {
   // 獲取DecodePath集合
    List<DecodePath<Data, TResource, Transcode>> decodePaths =
        getDecodePaths(dataClass, resourceClass, transcodeClass);
    // It's possible there is no way to decode or transcode to the desired types from a given
    // data class.
    if (decodePaths.isEmpty()) {
      result = null;
    } else {
     // 通過DecodePath來創建LoadPath,并將其保存到快取里,以便下次使用
      result =
          new LoadPath<>(
              dataClass, resourceClass, transcodeClass, decodePaths, throwableListPool);
    }
    loadPathCache.put(dataClass, resourceClass, transcodeClass, result);
  }
  return result;
}

這個方法主要做了三件事:

(1)從快取里面獲取LoadPath,可見優化無處不在,能復用的絕不多次創建;

(2)獲取DecodePath集合;

(3)根據DecodePath來創建LoadPath,并保存到快取里;

這里的實作沒有什么復雜的地方,下面來看看DecodePath的獲取,具體是在Registry的getDecodePaths方法里;

private <Data, TResource, Transcode> List<DecodePath<Data, TResource, Transcode>> getDecodePaths(
    @NonNull Class<Data> dataClass,
    @NonNull Class<TResource> resourceClass,
    @NonNull Class<Transcode> transcodeClass) {
  ...

  for (Class<TResource> registeredResourceClass : registeredResourceClasses) {
    ...

    for (Class<Transcode> registeredTranscodeClass : registeredTranscodeClasses) {

      // 通過Glide構造方法注冊的ResourceDecoder資料來創建DecodePath
      List<ResourceDecoder<Data, TResource>> decoders =
          decoderRegistry.getDecoders(dataClass, registeredResourceClass);
      ResourceTranscoder<TResource, Transcode> transcoder =
          transcoderRegistry.get(registeredResourceClass, registeredTranscodeClass);
      @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
      DecodePath<Data, TResource, Transcode> path =
          new DecodePath<>(
              dataClass,
              registeredResourceClass,
              registeredTranscodeClass,
              decoders,
              transcoder,
              throwableListPool);
      decodePaths.add(path);
    }
  }
  return decodePaths;
}

那么到這里我們就很清晰了,LoadPath和DecodePath都是在獲取的時候創建的,通過Registry類保存的ResourceDecoder資料,先創建了DecodePath,然后再創建了LoadPath;

下面我們來看一下ResourceDecoder這個類的創建是怎么實作的;

看到這里一切就清晰明朗了,ResourceDecoder是Registry類在Glide的構造方法里面創建的,并保存到集合里;

然后在解碼的時候,通過獲取LoadPath,進而獲取到DecodePath,而DecodePath是通過Registry類保存的ResourceDecoder資料來創建的;

看一下大致的流程圖:

上面我們看完了從磁盤快取獲取的邏輯,下面來看看如果從網路獲取圖片資料的;

4.9、SourceGenerator

加載網路快取的地方是在SourceGenerator類的startNext方法,我們來看一下大致的實作;

public boolean startNext() {
  if (dataToCache != null) {
    Object data = dataToCache;
    dataToCache = null;
    // 快取資料
    cacheData(data);
  }

  ...
  loadData = null;
  boolean started = false;
  while (!started && hasNextModelLoader()) {
    loadData = helper.getLoadData().get(loadDataListIndex++);
    if (loadData != null
        && (helper.getDiskCacheStrategy().isDataCacheable(loadData.fetcher.getDataSource())
            || helper.hasLoadPath(loadData.fetcher.getDataClass()))) {
      started = true;
      startNextLoad(loadData);
    }
  }
  return started;
}

這個方法做了兩步,第一步是快取資料,第二步就是從網路下載圖片資源了;

下面這個實作看起來是不是很熟悉,和磁盤快取的邏輯一樣,也是通過ModelLoader來加載資料;

而這個ModelLoader也是通過Registry來獲取的,創建的地方也是通過Registry在Glide的構造方法里進行創建,并快取到快取里;

而這里最終呼叫的ModelLoader是HttpGlideUrlLoader,加載網路資料的地方是在HttpUrlFetcher的loadData方法;

我們來看一下這個方法的實作;

public void loadData(
    @NonNull Priority priority, @NonNull DataCallback<? super InputStream> callback) {
     ...
    InputStream result = loadDataWithRedirects(glideUrl.toURL(), 0, null, glideUrl.getHeaders());
    callback.onDataReady(result);
  } catch (IOException e) {
    ...
    callback.onLoadFailed(e);
  }
  ...

}

最終呼叫的地方是在loadDataWithRedirects方法;

這里面的實作,其實就是Android原生的網路請求,通過創建HttpURLConnection來從網路獲取圖片資料;

獲取成功之后,就會將其保存到磁盤里去;

因為是原生的網路請求,這里其實并沒有做什么網路優化,那么我們可以通過自定義ModelLoader,將Glide的網路請求切換為OKHttp框架,OKHttp我就不詳細介紹了,史上最強網路請求框架!

獲取完成之后,解碼邏輯也是和磁盤快取一樣,也是通過LoadPath來進行解碼,并將其轉化為Bitmap資料;

文章里省略了部分原始碼,建議跟著原始碼走一遍邏輯,可以很好的加深印象;

有一些細節,鑒于文章篇幅,這里就不深入展開探討了;

那么到這里Glide原始碼就講的差不多了,如果有其他想法的歡迎在評論區和我討論;

最后

小編學習提升時,順帶從網上收集整理了一些 Android 開發相關的學習檔案、面試題、Android 核心筆記等等檔案,希望能幫助到大家學習提升,如有需要參考的可以直接去我 CodeChina地址:https://codechina.csdn.net/u012165769/Android-T3 訪問查閱,

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/293211.html

標籤:其他

上一篇:Android學習筆記-UI開發

下一篇:推薦一個直接用于專案開發的PID庫!很好用,很穩定

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more