The article background

This was a memory leak encountered in the project, because it was hidden deep, it took nearly two days to locate and resolve [crying]. Special record its investigation and solution process.

Found the problem

Since most Android applications now need to adapt to the run-time permission check newly added in Android 6.0, Splash usually applies for permission on the Splash screen at the first startup. Most use open source libraries to do this. Company a project using the com. Yanzhenjie. Permission: support: 2.0.1 for permission to apply for. When I checked the Bitmap OOM for problems, I used the profile memory monitoring tool in AndroidStudio and found an image that was too large.

abstract class ThemedResourceCache<T> {
    private ArrayMap<ThemeKey, LongSparseArray<WeakReference<T>>> mThemedEntries;
    private LongSparseArray<WeakReference<T>> mUnthemedEntries;
    private LongSparseArray<WeakReference<T>> mNullThemedEntries;
Copy the code

Weak references are directly reclaimed when GC is found. So the Bitmap should not be in memory, and then it turns out that the entire SplashActivity is not released:

AndPermission.with(this).runtime().permission(permissions)
                .onGranted(data -> {
                    requestReadPhonePermission();
                }).onDenied(permissions1 -> {
            if (AndPermission.hasAlwaysDeniedPermission(SplashActivity.this, permissions1)) {
                Toast.makeText(SplashActivity.this, "Reject", Toast.LENGTH_SHORT).show();
            } else {
                Toast.makeText(SplashActivity.this, "Never be allowed.", Toast.LENGTH_SHORT).show();
            }
        }).start();
Copy the code

Location problem

Then I want to see why the leak, first looked at the source code of this library, the general principle is as follows. All requests are wrapped in a BridgeRequest object that has an activity reference in it and a singleton RequestManager in it, and a thread RequestExecutor in it, so that thread is also singleton. Thread has a queue private final BlockingQueue

mQueue; The thread continuously fetches data from the queue, fetches data, registers a broadcast, and starts a transparent activity: BridgeActivity in bridgeAct application permissions, then the result is also a bridge onRequestPermissionsResult, then sends a broadcast, side to receive radio, upper onCallback callback to give us a call. The core code is as follows:

public class RequestManager {

    private static RequestManager sManager;

    public static RequestManager get() {
        if (sManager == null) {
            synchronized (RequestManager.class) {
                if(sManager == null) { sManager = new RequestManager(); }}}return sManager;
    }

    private final BlockingQueue<BridgeRequest> mQueue;

    private RequestManager() { this.mQueue = new LinkedBlockingQueue<>(); new RequestExecutor(mQueue).start(); } public void add(BridgeRequest request) { mQueue.add(request); }}Copy the code

RequestExecutor:

/**
 * Created by Zhenjie Yan on 2/13/19.
 */
final class RequestExecutor extends Thread implements Messenger.Callback {

    private final BlockingQueue<BridgeRequest> mQueue;
    private BridgeRequest mRequest;
    private Messenger mMessenger;

    public RequestExecutor(BlockingQueue<BridgeRequest> queue) {
        this.mQueue = queue;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (this) {
                try {
                    mRequest = mQueue.take();
                } catch (InterruptedException e) {
                    continue;
                }

                mMessenger = new Messenger(mRequest.getSource().getContext(), this);
                mMessenger.register();
                executeCurrent();

                try {
                    wait(a); } catch (InterruptedException e) { e.printStackTrace(); } } } } private voidexecuteCurrent() {
        switch (mRequest.getType()) {
            case BridgeRequest.TYPE_PERMISSION: {
                Intent intent = new Intent(source.getContext(), BridgeActivity.class);
                intent.putExtra(KEY_TYPE, BridgeRequest.TYPE_PERMISSION);
                intent.putExtra(KEY_PERMISSIONS, permissions);
                source.startActivity(intent);
                break;
            }

        }
    }

    @Override
    public void onCallback() { synchronized (this) { mMessenger.unRegister(); mRequest.getCallback().onCallback(); notify(); }}}Copy the code

Git does not set mRequest to null in the onCallbackl callback. Git does not set mRequest to null in the onCallbackl callback. Git does not set mRequest to null in the onCallbackl callback. (for the record, this issue was fixed in version 2.0.3, but supportV7 and above is no longer compatible with supportV7. AndroidX is only compatible with supportV7. There is no update to replace androidX in the project, so you have to try to fix this issue yourself.)

Find the leak mode

Here are a few ways to detect activity leaks. One. Own registered lifeCycler detection, log output leaked objects. The following code

        registerActivityLifecycleCallbacks(new EmptyActivityLifecycleCallbacks() {

            @Override
            public void onActivityDestroyed(Activity activity) {
                super.onActivityDestroyed(activity);

                ReferenceQueue<Activity> refer=new ReferenceQueue<>();
                WeakReference<Activity> weak=new WeakReference<>(activity,refer);
                new Thread(){
                    @Override
                    public void run() {
                        super.run();
                        while (true){
                            try {
                                Activity act=weak.get();
                                Reference<Activity> refe= (Reference<Activity>) refer.poll();

                                Log.d(TAG,"weak ="+ act+""+refe);
                                if(act==null && refe==null){
                                    return; } act=null; Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }.start(); }});Copy the code

2. Leakcanary. Output leak source three. Look in the androidProfile for the activity object.

Leakcanary detects a memory leak using a weak reference after onDestory. Normally, after GC, the object in the reference will be empty. After a period of time, the object will still be found, which is considered a leak, and then dump the memory, analyze the reference chain, and output a report. If you want to verify that a page is leaking, logging is faster.

Try to solve

Ok, let’s leave the two mRequest and mMessenger empty in onCallback ourselves. I can think of two ways,

  • With AspectJ, the AOP framework nulls this variable with reflection after the onCallback is executed.

  • Copy the source code for project 2.0.1 and modify it directly.

It looks as if the problem is about to be solved. If that were true, there would be no need to record it. Here the first use of AOP, found not, and then used the direct change to the source code, found that there is a leak…

Either way, the object does exist here, and we go straight to the report from Leakcanary.

Visibility between threads

JAVA multithreaded visibility

Invisibility between threads causes problems when we operate on the same variable in multiple threads, like when two threads add variable I at the same time, the number does not equal the sum, as you probably know.

Let’s do a little Java experiment

public class JavaTest {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start();

        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t.reset();
        System.out.println("main t=" + t.p2);
    }

    static class MyThread extends Thread {
        private Person p2;

        public MyThread() {
            this.p2 = new Person();
        }
        @Override
        public void run() {
            super.run();
            while(p2 ! = null) { // System.out.println("MyThread " + p2);
            }
        }
        public void reset() { p2 = null; }}}Copy the code

In this experiment, the child thread MyThread has a variable, and the child thread passes p2! =null does a loop check. After 100 ms, the main thread calls the reset method of the child thread to reset p2, and the child thread theoretically terminates the loop.

System.out.println("main 2=" + p + " t=" + t.p2);
onCallback()

The main thread has a copy of this variable, and the variable in the child thread has not been updated from main memory. Therefore, it is not null for the child thread. The solution is to make the variable volatile. Causes child threads to update immediately from main memory. With volatile:

To solve the problem

The other solution is to use the system’s thread pool and use the RequestExecutor as a runnable. The thread dispatches the runnable, the run callback completes the runnable, the runnable object is released, and the internal properties are also released.

As follows:

public class RequestManagerFix {
    private static RequestManagerFix sManager;
    public static RequestManagerFix get() {
        if (sManager == null) {
            synchronized (RequestManagerFix.class) {
                if(sManager == null) { sManager = new RequestManagerFix(); }}}return sManager;
    }
    private final Executor mExecutor;
    private RequestManagerFix() {
        this.mExecutor = Executors.newCachedThreadPool();
    }
    public void add(BridgeRequest request) {
        mExecutor.execute(new RequestExecutorFix(request));
    }
}

final class RequestExecutor implements Messenger.Callback, Runnable {

    private BridgeRequest mRequest;
    private Messenger mMessenger;

    public RequestExecutor(BridgeRequest queue) {
        this.mRequest = queue;
    }
    @Override
    public void run() {
        mMessenger = new Messenger(mRequest.getSource().getContext(), this);
        mMessenger.register();
        executeCurrent();
    }
    private void executeCurrent() {... } @Override public voidonCallback() { synchronized (this) { mMessenger.unRegister(); mRequest.getCallback().onCallback(); mRequest = null; mMessenger = null; }}}Copy the code

All the original RequestExecutor does is act like a thread pool, and there’s really no need to write your own thread pool.

The accompanying threading problem

In my testing process, I wrote this set of code to check whether the activity exists, the code is as follows

        registerActivityLifecycleCallbacks(new EmptyActivityLifecycleCallbacks() {

            @Override
            public void onActivityDestroyed(Activity activity) {
                super.onActivityDestroyed(activity);

                ReferenceQueue<Activity> refer=new ReferenceQueue<>();
                WeakReference<Activity> weak=new WeakReference<>(activity,refer);
                new Thread(){
                    @Override
                    public void run() {
                        super.run();
                        while (true){
                            try {
                                Activity act=weak.get();
                                Reference<Activity> refe= (Reference<Activity>) refer.poll();

                                Log.d(TAG,"weak ="+ act+""+refe);
                                if(act==null && refe==null){
                                    return; } act=null; Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }.start(); }});Copy the code

If the act = null; If I comment out this line of code, guess what happens? Try it out for yourself. An activity cannot be released. Why?

Explanation:

If this line is removed, then Activity act=weak.get(); This line of code will have a child reference to the activity object, and then sleep for a second. During this process, even if the main thread has no references, GC occurs, and the object is found to have references, so it will not be released. When sleep ends, the object is immediately removed from the weak reference and the reference is created again. So the object can never be freed, so act=null, this line of code has to be added. This is a multi-threaded reference problem.

conclusion

Although we in other time more or less have learned the problem between threads, such as visibility and so on, but in the encounter with practical problems, but do not often go to this aspect to think, combined with practical problems, to remember better, through this problem, but also to the knowledge of the previous thread review.

Git git git git Git Git Git Git Git Git Git Git Git Git Git Git Git Git Git Git