Mở đầu

Chào các bạn, hôm nay tôi sẽ chia sẻ về một vấn đề cũ rích mà không kém phần quan trọng mà chắc hẳn developer nào cũng từng nghe qua và áp dụng trong công việc hiện tại, đó là Memory Leak. Sở dĩ tôi lại lôi vấn đề được gọi là cũ rích này ra nói lại vì một số vấn đề khi trao đổi với bạn bè, đồng nghiệp và cả trong công việc hiện tại đôi lúc làm tôi nhận ra vấn đề này gọi là cũ nhưng chưa bao giờ cũ và vai trò vô cùng quan trọng của nó trong các ứng dụng hiện đại, đặc biệt là mobile app. Hôm nay tôi sẽ đi sâu hơn về các khái niệm cũng như cách thức loại bỏ vấn đề này trong Lập trình mobile với Android.

Memory Leak là gì?

Các ngôn ngữ lập trình native như C, C++ không có cơ chế tự động thu hồi vùng nhớ sau khi được cấp phát. Điều đó nghĩa là bạn phải làm thủ công tất cả. Ví như khi bạn muốn khai báo một biến, bạn cấp phát cho nó 1 vùng nhớ và thao tác, sau đó đừng quên release vùng nhớ đó để tái sử dụng cho những lần khác. Các ngôn ngữ lập trình bậc cao hiện đại khác như Java, C# lại làm điều đó một cách tự động, nghĩa là bạn chỉ cần khởi tạo và cấp phát vùng nhớ, việc dọn dẹp và thu hồi vùng nhớ sẽ được thực hiện một cách tự động.

Thế nhưng, cơ chế tự động thu dọn ở mỗi ngôn ngữ lập trình không hoàn toàn giống nhau và tuân theo một số rule nhất định, chúng ta sẽ nói về Java. Trong Java có một đơn vị làm nhiệm vụ thu hồi dọn dẹp vùng nhớ gọi là GC. GC sẽ thu hồi vùng nhớ của một đối tượng khi không còn bất kỳ một reference nào đến đối tượng đó, nghĩa là nó không còn được sử dụng nữa. Tuy nhiên trong lập trình, đôi lúc ta lại làm cho đối tượng đáng ra không còn được sử dụng lại không thể được GC thu dọn, chính điều này sẽ gây ra rò rỉ bộ nhớ mà chúng ta đang đề cập đến.

Tầm quan trọng?

Mặc dù các thiết bị hiện đại được trang bị bộ nhớ lớn hơn nhiều lần, tuy nhiên điều đó cũng đồng nghia với việc các ứng dụng ngày càng đòi hỏi cao hơn về bộ nhớ. Và dĩ nhiên, một ứng dụng càng ngốn ít bộ nhớ sẽ càng chạy mượt mà hơn. Tránh được sự rò rỉ bộ nhớ sẽ giúp cho ứng dụng của bạn luôn đảm bảo việc tiêu tốn bộ nhớ một cách ít nhất, tối ưu nhất và tăng hiệu suất một cách đáng kể.

Memory Leak sẽ làm lượng bộ nhớ mà ứng dụng của bạn sử dụng tăng đột biến mà không thể thu dọn, điều đó gây ra giật lag thậm chí là crash khi bộ nhớ bị tràn. Đó là một điều vô cùng tồi tệ.

Một số trường hợp gây Memory Leak và cách giải quyết

Trong Android, thông thường việc leak bộ nhớ xảy ra xung quanh Activity/Fragment nên chúng ta sẽ quan tâm đến việc bộ nhớ có được giải phóng khi Activity/Fragment không còn được dùng đến nữa hay không.
Tôi sẽ đơn cử một vài trường hợp gây ra rò rỉ bộ nhớ kinh điển trong Android mà chúng ta nên tuyệt đối tránh

1. Static Context.

Chúng ta biết rằng các biến static là những biến rất khó để được Gabage Collected - Tôi dùng từ khó ở đây vì trong một vài trường hợp đặc biệt, biến static vẫn có thể được thu dọn vùng nhớ. Vì vậy, chúng ta nên tránh việc sử dụng các biến Context dưới dạng biến static.

public class LeakMemory {

    private static Context sContext;

    public LeakMemory(Context context){
        sContext = context;
    }

}

Implementation ở trên trông khá tồi. Ví như bạn pass 1 Context instance - Activity nào đó chẳng hạn - để khởi tạo một instance của LeakMemory thì rõ ràng, Activity đó sẽ không thể được GC thu dọn. Nếu có thể, hãy hạn chế việc sử dụng các biến tĩnh giữ reference tới một instance có thể được Gabage Collected.

2. Inner Class và Anonymous Class

Chúng ta biết rằng, trong Java Inner Class và Anonymous Class giữ một reference tới Outer Class - nghĩa là bạn có thể sử dụng các field hay gọi các method được khai báo ở Outer Class. Trong một số trường hợp, Outer Class không còn được sử dụng và đáng ra sẽ được Gabage Collected, tuy nhiên Inner class hay Anonymous class đang giữ reference đến nó lại ngăn chặn điều đó bằng việc đang có một task long live chạy ngầm chẳng hạn.

public class LeakMemory {

 private LoadImage loadImage;

 public LeakMemory(){
     loadImage = new LoadImage();
     loadImage.execute("test");
 }

 public class LoadImage extends AsyncTask<String, Void, Bitmap> {
     @Override
     protected Bitmap doInBackground(String... strings) {
         return null;
     }
 }
}

Nếu LeakMemory object cần được thu dọn, nó sẽ không thể bởi inner class LoadImage còn giữ một long live task và do đó để khắc phục tình trạng này, chúng ta sẽ define LoadImage dưới dạng static nested class và method để cancel background task khi cần thiết.

public class LeakMemory {

    private LoadImage loadImage;

    public LeakMemory() {
        loadImage = new LoadImage();
        loadImage.execute("test");
    }

    public void cancel(){
        loadImage.cancel(true);
    }

    public static class LoadImage extends AsyncTask<String, Void, Bitmap> {
        @Override
        protected Bitmap doInBackground(String... strings) {
            return null;
        }
    }
}

3. Thread

Một vùng nhớ của đối tượng nào đó sẽ không thể được Gabage Collected nếu nó có một Thread đang được thực thi bên trong nó, nói cách khác vùng nhớ đó chỉ được thu dọn một khi tất các các tác vụ được thực thi bởi thread kia hoàn thành. Đến với một ví dụ đơn giản bên dưới.

public class LeakMemory {

    public void load() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

Một khi GC muốn thu dọn vùng nhớ của đối tượng này, nó phải chờ đến khi Thread đã được thực thi hoàn tất hoặc cách khác, bạn phải interupt nó ngay thời điểm không còn được sử dụng.

public class LeakMemory {
    
    private Thread thread;
    
    public void interupt(){
        if (thread == null) return;
        thread.interrupt();
    }

    public void load() {
        thread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread.start();
    }
}

4. Handler

Trong Android, một khi ứng dụng được khởi động, nó sẽ tạo ra một Looper trên Main Thread. Looper này chứa một Message Queue để xử lý các Message được gửi đến. Tất cả các Framework Events ví dụ như Activity Lifecycle Callback, các sự kiện click vào view ... đều được đóng gói thành một Message object và gửi vào Queue chờ xử lý. Và Looper này sẽ tồn tại cùng với vòng đời ứng dụng.

Một đối tượng Handler sinh ra trên Thread khởi tạo ra nó và gắn với Message Queue trong Looper tương ứng. Một Message được post sẽ giữ một reference đến Handler và Framework sẽ gọi lệnh Handler#handleMessage(Message)

Đến với ví dụ bên dưới

public class LeakMemory {

    private LeakHandler leakHandler;

    public void post(){
        leakHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                // DO something
            }
        }, 2000);
    }

    private static class LeakHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    }
}

Khi GC muốn thu dọn đối tượng của LeakMemory, nó không thể thực hiện điều đó vì một Message được post từ Handler vẫn ở trong hàng đợi trong khoảng thời gian 2000 ms. Do đó, có 2 cách để tránh việc Leak bộ nhớ trong trường hợp này

  • Define một static Runnable.
public class LeakMemory {

    private LeakHandler leakHandler;

    public void post() {
        MessageRunnable runnable = new MessageRunnable();
        leakHandler.postDelayed(runnable, 2000);
    }

    private static class LeakHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    }

    private static class MessageRunnable implements Runnable {
        @Override
        public void run() {

        }
    }
}
  • Remove Message object trong Message Queue
public class LeakMemory {

    private LeakHandler leakHandler;
    private Runnable runnable;

    public void cancel() {
        leakHandler.removeCallbacks(runnable);
    }

    public void post() {
        runnable = new Runnable() {
            @Override
            public void run() {
                // DO something
            }
        };
        leakHandler.postDelayed(runnable, 2000);
    }

    private static class LeakHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    }
}

5. Sensor Manager

Android cung cấp nhiều System Service thực hiện nhiều tác vụ khác nhau và chạy trên process riêng biệt của chúng. Các service này hỗ trợ các ứng dụng có thể làm việc với hệ thống phần cứng. Một trong số đó là Sensor Service cung cấp một interface để làm việc với hệ thống Sensor vật lý trên thiết bị. Một ví dụ khi muốn làm việc với Sensor Service

void registerListener() {
       SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
       Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
       sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_FASTEST);
}

Dễ dàng nhận thấy đoạn mã trên đăng ký một callback đến Sensor Service và nó sẽ giữ reference tới thể hiện này một khi nó chưa được unregister. Memory Leak có thể xảy ra trong trường hợp chúng ta chỉ register mà quên mất việc unregister nó. Luôn luôn đảm bảo rằng unregister là công việc bắt buộc khi bạn muốn tránh việc rò rỉ bộ nhớ.

Kết luận

Có rất nhiều trường hợp có khả năng gây ra Memory Leak trong Android, nhưng ở bài viết này tôi chỉ liệt kê một vài trường hợp thường gặp nhất. Hy vọng sẽ giúp các bạn tránh được vấn đề nhức nhối này.