Hello, everyone. I’m little little deer, a vegetable chicken Android app ape. Recently I have been reading the Glide source code, today we are going to study the part of Glide RequestManager life cycle management. This article should be Glide life cycle management. But in the source code reading, I found that my previous project for Glide there are some memory leaks, so I temporarily decided to change the name of the article, hoping to attract attention.

So this is our main screen style

Use the bottom row of tabs to control the primary fragment of the main interface. The primary fragment contains several sub-fragments, and the fragment contains other views. Take RecyclerView as an example. Context objects are passed when the corresponding Adapter is created. When you load

Glide.with(context).load(“path”).into (imagerView) Doing this may cause memory leaks.

The following formal analysis of the principle of memory leakage caused by improper use of Glide.

Glide life cycle

As an Android developer, when it comes to the life cycle, the first thing that comes to mind should be the activity life cycle. The activity lifecycle is a set of template methods given to us by android system developers. We only need to implement the corresponding business logic in the corresponding methods. So how does Glide’s life cycle come about?

Glide Life The life cycle is divided into two main parts:

  1. Activity/Fragment lifecycle method calls that affect all requests on the entire page.
  2. Network state changes cause all requests managed by the entire requestManager to change.

Page management

The Glide#with method returns a RequestManager object, which is actually called in the RequestManagerRetriever#get retriever.

RequestManagerRetriever is used to create a new RequestManager or retrieve an existing one from activities and fragments.

The construction of RequestManagerRetriever

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

Its factory is passed in by Glide and defaults to null if we don’t configure it. Create it using 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); }};Copy the code

RequestManagerRetriever Obtains the corresponding RequestManager

The parameters passed in RequestManagerRetriever#get have the following classes.

  1. The Context will try to convert it to the corresponding activity otherwise it will get the Application-level RequestManager
  2. Activity/ Fragment RequestManagerRetriever Attempts to obtain an invisible sub-fragment from their FragmentManager or create a new one if the retriever fails. Add it to the activity/fragment.
  3. View When a View is passed in, the activity is first retrieved. If not, use the Application-level RequestManager. If you get an Activity, it checks to see if the current View is in an activity. If you are using the fragment to get the corresponding ReauestManager, if not, use the Activity’s RequestManager.

It is important to note that no matter what parameters are passed, the image load in the child thread uses the Application-level RequestManager uniformly.

The RequestManagerRetriever#get(View View) is used to illustrate the process

@nonnull public RequestManager get(@nonnull View View) { Using Application level of RequestManager if (Util. IsOnBackgroundThread () {return get (the getContext () getApplicationContext ());  } / / not empty judgment Preconditions. CheckNotNull (view); Preconditions.checkNotNull(view.getContext(), "Unable to obtain a request manager for a view without a Context"); Activity Activity = findActivity(view.getContext()); If (Activity == null) {return get(view.getContext().getApplicationContext()); } // Check whether the view belongs to a fragment or not. 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); }Copy the code

Use the Activity to find the fragment of the current View

private android.app.Fragment findFragment(@NonNull View target, @nonnull Activity Activity) {ArrayMap tempViewToFragment.clear(); // Add all fragments to the tempViewToFragment recursively findAllFragmentsWithViews(activity.getFragmentManager(), tempViewToFragment); android.app.Fragment result = null; View activityRoot = activity.findViewById(android.R.id.content); View current = target; // Continue to compare until the current view is contentView stop searching for fragment while (! current.equals(activityRoot)) { result = tempViewToFragment.get(current); // Exit the loop if (result! = null) { break; } if (current.getParent() instanceof View) { current = (View) current.getParent(); } else { break; } } tempViewToFragment.clear(); return result; }Copy the code

Obtain the RequestManager using the fragment

RequestManagerRetriever#get(Fragment Fragment) will call supportFragmentGet to obtain the RequestManager.

@NonNull private RequestManager supportFragmentGet( @NonNull Context context, @NonNull FragmentManager fm, @Nullable Fragment parentHint, Boolean isParentVisible) {/ / get the current under FragmentManager SupportRequestManagerFragment within getSupportRequestManagerFragment, If there is no corresponding fragment, it is added. SupportRequestManagerFragment current = getSupportRequestManagerFragment(fm, parentHint, isParentVisible); RequestManager requestManager = current.getRequestManager(); / / the current SupportRequestManagerFragment no RequestManager then create a RequestManager binding with its life cycle. if (requestManager == null) { Glide glide = Glide.get(context); requestManager = factory.build( glide, current.getGlideLifecycle(), current.getRequestManagerTreeNode(), context); current.setRequestManager(requestManager); } return requestManager; }Copy the code

The build process of the RequestManager

RequestManager has two constructors, but ultimately executes the following one.

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; Network listening is Glide's default implementation, we can also specify factory to implement some other business logic. // All failed requests will be retried when the network is connected. connectivityMonitor = factory.build( context.getApplicationContext(), new RequestManagerConnectivityListener(requestTracker)); / / implementation if (Util. IsOnBackgroundThread ()) {mainHandler. Post (addSelfToLifecycle); } else { lifecycle.addListener(this); } lifecycle. AddListener (connectivityMonitor); defaultRequestListeners = new CopyOnWriteArrayList<>(glide.getGlideContext().getDefaultRequestListeners()); setRequestOptions(glide.getGlideContext().getDefaultRequestOptions()); / / add the current RequestManager to Glide convenient manage unified Glide. RegisterRequestManager (this); }Copy the code

Network monitoring

In the constructor, create connectivityMonitor, pass requestTracker RequestManagerConnectivityListener. His implementation is as follows:

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 connected) {if (isConnected) {// The connection will restart. synchronized (RequestManager.this) { requestTracker.restartRequests(); }}}}Copy the code

RequestManager#onDestory

Override public synchronized void onDestroy() {Override public synchronized void onDestory targetTracker.ondestroy (); // Notify each target to clear for (target <? > target : targetTracker.getAll()) { clear(target); } // Clear the collection targettracker.clear (); / / remove the current requestManager management request requestTracker. ClearRequests (); lifecycle.removeListener(this); lifecycle.removeListener(connectivityMonitor); mainHandler.removeCallbacks(addSelfToLifecycle); glide.unregisterRequestManager(this); }Copy the code

Target cleanup process

The clear RequestManger (Target <? > target) will use untrackOrDelegate internally

private void untrackOrDelegate(@NonNull Target<? > target) {// Clear the corresponding request and reset the request into the object reuse pool. boolean isOwnedByUs = untrack(target); // If the current target is not managed by the target, the target will be traversed through all the RequestManagers to find the appropriate requestManager for processing. if (! isOwnedByUs && ! glide.removeFromManagers(target) && target.getRequest() ! = null) { Request request = target.getRequest(); target.setRequest(null); request.clear(); }}Copy the code

Request Cleanup process

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; } the if (requestTracker clearRemoveAndRecycle (request)) {/ / removed from the corresponding collection 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; } // If the request can be removed successfully from the collection, the request belongs to the current RequestManager Boolean isOwnedByUs = requests. Remove (request); // Avoid short circuiting. isOwnedByUs = pendingRequests.remove(request) || isOwnedByUs; Request.clear () {if (isOwnedByUs) {// Execute request.clear to change the request state and notify request.clear(); If (isSafeToRecycle) {if (isSafeToRecycle) {if (isSafeToRecycle) {if (isSafeToRecycle) {if (isSafeToRecycle) { SingleRequest request.recycle(); } } return isOwnedByUs; }Copy the code

Does Glide really not leak memory?

Glide’s life cycle is known to suspend and reclaim related requests and cut off network request callback references when life-related activities/fragments are destroyed. So does Glide really prevent memory leaks entirely?

This leads me to the conclusion that Normally using Glide does not cause memory leaks in activities, fragments, and Views. But memory leaks can occur if Glide is used improperly. For example, use Glide#with to pass an activity object in the Fragment. The reason is that when the Fragment ends, the Activity several times does not receive the corresponding lifecycle method from the RequestManager.

Experimental results show that:

Modify the ModelLoader we wrote on Glide data INPUT and output to load the audio cover so that the thread sleeps for 300 seconds when it encounters that particular audio

@Override public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super ByteBuffer> callback) { try { Log.d(TAG,"loadData assetPath "+assetPath); AssetFileDescriptor fileDescriptor = assetManager.openFd(assetPath); If (assetPath.contains("DuiMianDeNvHaiKanGuoLai-- renxianqi.mp3 ")){systemclock. sleep(10*30*1000); // Sleep 300s} mediaMetadataRetriever.setDataSource(fileDescriptor.getFileDescriptor(),fileDescriptor.getStartOffset(),fileDescriptor.g etDeclaredLength()); 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); }}Copy the code

Add a Fragment to the Activity. When the page is successfully created, pass the Activity /context object with Glide#with and remove the Fragment from the Activity.

Force-associate the root View of the fragment with the fragment. LeakCanary can be used to detect memory leaks.

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); 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")).diskCacheStrat egy(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); }}Copy the code

Experimental results:

As you can see here, the Fragment is leaking memory because it is held by the View. This is a reflection of the memory leak that can occur when Glide is used improperly. Resolution: Pass the correct argument to with. Or call ViewTarget#clearOnDetach. I have not used clearOnDetach. According to Glide note, this is a set of experimental apis that may be removed later.

Summary Glide use matters needing attention

The Glide#with method uses the priority of the argument

fragment > view > activity > application

The view and activity pass the activity first when they know clearly that the page they are using is an activity, because the view will loop through many times to find the fragment and activity. Proper use of Glide can prevent memory leaks caused by Glide.

Glide RequestOptions can be divided into three levels:

  1. The application level can be configured globally
  2. The page-level Activty/Fragment can be customized for each particular page, working with the RequestManager
  3. A single request works on the RequestBuilder to build request configuration items for each request

How does Glide guarantee that images will load correctly

ViewTarget#setRequest will call the View’s setTag to put the request object in the View. The request is made through ViewTarget#getRequest, and if it returns the same as the previous request, the original request is used, otherwise the original request is cleared.

Use caution with application loading and image loading in child threads unless you know their usage scenarios fit your business.