Android Performance Optimization: Memory Management Techniques

Android Performance Optimization: Memory Management Techniques

Hot Articles | Click Title to Read

How to Advance to Become a Java and Android Architect?

Why Should We Prohibit HTTP Methods Other Than GET and POST?

Why Do Internet Companies in Beijing Experience Rush Hour at 10 PM?

Author: Overried

Source: http://www.apkbus.com/blog-955864-78235.html

Memory optimization is a basic skill for programmers. Sometimes, it is necessary to make choices based on the actual needs of the project.

1. Solve All Memory Leaks

Memory leak concept: An object that is no longer used is not collected, which is a memory leak.

1. Singleton Leak

The main reason is that in general, singletons are global, and sometimes they reference variables with a shorter lifecycle, preventing their release.

For example:

  • Assigning the activity’s content to a member variable in a singleton object

code:

    private static volatile ClassXX instance;
    private Context context;
    private ClassXX(Context context) {
        this.context = context;
    }

    public static ClassXX getInstance(Context context) {
        if (instance == null) {
            synchronized (instance) {
                if(instance == null) {
                    instance =  new ClassXX(context);
                }
            }
        }

        return instance;
    }

If this Context is an Activity context, after your Activity finish();, the memory of the Activity object is still in the heap and has not been released. Because the singleton object holds a reference to the Activity, the JVM believes that you are still using this object and does not dare to collect your Activity. When will the singleton be collected? Only when the whole process is collected will the singleton be collected.

Process killed (recycled):

  • Process.killProcess(Process.myPid())

  • User manually destroys the card (proven to be effective)

Solution:

  • Pass in an object with the same lifecycle as the singleton, such as context.getApplication();

  • Do not store context in the singleton’s member variable.

2. Memory Leaks from Handler, AsyncTask, and Other Inner Classes

The main reason is that inner classes default to holding a reference to the outer class.

Everyone should like to write Handler as an inner class, for example:

private Handler mMainActivityHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
      super.handleMessage(msg);
}
};

Actually, I also like it, and each Activity corresponds to a Handler, with each Handler responsible for updating the UI of its Activity, a one-to-one relationship with clear division of labor. It is extremely useful.

However, Java inner classes automatically hold a reference to an outer class because the JVM includes a reference to the outer class in the default constructor when compiling the .java source file into .class bytecode. Therefore, we can access the outer class reference in the inner class.

Then the problem arises: the current Handler holds a reference to the current Activity, and if the Handler is not released, the Activity cannot be released either. MMP

(Why might the Handler not be released sometimes?)

Solution:

  • Pass the Activity into the constructor and use WeakReference mActivity; to save it as a weak reference. During GC, it will not count as a reference to Activity and can be collected.

  • When Activity OnDestroy, terminate all related requests and clear the message queue using removeCallbacksAndMessages(null); to prevent data from being returned to the UI layer. (Of course, if this is not done, the Activity will still be collected, but the Handler will not be collected in time.)

(What are strong references, soft references, weak references, and phantom references, and how does the Handler message-driven model work? This will not be discussed here; this article focuses on memory leaks.)

Of course, AsyncTask and other inner class objects also have this problem; the solution is the same.

3. Resources Not Closed After Use

The main issues are:

  • Broadcast (BroadcastReceiver) must be unregistered after dynamic registration; it is recommended to execute in the corresponding lifecycle of onStart onStop.

  • Service (Service) must be stopped after it is started. The timing of starting the service depends on the needs. Generally, it is not recommended to start it in Application (starting a Service generally takes more than 100ms).

  • IO Cursor streams must be closed; always close in finally to prevent exceptions from preventing close from executing, which would cause leaks.

  • Bitmap is a large memory consumer; remember to recycle it. Of course, in 90% of scenarios, Glide has already handled this for us.

4. Tools to Detect Memory Leaks

Sometimes, it is impossible to avoid all memory leaks while writing code, so we need to use some tools to detect them:

  • LeakCanary

  • Android Studio Profile

  • MAT

Choose your favorite tool and research it. (There are many tutorials online.)

2. Image Compression

1. Bitmap Compression

Everyone knows that Bitmap consumes a lot of memory, and after use, it needs to be recycled. I wonder if anyone has encountered a situation where loading an image causes an out-of-memory (OOM) error; I have encountered it (my mind is racing with a million thoughts).

First, when an image is obtained from the network, the InputStream is converted into a Bitmap. How do we calculate how much memory this Bitmap occupies?

Here’s the code: Bitmap.getAllocationByteCount(); The byte count is calculated as Width * Height * 4 (assuming each pixel is RGB888, which is 4 bytes). A pixel using RGB565 occupies 3 bytes; of course, RGB888 occupies more bytes and has higher definition. The initial version of Glide used RGB565, while currently, Glide 4.XX defaults to RGB888, but you can configure it.

To solve this problem, we generally use the following code:

BitmapFactory.Options options = new BitmapFactory.Options();  
options.inJustDecodeBounds = true;  
// Get the width and height of the image through this bitmap 
Bitmap bitmap = BitmapFactory.decodeFile("/sdcard/MTXX/3.jpg", options);  
float realWidth = options.outWidth;  
float realHeight = options.outHeight;
// Calculate scale
options.inSampleSize = scale;  
options.inJustDecodeBounds = false;  
// Note that this time options.inJustDecodeBounds should be set to false, this time the image is to be read out.
bitmap = BitmapFactory.decodeFile("/sdcard/MTXX/3.jpg", options);  
  • First, get the image size and calculate the scaling ratio based on the desired size. (The image size is stored in the image header, so the entire image does not need to be loaded at this time.)

  • Scale it to get a size that fits your control dimensions. (Of course, there are some illegal image headers that cannot yield length * width. At this time, remember to set a default scaling rate to prevent OOM.)

Sometimes, to optimize memory, compressing an image can save more memory. For example, a 1080 * 1920 image multiplied by 4 equals 7.9 MB. I compressed it to a thumbnail of 200*200, which equals 156 KB. The difference is really significant, and I can’t help but feel frustrated.

3. Solve Memory Jitter

1. String vs StringBuffer vs StringBuilder

Everyone should be familiar with these three classes. Let’s look at the code:

long time = System.currentTimeMillis();
String s = new String("JAVA");
for(int i = 0 ;i < 10000; i++) {
   s = s + "VERSION";
}
Log.d("TestString","Time consumption:"+(System.currentTimeMillis() - time));
time = System.currentTimeMillis();

StringBuilder s1 = new StringBuilder("JAVA");
for(int i = 0 ;i < 10000; i++) {
   s1.append("VERSION");
}
Log.d("TestString","Time consumption:"+(System.currentTimeMillis() - time));


D/TestString: Time consumption:3786
D/TestString: Time consumption:2

It is clear that using StringBuilder to concatenate strings is much more efficient than using the plus sign; let’s find out why.

Let’s look at the bytecode when using the + sign for concatenation:

Android Performance Optimization: Memory Management Techniques

  • Using the + sign to concatenate strings, the JVM will create a temporary StringBuilder

25 new #24 <java/lang/StringBuilder>

  • Then it passes the last result set into the constructor,

29 invokespecial #25 <java/lang/StringBuilder.<init>> // Call the constructor; this symbol reference is similar to JNI callback Java class lookup writing
  • Then it concatenates the characters that need to be concatenated this time. The result is stored in the local variable table, waiting for the next loop operation.

   44 astore_3
  • Then jump to number 17 to continue the loop. At this time, a new StringBuilder is created for concatenation again. This is really a waste.

48 goto 17 (-31)

Let’s look at the bytecode when using StringBuilder for concatenation:

Android Performance Optimization: Memory Management Techniques

This is clear: the new StringBuilder bytecode is outside the loop, so no new objects are created in the loop.

Conclusion:

From the above example, using String concatenation created 10,000 StringBuilder objects, which were discarded after use. This is particularly wasteful, and in memory-tight situations, it can easily trigger GC, causing the App to lag. Some students may ask how much memory an empty StringBuilder occupies in the heap? Let’s calculate:

  • An object = object header + member attributes

  • Object header = MardWord + Klass = 12 bytes (excluding arrays)

Object structure diagram:

Android Performance Optimization: Memory Management Techniques

List of MardWord fields (taken from the internet):

Android Performance Optimization: Memory Management Techniques

This MardWord has so many lock states; what are these lock states? This involves knowledge of synchronized locks, which is not within the scope of this article.

So what are the member attributes of StringBuilder? Here’s the list:

static final long serialVersionUID = 4383685877147921099L;
char[] value;
int count;

Calculating gives us: 12 + 8 + 8 + 4 + 24 = 56 bytes for 10,000 objects, which is 560KB of memory. Not small, right? Of course, we wouldn’t normally create this many objects at once, but if we use String in many places, it adds up, and then the APP memory could be significantly higher than others. That would be awkward.

4. Try to Use “Pools”

Common pools include:

  • Thread Pool

  • LruCache Cache Pool

  • OkHttp Connection Pool (socket reuse pool)

  • Okio SegmentPool (buffer reuse pool)

The function of the pool: to reuse objects, reduce memory overhead, memory jitter, and CPU overhead.

  • Thread Pool

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,  
                              RejectedExecutionHandler handler)

Try to use thread pools to run tasks, rather than creating a new thread every time; this way, the threads can be reused. When the task volume increases, the efficiency of using thread pools will exceed your imagination (check the source code for details); after all, starting a thread incurs CPU and memory overhead.

Here, I recommend the RxJava third-party library, a framework that takes the decorator pattern to a new level, making it easy to switch threads and supporting functional programming to avoid callback hell, etc.:

Observable.create(new Action1<Emitter<Integer>>() {
@Override
public void call(Emitter<Integer> subscriber) {}
}, Emitter.BackpressureMode.BUFFER)
.subscribeOn(Schedulers.io()) // Switch to IO thread pool
.subscribeOn(Schedulers.computation()) // Switch to computation thread pool
.subscribeOn(Schedulers.immediate()) // Use current thread
.observeOn(AndroidSchedulers.mainThread()) // Switch to Android UI main thread
.subscribe();

2. LruCache Cache Pool

LruCache: Least Recently Used cache pool, implemented using LinkedHashMap.

Google’s Glide image loading library uses LruCache and LruDiskCache for image caching, thereby improving user experience.

3. ConnectionPool Cache Pool

ConnectionPool: Reuses TCP socket for network communication; after each HTTP request ends, the connection is not closed and can be reused for the next request, maximizing network transmission speed.

Each HTTP request involves:

  • TCP three-way handshake

  • Data transmission

  • TCP four-way handshake

If every request goes through the entire process, others may have loaded all data while I am still in the handshake… This is intolerable. (Of course, HTTP 1.1+ supports this connection reuse; check the OkHttp source code for details; this article does not elaborate.)

4. Okio SegmentPool (Buffer Reuse Pool)

SegmentPool: Same as above.

Conclusion:

For objects that need to be generated and recycled frequently, it is recommended to use pools. If you don’t have a library, you can also write one manually.

5. Others

  • Common Data Structure Optimization

  • XML Hierarchy and View

1. Common Data Structure Optimization

High memory users: HashMap (and its subclasses) HashMap is a typical case of space for time; the time complexity approaches O(1), but it occupies more space than size / 0.75 (load factor).

/**
* HashMap put partial source code,
* size is the current number of stored data
* threshold = capacity * 0.75
*/

if (++size > threshold)
resize();

In simple terms, storing 100 pieces of data will occupy 133 pieces of memory (or more); in cases where the data volume is small or speed is not a priority, you can use SparseArray (implemented as a binary tree) instead.

2. XML Hierarchy and View

It is best to keep the XML hierarchy within 5 layers.

For using views, prefer:

  • ViewStub

  • Include

  • Merge

If you have good articles to share, feel free to submit the article link directly to me

Finally, everyone is welcome to join our Knowledge Planet, this issue ends on March 10, 2019, so the earlier you join, the better. There are nearly 1000 members joining now, and the price will increase significantly when it reaches 1000 members (there are only a few slots left), so hurry up!

Android Performance Optimization: Memory Management Techniques

Scan the QR code or click the QR code above to receive advanced resources for Android, Python, AI, Java, etc.

For more learning materials, click below to get the “Read the Original”

Android Performance Optimization: Memory Management Techniques

Leave a Comment

×