How can an AsyncTask still use an Activity if the user has already navigated away from it?

后端 未结 1 1250
刺人心
刺人心 2020-12-13 07:52

On Android you can do work in a separate Thread for example by using a Runnable or AsyncTask. In both cases you might need to do some

相关标签:
1条回答
  • 2020-12-13 08:15

    Simple answer: You have just discovered

    Memory Leaks

    As long as some part of the app like an AsyncTask still holds a reference to the Activity it will not be destroyed. It will stick around until the AsyncTask is done or releases its reference in some other way. This can have very bad consequences like your app crashing, but the worst consequences are the ones you don't notice: your app may keep reference to Activities which should have been released ages ago and each time the user does whatever leaks the Activity the memory on the device might get more and more full until seemingly out of nowhere Android kills your app for consuming too much memory. Memory leaks are the single most frequent and worst mistakes I see in Android questions on Stack Overflow


    The solution

    Avoiding memory leaks however is very simple: Your AsyncTask should never have a reference to an Activity, Service or any other UI component.

    Instead use the listener pattern and always use a WeakReference. Never hold strong references to something outside the AsyncTask.


    A few examples

    Referencing a View in an AsyncTask

    A correctly implemented AsyncTask which uses an ImageView could look like this:

    public class ExampleTask extends AsyncTask<Void, Void, Bitmap> {
    
        private final WeakReference<ImageView> mImageViewReference;
    
        public ExampleTask(ImageView imageView) {
            mImageViewReference = new WeakReference<>(imageView);
        }
    
        @Override
        protected Bitmap doInBackground(Void... params) {
            ...
        }
    
        @Override
        protected void onPostExecute(Bitmap bitmap) {
            super.onPostExecute(bitmap);
    
            final ImageView imageView = mImageViewReference.get();
            if (imageView != null) {
                imageView.setImageBitmap(bitmap);
            }
        }
    }
    

    This illustrates perfectly what a WeakReference does. WeakReferences allow for the Object they are referencing to be garbage collected. So in this example We create a WeakReference to an ImageView in the constructor of the AsyncTask. Then in onPostExecute() which might be called 10 seconds later when the ImageView does not exist anymore we call get() on the WeakReference to see if the ImageView exists. As long as the ImageView returned by get() is not null then the ImageView has not been garbage collected and we can therefore use it without worry! Should in the meantime the user quit the app then the ImageView becomes eligible for garbage collection immediately and if the AsyncTask finishes some time later it sees that the ImageView is already gone. No memory leaks, no problems.


    Using a listener

    public class ExampleTask extends AsyncTask<Void, Void, Bitmap> {
    
        public interface Listener {
            void onResult(Bitmap image);
        }
    
        private final WeakReference<Listener> mListenerReference;
    
        public ExampleTask(Listener listener) {
            mListenerReference = new WeakReference<>(listener);
        }
    
        @Override
        protected Bitmap doInBackground(Void... params) {
            ...
        }
    
        @Override
        protected void onPostExecute(Bitmap bitmap) {
            super.onPostExecute(bitmap);
    
            final Listener listener = mListenerReference.get();
            if (listener != null) {
                listener.onResult(bitmap);
            }
        }
    }
    

    This looks quite similar because it actually is quite similar. You can use it like this in an Activity or Fragment:

    public class ExampleActivty extends AppCompatActivity implements ExampleTask.Listener {
    
        private ImageView mImageView;
    
        ...
    
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            ...
    
            new ExampleTask(this).execute();
        }
    
        @Override
        public void onResult(Bitmap image) {
            mImageView.setImageBitmap(image);
        }
    } 
    

    Or you can use it like this:

    public class ExampleFragment extends Fragment {
    
        private ImageView mImageView;
    
        private final ExampleTask.Listener mListener = new ExampleTask.Listener() {
    
            @Override
            public void onResult(Bitmap image) {
                mImageView.setImageBitmap(image);   
            }
        };
    
        @Override
        public void onViewCreated(View view, Bundle savedInstanceState) {
            super.onViewCreated(view, savedInstanceState);
    
            new ExampleTask(mListener).execute(); 
        }
    
        ...
    }
    

    WeakReference and its consequences when using a listener

    However there is another thing you have to be aware of. A consequence of only having a WeakReference to the listener. Imagine you implement the listener interface like this:

    private static class ExampleListener implements ExampleTask.Listener {
    
        private final ImageView mImageView;
    
        private ExampleListener(ImageView imageView) {
            mImageView = imageView;
        }
    
        @Override
        public void onResult(Bitmap image) {
            mImageView.setImageBitmap(image);
        }
    }
    
    public void doSomething() {
       final ExampleListener listener = new ExampleListener(someImageView);
       new ExampleTask(listener).execute();
    }
    

    Quite an unusual way to do this - I know - but something similar might sneak into your code somewhere without you knowing it and the consequences can be difficult to debug. Have you noticed by now what might be wrong with the above example? Try figuring out, otherwise continue reading below.

    The problem is simple: You create an instance of the ExampleListener which contains your reference to the ImageView. Then you pass it into the ExampleTask and start the task. And then the doSomething() method finishes, so all local variables become eligible for garbage collection. There is no strong reference left to the ExampleListener instance you passed into the ExampleTask, there is just a WeakReference. So the ExampleListener will be garbage collected and when the ExampleTask finishes nothing will happen. If the ExampleTask executes fast enough the garbage collector might not have collected the ExampleListener instance yet, so it may work some of the time or not at all. And debugging issues like this can be a nightmare. So the moral of the story is: Always be aware of your strong and weak references and when objects become eligible for garbage collection.


    Nested classes and using static

    Another thing which probably is the cause of most memory leaks I see on Stack Overflow people using nested classes in the wrong way. Look at the following example and try to spot what causes a memory leak in the following example:

    public class ExampleActivty extends AppCompatActivity {
    
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            ...
    
            final ImageView imageView = (ImageView) findViewById(R.id.image);
            new ExampleTask(imageView).execute();
        }
    
        public class ExampleTask extends AsyncTask<Void, Void, Bitmap> {
    
            private final WeakReference<ImageView> mListenerReference;
    
            public ExampleTask(ImageView imageView) {
                mListenerReference = new WeakReference<>(imageView);
            }
    
            @Override
            protected Bitmap doInBackground(Void... params) {
                ...
            }
    
            @Override
            protected void onPostExecute(Bitmap bitmap) {
                super.onPostExecute(bitmap);
    
                final ImageView imageView = mListenerReference.get();
                if (imageView != null) {
                    imageView.setImageAlpha(bitmap);
                }
            }
        }
    }
    

    Do you see it? Here is another example with the exact same problem, it just looks different:

    public class ExampleActivty extends AppCompatActivity {
    
    
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            ...
    
            final ImageView imageView = (ImageView) findViewById(R.id.image);
            final Thread thread = new Thread() {
    
                @Override
                public void run() {
                    ...
                    final Bitmap image = doStuff();
                    imageView.post(new Runnable() {
                        @Override
                        public void run() {
                            imageView.setImageBitmap(image);
                        }
                    });
                }
            };
            thread.start();
        }
    }
    

    Have you figured out what the problem is? I daily see people carelessly implementing stuff like above probably without knowing what they are doing wrong. The problem is a consequence of how Java works based on a fundamental feature of Java - there is no excuse, people who implement things like above are either drunk or don't know anything about Java. Let's simplify the problem:

    Imagine you have a nested class like this:

    public class A {
    
        private String mSomeText;
    
        public class B {
    
            public void doIt() {
                System.out.println(mSomeText);
            }
        }
    }
    

    When you do that you can access members of the class A from inside the class B. That's how doIt() can print mSomeText, it has access to all the members of A even private ones.
    The reason you can do that is that if you nest classes like that Java implicitly creates a reference to A inside of B. It is because of that reference and nothing else that you have access to all members of A inside of B. However in the context of memory leaks that again poses a problem if you don't know what you are doing. Consider the first example (I'll strip all the parts that don't matter from the example):

    public class ExampleActivty extends AppCompatActivity {
    
        public class ExampleTask extends AsyncTask<Void, Void, Bitmap> {
            ...
        }
    }
    

    So we have an AsyncTask as a nested class inside an Activity. Since the nested class is not static we can access members of the ExampleActivity inside the ExampleTask. It doesn't matter here that ExampleTask doesn't actually access any members from the Activity, since it is a non static nested class Java implicitly creates a reference to the Activity inside the ExampleTask and so with seemingly no visible cause we have a memory leak. How can we fix this? Very simple actually. We just need to add one word and that is static:

    public class ExampleActivty extends AppCompatActivity {
    
        public static class ExampleTask extends AsyncTask<Void, Void, Bitmap> {
            ...
        }
    }
    

    Just this one missing keyword on a simple nested class is the difference between a memory leak and completely fine code. Really try to understand the issue here, because it is at the core of how Java works and understanding this is crucial.

    And as for the other example with the Thread? The exactly same issue, anonymous classes like that are also just non static nested classes and immediately a memory leak. However it is actually a million times worse. From every angle you look at it that Thread example is just terrible code. Avoid at all costs.


    So I hope these example helped you understand the problem and how to write code free of memory leaks. If you have any other questions feel free to ask.

    0 讨论(0)
提交回复
热议问题