Android內存泄漏--基礎介紹與延伸解析

    摘要:Android中內存泄漏的的分析。

Android的內存基礎知識

    Android系統在安裝、加載一個apk文件時,會在系統內存中劃出一部分作爲該apk的運行內存。

這個運行內存的大小,目前隨着Android設備的進化,也已經適量增大。從早期默認的90M左右到現在200M、300M。當你在設定屬性android:largeheap = "true"時,內存大小基本還會翻倍。如果想要得到具體可用內存,可在代碼中獲取具體數值:

Runtime runtimeMemory=Runtime.getRuntime();
long maxMemory=runtimeMemory.maxMemory()/(1024*1024);

    在apk可用內存增大的情況下,你仍然需要注意合理的分配內存,使用內存。雖然在大內存的情況下,可能將一些內存使用的隱患隱藏起來,沒有造成apk崩潰等,但如果apk中存在內存使用不善的情況,如內存泄漏,仍會影響apk的運行效率,嚴重的情況下,apk會發生內存溢出,導致崩潰。

    如果將apk可用內存比喻成一隻水桶,apk運行時佔用的內存比喻成桶裏的水。那麼現在這個桶變大變高了,正常情況下桶裏的水是不會滿溢的,Android與Java一樣,會隱式的進行GC垃圾對象回收(你可以顯示調用GC方法回收)。但當apk中存在內存泄漏的情況下,每一次泄漏導致無法GC回收,桶裏的水位就會慢慢增長,直至滿溢,造成內存溢出。

內存溢出是日常代碼編寫中,因不易發現,會導致很多線上問題的產生。代碼編寫的規範與良好習慣,是避免這個問題的主要辦法。

Android中常見的內存泄漏情景

    內存泄漏的產生過程:apk運行時,操作系統爲apk中的各種變量以及對象實例分配內存。假設程序運行後,產生了兩個對象:長生命週期對象A,短生命週期對象B,其中A持有了B的引用。在B生命週期結束後,理論上系統GC應該要釋放B佔用的內存,但因爲引用被A持有,導致內存無法釋放,這就造成了內存泄漏。

Android中我們常見的泄漏場景,列出代表性的如下:

1、靜態變量持有短生命週期的對象引用

    示例如下:object是一個靜態變量,在onCreate中將當前activity的引用賦值給了object。因爲static變量賦值後,會將引用保存在整個app的方法區。生命週期是與整個app是一致的,會一直持有這個引用,導致當前activity即使onDestroy後,引用也無法被GC釋放,造成內存泄漏。

public class StaticViewTestActivity extends AppCompatActivity {

    private static Object object;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_leak_test);
        object = this;
    }
}

    解法:(1)儘量不要將生命週期短暫的對象賦值給static變量,謹慎使用 (2)如果業務有這個需求,以上述代碼爲例,在使用完object後,在onDestroy請將object置爲null。

   謹慎使用靜態變量也要分清何時使用,不能總是擔心引發問題而不用。

   靜態相關延伸-1

   靜態方法是否會造成內存泄漏呢?看下面這段代碼:

public class StaticMethodTestActivity extends AppCompatActivity {


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_leak_test);
        test(this);
    }

    private static void test(Activity activity){
        Object object = activity;
    }
}

  先給出結論:這是不會造成內存泄漏的。原因在於java、android中,調用方法時(無論是靜態方法還是非靜態方法),JVM的虛機棧都會爲這個方法創建一個方法棧幀。你可以理解爲:一個線程中,有多個方法時,會產生多個棧幀,存於這個線程的棧幀隊列中。當方法被調用完畢後,該棧幀被彈出銷燬。方法中的局部變量等,也都將被釋放,因此不會存在內存泄漏情況。

  靜態相關延伸-2

  當一個類中,同時存在靜態變量、靜態方法、普通方法時,關係如下:

 

    如圖中所述:一個類中靜態變量、靜態方法的生命週期與該類的實例化對象是沒有關係的。

public static void main(String[] args){
        A a = new A();
        a.d();
        A.c();
        A.b = 1;
    }

執行以上這段代碼後,對象a的實例將會存在內存中的堆中,靜態方法c與靜態變量b將會存在內存的方法區。

  靜態相關延伸-3

  還有哪些常見場景是靜態變量持有短生命週期的引用,會引發泄漏?

   (1)單例模式,持有短生命週期的context

public class Test {
    private static Test INSTANCE;
    private Context context;

    private Test(Context context){
        this.context = context;
    }

    public static Test getInstance(Context context) {
        if (INSTANCE == null) {
            synchronized (Test.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Test(context);
                }
            }
        }
        return INSTANCE;
    }
}

      單例模式下getInstance中如果傳入的Context對象引用是activity的引用,因爲單例模式內部INSTANCE是靜態對象,沒有賦值爲null前,都會長存於內存中,context作爲該對象的屬性,也不會釋放,引發內存泄漏。

      解法:如果單例中必要傳入Context對象,使用Application的Context對象,因爲這個是與整個app生命週期同步的。

    (2) 靜態集合類引發的內存泄漏

public class Test {
    private static List<Object> ls;
    void operationList(){
         ls = new ArrayList<>();
      
         for(int i=0;i<100;i++){
            Object o = new Object();
             ls.add(o);
             o = null;
         }
    }
}

    以上代碼雖然在循環中,每次在集合ls添加Object對象o後,都將o對象置爲null,但集合ls仍持有o的引用,不會釋放。

    Object o = new Object(); 這個語句細分爲三個階段。

     Object o:聲明引用變量o,並在內存中分配空間;

     new Object():創建Object對象,並在內存的堆中分配空間存放它;

     = :等號,是個指向,將引用變量o指向創建好的Object對象。

    以此分析,上面的代碼在循環中,只是把引用變量本身的指向置爲null,在這之前,已經把引用存入到了靜態集合中,所以不會釋放。

     解法:不使用這種寫法。

2、非靜態內部類持有短生命週期的對象引用

     內部類概述:

     內部類包含靜態內部類和非靜態內部類。

     非靜態內部類包含匿名內部類以及內部類(有類名)。

     Java與Android中不存在頂層的靜態類,所有的靜態類都是指靜態內部類。     

      內部類有以下幾種場景:

public class Test {
    //局部變量
    private int val = 1;

    //一個成員內部類
    class Inner{
        public void testInInner(){
            System.out.println("這是一個成員內部類的方法");
            System.out.println("可以直接引用外部類Test的變量,val=" + val);
            System.out.println("可以直接引用外部類Test的變量,該寫法是在內部類中有同名變量時使用,val=" + Test.this.val);
        }
    }

    public void test1(){
        //匿名內部類
        //此處有匿名內部類, new Runnable
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("這是一個匿名內部類的方法");
                System.out.println("可以直接引用外部類Test的變量,val=" + val);
                System.out.println("可以直接引用外部類Test的變量,該寫法是在內部類中有同名變量時使用,val=" + Test.this.val);
            }
        }).start();
    }

    public void test2(){
        class MethodInner{
            public void testInMethodInner(){
                System.out.println("這是一個方法內部類的方法");
                System.out.println("可以直接引用外部類Test的變量,val=" + val);
                System.out.println("可以直接引用外部類Test的變量,該寫法是在內部類中有同名變量時使用,val=" + Test.this.val);
            }
        }
    }


    static class StaticInner{
        public void testInStatic(){
            System.out.println("這是一個成員內部類的方法");
            System.out.println("與外部類無關,不可以直接引用外部類Test的非靜態變量val");
        }
    }
}

        

(1)非靜態內部類爲何容易造成內存泄漏

     主要原因在於:非靜態內部類隱性的持有外部類的引用,當內部類中進行耗時等操作時,外部類的引用會被一直持有,無法被釋放。

     將上面的Test類做編譯操作(javac),能看到同級目錄下生成了5個字節碼文件:Test$1.class 、Test$1MethodInner.class 、Test$Inner.class、Test$StaticInner.class、Test.class;

     以Test$1.class爲例,字節碼文件中默認生成的構造函數中,參數是外部類的對象引用。除靜態內部類外,其他的內部類也是類似的構造,因此說非靜態內部類隱性的持有外部類的引用。

class Test$Inner {
    Test$Inner(Test var1) {
        this.this$0 = var1;
    }

    public void testInInner() {
        System.out.println("這是一個成員內部類的方法");
        System.out.println("可以直接引用外部類Test的變量,val=" + Test.access$000(this.this$0));
        System.out.println("可以直接引用外部類Test的變量,該寫法是在內部類中有同名變量時使用,val=" + Test.access$000(this.this$0));
    }
}

    其中access$000是編譯器默認給外部類生成的方法。

(2)內部類的泄漏,有哪些場景

     - 如上面舉例的new Runnable(){...},如果再run中有耗時操作,在耗時操作未結束前,就退出頁面,因持有外部類引用並不釋放,就會造成內存泄漏。

public class ThreadTestActivity extends AppCompatActivity {
    private static final String TAG = "ThreadTestActivity";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_leak_test);
        Log.d(TAG, "onCreate-this:"+this.toString());
        testThread();
    }

    private void testThread(){
        new Thread(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG, "testThread-this:"+this.toString());
                SystemClock.sleep(10*1000);
            }
        }).start();
    }
}

    解法:改寫方法,如必要這麼寫,定義靜態內部類實現Runnable接口。

  - 如定時器 TimeTask

new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                 while (true);
            }
        },3 * 1000);

  解法:在相對位置,對定時器做cancel,或者改爲靜態內部類實現。

 - 如Handler。下方例子:匿名內部類new Handler(){...}持有外部類Test的引用。handlerOp方法執行後,主線程的消息隊列在60s內都會持有handler的引用。handler又持有了外部類Test的引用,導致Test對象無法回收,造成內存泄漏。

public class Test {

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


    public void handlerOp(){
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                // doSomeThing
            }
        },60 * 1000);
    }
}

 解法:靜態內部類實現,或者handler改爲弱引用,或者在相對位置對handler做remove,語句(Handler.removeCallbacksAndMessages(null);)  

  - 其他場景,如AsyncTask等,類似處理。

3、資源對象未釋放

   資源對象未釋放,也是出現內存泄漏的一個常見場景。

   (1)如文件未關閉,導致分配給該文件引用的緩衝未及時釋放。如果多次未關閉,文件句柄太多沒有被關閉(Could not read input channel file descriptors from parcel)

   (2)如數據庫操作的遊標 Cursor未關閉,頻繁操作後會導致(android.database.CursorWindowAllocationException: Cursor window allocation of 2048 kb failed)

   如下示例,頻繁操作而out不做處理,可能就會導致句柄過多,內存泄漏直至溢出。

public class ResourceTestActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_leak_test);
        test();
    }

    private void test(){
        String filename = "app.txt";
        File file = new File(getExternalCacheDir(),filename);
        try{
            file.createNewFile();
            FileOutputStream out = new FileOutputStream(file);
        } catch (FileNotFoundException e){

        } catch (IOException e){

        }
    }
}

   解法:針對以上等情況,在正常流程或異常流程中,對資源關閉做好處理,如finally中做好關閉操作。

 

   總結的說,內存泄漏本質就是本該被回收的內存,沒有被回收,多關注生命週期。

   內存泄漏不經意間就會被你寫出,時時輕拂拭,莫使惹塵埃。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章