2016年3月1日

Android 記憶體管理

Android Dalvik GC 採用 Mark and Sweep 處理機制,mark 會 traverse 整個 object tree,並將所有未被其他物件參考到的物件,標示為 unused,未被使用的物件,就可以被 GC。

APP 執行緒潛藏 memory leak 的風險,只有在 thread 終止時,他們使用到的物件才會被GC。

worker thread

當執行緒在執行時,Thread 物件本身會變成 GC root,他所參考到的物件都是可以到達的。而 Thread 與 Runnable instance 保存了只向其他物件的 references,必須等到 Thread 結束,才能回收那些物件。當 method return 後,方法裡面所建立的物件就能被 GC,除非該 method 將物件回傳給他的呼叫者,讓其他 method 可以使用該物件。

  • 內部類別

inner class 是 outer class 的成員,可以存取外部類別的其他成員,因此內部類別隱含地參考到外部類別,被定義為內部類別的執行緒,保存了指向外部類別的 reference,只要執行緒還在執行,外部類別就不會被標示為可以被 GC。

定義為 local class 與 anonymous inner class 的執行緒跟外部類別的關係,與內部類別相同,在執行期間,可以讓外部類別從 GC Root 到達。

以下範例中,只要 SampleThread 還在執行中,在 Outer 類別裡的任何物件,都必須保存在記憶體中,伴隨著內部的 SampleThread 類別裡的物件。

public class Outer {
    public void sampleMethd() {
        SampleThread sampleThread = new SampleThread();
        sampleThread.start();
    }
    
    private class SampleThread extends Thread {
        public void run() {
            Object sampleObject = new Object();
        }
    }
}
  • 靜態內部類別

static inner class 是外部類別 instance 的成員,定義在靜態內部類別的執行緒保存了指向外部物件的類別的 member reference,而不是直接只向外部物件,所以一旦指向外部物件的其他參考消失,就可以被 GC。

public class Outer {
    public void sampleMethd() {
        SampleThread sampleThread = new SampleThread();
        sampleThread.start();
    }
    
    private static class SampleThread extends Thread {
        public void run() {
            Object sampleObject = new Object();
        }
    }
}

在大多數情況下,我們會想要將 Thread 與 Runnable 分開,如果建立了新的 Runnable 作為內部類別,在執行期間會保存指向外部類別的 reference。

public class Outer {
    public void sampleMethd() {
        SampleThread sampleThread = new SampleThread(new Runnable () {
            @Override
            public void run() {
                Object sampleObject = new Object();
            }
        });
        sampleThread.start();
    }
    
    private static class SampleThread extends Thread {
        public SampleThread(Runnable runnable) {
            super(runnable);
        }
    }
}

執行緒溝通

執行緒跟訊息傳遞機制是 memory leak 的潛在根源。對收到訊息物件的 thread 來說,他必須使用 MessageQueue 來存放待處理的訊息,使用 Looper 派送訊息,使用 Handler 處理訊息,這些物件都會被 worker thread 參考。然而 Handler 是 memory leak 發生的主因,因為他是透過一系列物件,被 consumer thread 參考到的, Handler 跟他所參考到的物件,必須等到 thread 終止時,才能被 GC。

  • 傳送資訊訊息

資料物件能以多種方式被傳遞,選擇哪一種實作方式,會決定 memory leak的規模。

public class Outer {
    Handler mHandler = new Handler() {
        public void handleMessage(Message msg) {
        }
    }
    
    public void doSend() {
        Message message = mHandler.obtainMessage();
        message.obj = new SampleObject();
        mHandler.sendMessageDelayed(message, 60*10000);
    }
}
  • 發布任務訊息

Handler 與 Runnable 都會參考到 Outer,增加了 memory leak 的風險

public class Outer {
    Handler mHandler = new Handler() {
        public void handleMessage(Message msg) {
        }
    }
    
    public void doPost() {
        mHandler.post(new Runnable() {
            public void run() {
            }
        });
    }
}

如何避免 memory leak

  • 使用靜態內部類別 不使用具有外部物件參考的巢狀類別,改用靜態內部類別。因為他們只會參考到痊癒的 class object,而不是 instance object。

  • 使用 WeakReference

WeakReference 可避免 reference counting。

public class Outer {
    private int mField;
    
    pirvate static class SampleThread extends Thread {
        private final WeakReference<Outer> mOuter;
        
        SampleThread(Outer outer) {
            mOuter = new WeakReference<Outer>(outer);
        }
        
        public void run() {
            if(mOuter.get() !=null ) {
                mOuter.get().mField=1;
            }
        }
    }
}
  • 停止 Work Thread

  • 保留 Work Thread

以 retain 的方式,保留舊物件給新的 Activity 使用。

  • 清除 Message Queue
removeCallbacks(Runnable r)
removeCallbacks(Runnable r, Object token)
removeCallbacksAndMessages(Object token)
removeMessages(int what)
removeMessages(int what, Object object)

Android 管理執行緒的生命週期

每個 APP 必須管理他們建立的執行緒,避免發生 memory leak。

  • 定義與啟動

    定義與啟動 Thread 的方式會影響記憶體洩漏的風險與規模。以下是幾種建立執行緒的方式:

    1. 匿名內部類別:容易實作,但保有只向外部類別的參考,可能會產生記憶體洩漏

      public class AnyObject {
          @UiThread
          public void anyMethod() {
              new Thread {
                  public void run() {
                      doLongRunningTask();
                  }
              }.start();
          }
      }
    2. 公用執行緒:將執行緒定義為獨立的類別

      class MyThread extends Thread {
          public void run() {
              doLongRunningTask();
          }
      }
      public class AnyObject {
          private MyThread myThread;
      
          @UiThread
          public void anyMethod() {
              myThread = new MyThread();
              myThread.start();
          }
      }
    3. 靜態內部類別執行緒:在類別物件上定義執行緒,而不是 instance

      public class AnyObject {
          static class MyThread extends Thread {
              public void run() {
                  doLongRunningTask();
              }
          };
      
          private MyThread myThread;
      
          @UiThread
          public void anyMethod() {
              myThread = new MyThread();
              myThread.start();
          }
      }
  • 保留

    因為執行緒的生命週期可能會超過啟動它的元件,或是 Activity 因為設備旋轉而重新產生了新的 Activity,這時候的背景執行緒只有舊的 Activity 才知道,也需要重新啟動一次執行緒。因此我們需要一個保留工作執行緒的方法。

    public Object onRetainNonConfigurationInstance() 在組態改變前由平台呼叫,可以回傳想要保留並傳遞給新 Activity 的物件

    public Object getLastNonConfigurationInstance() 組態改變後,可在 onCreate 或 onStart 中被呼叫,取回先前紀錄的物件

    在 Activity 中保留 thread 的範例:

    public class ThreadRetainActivity extends Activity {
    
        // worker thread 宣告為靜態內部類別
        private static class MyThread extends Thread {
            private ThreadRetainActivity mActivity;
    
            public MyThread(ThreadRetainActivity activity) {
                mActivity = activity;
            }
    
            // attach 用來記錄 Activity 的 reference
            private void attach(ThreadRetainActivity activity) {
                mActivity = activity;
            }
    
            @Override
            public void run() {
                final String text = getTextFromNetwork();
                mActivity.setText(text);
            }
    
            // Long operation
            private String getTextFromNetwork() {
                // Simulate network operation
                SystemClock.sleep(5000);
                return "Text from network";
            }
        }
    
        private static MyThread t;
        private TextView textView;
    
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_retain_thread);
            textView = (TextView) findViewById(R.id.text_retain);
    
            // 查詢有沒有先前紀錄的物件
            Object retainedObject = getLastNonConfigurationInstance();
            if (retainedObject != null) {
                t = (MyThread) retainedObject;
                // 修改 worker thread 裡面記錄的 activity reference
                t.attach(this);
            }
        }
    
        @Override
        public Object onRetainNonConfigurationInstance() {
            // 在 worker thread 還在處理中時,回傳 t
            if (t != null && t.isAlive()) {
                return t;
            }
            return null;
        }
    
        public void onStartThread(View v) {
            // 啟動 thread
            t = new MyThread(this);
            t.start();
        }
    
        private void setText(final String text) {
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    textView.setText(text);
                }
            });
        }
    }

    在 Fragement 保留 thread 的方法:Fragment 通常用來處理部分 UI,可以將保留 thread instance 的責任轉交給 Fragment。

    public class ThreadRetainWithFragmentActivity extends Activity {
    
        private static final String TAG = "ThreadRetainActivity";
    
        private static final String KEY_TEXT = "key_text";
    
        private ThreadFragment mThreadFragment;
    
        private TextView mTextView;
    
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_retain_thread);
            mTextView = (TextView) findViewById(R.id.text_retain);
    
            // Activity 第一次啟動,要產生 Fragment
            FragmentManager manager = getFragmentManager();
            mThreadFragment = (ThreadFragment) manager.findFragmentByTag("threadfragment");
    
            if (mThreadFragment == null) {
                FragmentTransaction transaction = manager.beginTransaction();
                mThreadFragment = new ThreadFragment();
                transaction.add(mThreadFragment, "threadfragment");
                transaction.commit();
            }
        }
    
        @Override
        protected void onRestoreInstanceState(Bundle savedInstanceState) {
            super.onRestoreInstanceState(savedInstanceState);
            mTextView.setText(savedInstanceState.getString(KEY_TEXT));
        }
    
        @Override
        protected void onSaveInstanceState(Bundle outState) {
            super.onSaveInstanceState(outState);
            outState.putString(KEY_TEXT, (String)mTextView.getText());
        }
    
        // Method called to start a worker thread
        public void onStartThread(View v) {
            // 啟動 worker thread
            mThreadFragment.execute();
        }
    
        public void setText(final String text) {
            // 讓 fragment 回頭更新 UI
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    mTextView.setText(text);
                }
            });
        }
    }
    
    public class ThreadFragment extends Fragment {
    
        // 保留上層的 Activity reference
        private ThreadRetainWithFragmentActivity mActivity;
        private MyThread t;
    I
        private class MyThread extends Thread {
    
            @Override
            public void run() {
                final String text = getTextFromNetwork();
                mActivity.setText(text);
            }
    
            // Long operation
            private String getTextFromNetwork() {
                // Simulate network operation
                SystemClock.sleep(5000);
                return "Text from network";
            }
        }
    
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            // 在狀態改變時,保留狀態
            setRetainInstance(true);
        }
    
        @Override
        public void onAttach(Activity activity) {
            super.onAttach(activity);
            mActivity = (ThreadRetainWithFragmentActivity) activity;
        }
    
        @Override
        public void onDetach() {
            super.onDetach();
            mActivity = null;
        }
    
        public void execute() {
            // 啟動 worker thread
            t = new MyThread();
            t.start();
        }
    }
  • 例外

    如果執行緒因例外而結束,可以用 UncaughtExceptionHandler 捕捉此狀況。

    1. 執行緒全域處理器

      static void setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler handler);
    2. 執行緒區域處理器:比全域處理器還早被執行

      void setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler handler);

    範例程式:

    Thread t = new Thread(new Runnable() {
        @Override
        public void run() {
            throw new RuntimeException("unexpected error");
        }
    });
    
    t.setUncaughtExceptionHandler(new Thread.UncaughtExceptioHandler() {
        @Override
        pbulic void uncaughtException(Thread thread, Throwable throwable) {
            Log.d(TAG, throwable.toString());
        }
    });

    APP 裡面有一個全域的 UncaughtExceptionHandler,他會以 kill process 的方式,處理所有例外。可以針對所有 thread,以全域的方式複寫,將例外重導到此 handler。

    Thread.setDefaultUncaughtEcceptionHandler(new ErrorReportExceptionHandler());
    
    public class ErrorReportExceptionHandler implements Thread.UncaightExceptionHandler {
        private final Thread.UncaughtExceptionHandler defaultHandler;
    
        public ErrorReportExceptionHandler() {
            this.defaultHandler=Thread.getDefaultUncaughtExceptionHandler();
        }
    
        @Override
        public void uncaughtException(Thread thread, Throwable throwable) {
            reportErrorToFile(throwable);
            defaultHandler.uncaughtException(thread, throwable);
        }
    }

Reference

Android 高效能多執行緒