看完這篇,你確定你的Glide不會發生記憶體洩漏嗎?

語言: CN / TW / HK

hello:大家好我是 小小小小小鹿,一枚菜雞Android程式猿。最近正在閱讀Glide原始碼,今天我們要研究的部分是Glide RequestManager 生命週期管理。 本來這個也是這篇文章應該是Glide生命週期管理。但是在原始碼閱讀中我發現原來我以前的專案對於Glide的使用存在著一些記憶體洩漏的可能,因此臨時決定更改了文章的名字,希望能夠引起大家的重視。

這個是我們的主介面樣式

介面樣式.png 通過最下面的一排選項卡,控制主介面的一級fragment ,一級Fragment下面又有若干的子Fragment,fragment又包含一些其它的View。以RecyclerView舉例,在對應的Adapter建立的時候會傳遞Context物件。載入的時候

Glide.with(context).load("path").into(imagerView) 這樣做會存在記憶體洩漏的可能。

下面正式分析內因為Glide使用不當造成記憶體洩漏的原理。

Glide生命週期

作為一個android開發者,說到生命週期,最先想到的應該是activity的生命週期了吧。activity的生命週期是android系統開發者給我們設定的一些模板方法,我們只需要在對應的方法中實現對應的業務邏輯即可。那麼Glide的生命週期是怎麼來的呢?

Glide生命生命週期主要分為兩個:

  1. activity/fragment 生命週期方法呼叫,影響到整個頁面所有請求。
  2. 網路狀態變化引起整個requestManager 所管理的所有請求發生改變。

頁面管理

Glide#with方法返回的是一個RequestManager物件,而RequestManager的獲取實際上都呼叫了RequestManagerRetriever#get來獲取RequestManager物件的。

RequestManagerRetriever用於建立新的 RequestManager 或從Activity和Fragment中檢索現有的。

RequestManagerRetriever的構建

public RequestManagerRetriever(@Nullable RequestManagerFactory factory) {  this.factory = factory != null ? factory : DEFAULT_FACTORY;  handler = new Handler(Looper.getMainLooper(), this /* Callback */); }

它的factory由Glide傳遞過來,如果我們不進行配置預設為空。就是使用DEFAULT_FACTORY進行建立

private static final RequestManagerFactory DEFAULT_FACTORY = new RequestManagerFactory() {  @NonNull  @Override  public RequestManager build(@NonNull Glide glide, @NonNull Lifecycle lifecycle,      @NonNull RequestManagerTreeNode requestManagerTreeNode, @NonNull Context context) {    return new RequestManager(glide, lifecycle, requestManagerTreeNode, context); } };

RequestManagerRetriever獲取對應的RequestManager

RequestManagerRetriever#get傳遞的引數有下面幾類。

  1. Context 會嘗試將其轉換成對應的activity否則獲取的是Application 級別的RequestManager
  2. Activity/fragment RequestManagerRetriever會嘗試通過他們的FragmentManager獲取一個不可見的子fragment,如果沒有獲取成功則新建一個。並新增到activity/fragment中。
  3. View 當傳遞一個View進來的時候,會先獲取對應的activity。如果獲取不到則直接使用Application級別的RequestManager,如果獲取到了activity,會檢視當前View是否在某一個activity中,如果在使用fragment獲取對應的ReauestManager 如果不在則使用Activity的RequestManager。

需要特別注意的是:不論傳遞什麼引數,在子執行緒進行圖片載入都會統一使用Application級別的RequestManager。

這裡以RequestManagerRetriever#get(View view)來說明其流程

@NonNull public RequestManager get(@NonNull View view) {    //如果在子執行緒,使用Application級別的RequestManager  if (Util.isOnBackgroundThread()) {    return get(view.getContext().getApplicationContext()); } //進行非空判斷  Preconditions.checkNotNull(view);  Preconditions.checkNotNull(view.getContext(),      "Unable to obtain a request manager for a view without a Context");  //查詢對應的Activity  Activity activity = findActivity(view.getContext()); //如果activity為空則直接使用  if (activity == null) {    return get(view.getContext().getApplicationContext()); } ​  //查詢到view所屬的fragment,則使用fragment 查詢不到則使用activity。  if (activity instanceof FragmentActivity) {    Fragment fragment = findSupportFragment(view, (FragmentActivity) activity);    return fragment != null ? get(fragment) : get(activity); } ​  // Standard Fragments.  android.app.Fragment fragment = findFragment(view, activity);  if (fragment == null) {    return get(activity); }  return get(fragment); }

通過activity查詢當前View所屬的fragment

private android.app.Fragment findFragment(@NonNull View target, @NonNull Activity activity) {    //是以View作為key Fragment作為value的ArrayMap  tempViewToFragment.clear();    //將所有的fragment 包含activity下的fragment和fragment中的子Fragment    //通過遞迴的方式全部新增到 tempViewToFragment  findAllFragmentsWithViews(activity.getFragmentManager(), tempViewToFragment); ​  android.app.Fragment result = null; ​  View activityRoot = activity.findViewById(android.R.id.content);  View current = target;  //不斷對比直到當前的view為contentView 則停止查詢fragment  while (!current.equals(activityRoot)) {    result = tempViewToFragment.get(current);      //查詢到了對應的fragment,退出當前迴圈    if (result != null) {      break;   }    if (current.getParent() instanceof View) {      current = (View) current.getParent();   } else {      break;   } }  tempViewToFragment.clear();  return result; }

通過fragment獲取RequestManager

RequestManagerRetriever#get(Fragment fragment) 會呼叫supportFragmentGet來獲取RequestManager。

@NonNull  private RequestManager supportFragmentGet(      @NonNull Context context,      @NonNull FragmentManager fm,      @Nullable Fragment parentHint,      boolean isParentVisible) {      //獲取當前FragmentManager 下的SupportRequestManagerFragment 在getSupportRequestManagerFragment內部,如果沒有對應的fragment,會為其新增。    SupportRequestManagerFragment current =        getSupportRequestManagerFragment(fm, parentHint, isParentVisible);    RequestManager requestManager = current.getRequestManager();     //當前SupportRequestManagerFragment 沒有RequestManager 則建立一個RequestManager與其生命週期繫結。    if (requestManager == null) {           Glide glide = Glide.get(context);      requestManager =          factory.build(              glide, current.getGlideLifecycle(), current.getRequestManagerTreeNode(), context);      current.setRequestManager(requestManager);   }    return requestManager; }

RequestManager的構建過程

RequestManager有兩個構造方法,但是最終都會執行下面這個。

RequestManager(    Glide glide,    Lifecycle lifecycle,    RequestManagerTreeNode treeNode,    RequestTracker requestTracker,    ConnectivityMonitorFactory factory,    Context context) {  this.glide = glide;  this.lifecycle = lifecycle;  this.treeNode = treeNode;  this.requestTracker = requestTracker;  this.context = context; //建立一個網咯變化的監聽 網路監聽是Glide預設實現的,我們也可以通過指定factory實現其它的一些業務邏輯。 //當網路連線上後會將所有請求失敗的重新嘗試。  connectivityMonitor =      factory.build(          context.getApplicationContext(),          new RequestManagerConnectivityListener(requestTracker)); ​  //實現  if (Util.isOnBackgroundThread()) {    mainHandler.post(addSelfToLifecycle); } else {    lifecycle.addListener(this); }  //將網路變化的監聽與生命週期進行繫結  lifecycle.addListener(connectivityMonitor); ​  defaultRequestListeners =      new CopyOnWriteArrayList<>(glide.getGlideContext().getDefaultRequestListeners());  setRequestOptions(glide.getGlideContext().getDefaultRequestOptions()); //將當前RequestManager新增到Glide方便統一進行管理  glide.registerRequestManager(this); }

網路監聽

在構造方法中,建立connectivityMonitor的時候,將requestTracker傳遞給了RequestManagerConnectivityListener。他的實現如下:

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();     }   } } }

RequestManager#onDestory

@Override public synchronized void onDestroy() {    //通知所有target呼叫onDestory    targetTracker.onDestroy();    //通知每一個target 進行清除  for (Target<?> target : targetTracker.getAll()) {    clear(target); }    //清除集合  targetTracker.clear();    //清除當前requestManager管理的request  requestTracker.clearRequests();  lifecycle.removeListener(this);  lifecycle.removeListener(connectivityMonitor);  mainHandler.removeCallbacks(addSelfToLifecycle);  glide.unregisterRequestManager(this); }

target的清除過程

RequestManger的clear(Target<?> target)會內用untrackOrDelegate

private void untrackOrDelegate(@NonNull Target<?> target) {  //清除對應的request,並對request進行重置,放進物件重用池。  boolean isOwnedByUs = untrack(target); //如果當前的target不歸自己管理,會遍歷所有的requestManager查詢到合適的requestManager進行處理。  if (!isOwnedByUs && !glide.removeFromManagers(target) && target.getRequest() != null) {    Request request = target.getRequest();    target.setRequest(null);    request.clear(); } }

request清除過程

synchronized boolean untrack(@NonNull Target<?> target) {  Request request = target.getRequest();  // If the Target doesn't have a request, it's already been cleared.  if (request == null) {    return true; } ​  if (requestTracker.clearRemoveAndRecycle(request)) {    //從對應的集合中移除    targetTracker.untrack(target);    target.setRequest(null);    return true; } else {    return false; } } ​ public boolean clearRemoveAndRecycle(@Nullable Request request) {    return clearRemoveAndMaybeRecycle(request, /*isSafeToRecycle=*/ true); } ​  private boolean clearRemoveAndMaybeRecycle(@Nullable Request request, boolean isSafeToRecycle) {     if (request == null) {      return true;   }    //如果能夠從集合中移除成功,那麼這個request歸屬當前的RequestManager管理    boolean isOwnedByUs = requests.remove(request);    // Avoid short circuiting.    isOwnedByUs = pendingRequests.remove(request) || isOwnedByUs;    if (isOwnedByUs) {      //執行request.clear 對request狀態進行轉變,和做相應的通知      request.clear();      if (isSafeToRecycle) {        //這個是不會進行記憶體洩漏的關鍵,將request對應的引用回撥置空,切斷引用關係。        //對應程式碼可以參考SingleRequest        request.recycle();     }   }    return isOwnedByUs; }

Glide真的不會發生記憶體洩漏嗎?

前面我們梳理了Glide的生命週期,知道在生命相關的activity/Fragment銷燬的時候會暫停和回收相關的請求,並且切斷網路請求回撥的引用。那麼Glide是不是真的能夠完全避免記憶體內洩漏呢?

這個直接給出我的結論:正常情況下使用Glide不會造成內Activity、Fragment、View記憶體洩漏。但是如果Glide使用不當是可能造成記憶體洩漏的。比如在Fragment使用Glide#with傳遞activity物件。 原因是Fragment結束的時候,Activity幾倍RequestManager並沒有接收到相應的生命週期方法。

實驗證明:

改造我們在Glide資料輸入輸出編寫的載入音訊封面的ModelLoader,當遇到特定該音訊的時候執行緒休眠300秒

@Override public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super ByteBuffer> callback) {    try {        Log.d(TAG,"loadData assetPath "+assetPath);        AssetFileDescriptor fileDescriptor = assetManager.openFd(assetPath);        //特定路徑 休眠300s 模擬網路載入緩慢        if(assetPath.contains("DuiMianDeNvHaiKanGuoLai--RenXianQi.mp3")){            SystemClock.sleep(10*30*1000);//休眠300s       }        mediaMetadataRetriever.setDataSource(fileDescriptor.getFileDescriptor(),fileDescriptor.getStartOffset(),fileDescriptor.getDeclaredLength());        byte[] bytes = mediaMetadataRetriever.getEmbeddedPicture();        if(bytes == null){            callback.onLoadFailed(new FileNotFoundException("the file not pic"));            return;       }        ByteBuffer buf = ByteBuffer.wrap(bytes);        Log.d(TAG,"loadData assetPath "+assetPath +" success");        callback.onDataReady(buf);   } catch (IOException e) {        e.printStackTrace();        callback.onLoadFailed(e);   } }

在Activity中新增一個Fragment,當頁面建立成功後,使用Glide#with傳遞activity/context物件,並在activity中移除該Fragment。

將fragment的根View與fragment強制關聯。方便利用LeakCanary進行記憶體洩漏檢測。

public static class MyTestFragment extends Fragment {    ImageView imageView;    @Nullable    @Override    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {        View root = inflater.inflate(R.layout.fragment_glide_source_test,container,false);        imageView = root.findViewById(R.id.imageView);        //強制保留引用關係 方便進行檢測        root.setTag(this);        Log.d(TAG,"onCreateView finish");        return root;   } ​    @Override    public void onCreate(@Nullable Bundle savedInstanceState) {        super.onCreate(savedInstanceState);   } ​    @Override    public void onActivityCreated(@Nullable Bundle savedInstanceState) {        super.onActivityCreated(savedInstanceState);        Glide.with(getActivity()).load(Uri.parse("file:///android_asset/DuiMianDeNvHaiKanGuoLai--RenXianQi.mp3")).diskCacheStrategy(DiskCacheStrategy.NONE).into(imageView);        Log.d(TAG,"onActivityCreated load ");        imageView.postDelayed(new Runnable() {            @Override            public void run() {                Log.d(TAG,"onActivityCreated remove ");                getActivity().getSupportFragmentManager().beginTransaction().remove(MyTestFragment.this).commit();           }       },300); ​   } ​    @Override    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {        super.onViewCreated(view, savedInstanceState);   } }

實驗結果:

Glide記憶體洩漏.jpg

可以看這裡因為Fragment被View持有導致了Fragment記憶體洩漏。這個也就反應了當Glide使用不當,會導致View的記憶體洩漏。 解決:傳遞正確的引數給with。或者呼叫ViewTarget#clearOnDetach。我沒有使用過clearOnDetach 根據Glide註釋,這個是一組實驗性api,後續可能會被移除。

小結Glide使用注意事項

Glide#with方法在引數使用優先順序

fragment > view > activity > application

其中view和activity 在明確知道當前使用的頁面是activity優先傳遞activity 因為view會通過多次迴圈遍歷查詢fragment、activity。正確的使用Glide可以避免因為Glide造成記憶體洩漏。

Glide RequestOptions 可以分為三個級別:

  1. 應用級 可以進行全域性配置
  2. 頁面級別 activty/fragment 可以為每一個特殊的頁面進行定製化處理,作用於RequestManager
  3. 單個請求 作用於RequestBuilder 為每一個請求構建請求配置項

Glide如何保證圖片的載入不會出現錯亂

ViewTarget#setRequest會呼叫View的setTag 將request請求物件放在View中。在請求的時候會通過ViewTarget#getRequest,如果返回的與前一個請求一致則使用原來的請求,否則清除原來的請求。

對於使用application載入和在子執行緒進行圖片載入,需要謹慎使用,除非你明確他們的使用場景與自身的業務契合。