Android性能優化:看完這篇文章,至少解決 APP 中 90 % 的內存異常問題

我們爲什麼要優化內存

在 Android 中我們寫的 .java 文件,最終會編譯成 .class 文件, class 又由類裝載器加載後,在 JVM 中會形成一份描述 class 結構的元信息對象,通過該元信息對象可以知道 class 的結構信息 (構造函數、屬性、方法)等。JVM 會把描述類的數據從 class 文件加載到內存,Java 有一個很好的管理內存的機制,垃圾回收機制 GC 。爲什麼 Java 都給我們提供了垃圾回收機制,程序有時還會導致內存泄漏,內存溢出 OOM,甚至導致程序 Crash 。接下來我們就對實際開發中出現的這些內存問題,來進行優化。

JAVA 虛擬機

我們先來大概瞭解一下 Java 虛擬機裏面運行時的數據區域有哪些,如果想深入瞭解 Java 虛擬機 建議可以購買<<深入理解 Java 虛擬機>> 或者直接點擊我這裏的 PDF 版本 密碼: jmnf

線程獨佔區

程序計數器

  • 相當於一個執行代碼的指示器,用來確認下一行執行的地址
  • 每個線程都有一個
  • 沒有 OOM 的區

虛擬機棧

  • 我們平時說的棧就是這塊區域
  • java 虛擬機規範中定義了 OutOfMemeory , stackoverflow 異常

本地方法棧

  • java 虛擬機規範中定義了 OutOfMemory ,stackoverflow 異常

注意

  • 在 hotspotVM 中把虛擬機棧和本地方法棧合爲了一個棧區

線程共享區

方法區

  • ClassLoader 加載類信息
  • 常量、靜態變量
  • 編譯後的代碼
  • 會出現 OOM
  • 運行時常量池
    • public static final
    • 符號引用類、接口全名、方法名

java 堆 (本次需要優化的地方)

  • 虛擬機能管理的最大的一塊內存 GC 主戰場
  • 會出現 OOM
  • 對象實例
  • 數據的內容

JAVA GC 如何確定內存回收

隨着程序的運行,內存中的實例對象、變量等佔據的內存越來越多,如果不及時進行回收,會降低程序運行效率,甚至引發系統異常。

目前虛擬機基本都是採用可達性分析算法,爲什麼不採用引用計數算法呢?下面就說說引用計數法是如果統計所有對象的引用計數的,再對比可達性分析算法是如何解決引用計數算法的不足。下面就來看下這 2 個算法:

引用計數算法

每個對象有一個引用計數器,當對象被引用一次則計數器加一,當對象引用一次失效一次則計數器減一,對於計數器爲 0 的時候就意味着是垃圾了,可以被 GC 回收。

下面通過一段代碼來實際看下

public class GCTest {
    private Object instace = null;

    public static void onGCtest() {
        //step 1
        GCTest gcTest1 = new GCTest();
        //step 2
        GCTest gcTest2 = new GCTest();
        //step 3
        gcTest1.instace = gcTest2;
        //step 4
        gcTest2.instace = gcTest1;
        //step 5
        gcTest1 = null;
        //step 6
        gcTest2 = null;

    }

    public static void main(String[] arg) {
        onGCtest();
    }
}

分析代碼

//step 1 gcTest1 引用 + 1 = 1
//step 2 gcTest2 引用 + 1 = 1
//step 3 gcTest1 引用 + 1 = 2
//step 4 gcTest2 引用 + 1 = 2
//step 5 gcTest1 引用 - 1 = 1
//step 6 gcTest2 引用 - 1 = 1

很明顯現在 2 個對象都不能用了都爲 null 了,但是 GC 確不能回收它們,因爲它們本身的引用計數不爲 0 。不能滿足被回收的條件,儘管調用 System.gc() 也還是不能得到回收, 這就造成了 內存泄漏 。當然,現在虛擬機基本上都不採用此方式。

可達性分析算法

從 GC Roots 作爲起點開始搜索,那麼整個連通圖中額對象邊都是活對象,對於 GC Roots 無法到達的對象便成了垃圾回收的對象,隨時可能被 GC 回收。

可以作爲 GC Roots 的對象

  • 虛擬機棧正在運行使用的引用
  • 靜態屬性 常量
  • JNI 引用的對象

GC 是需要 2 次掃描纔回收對象,所以我們可以使用 finalize 去救活丟失的引用

 @Override
    protected void finalize() throws Throwable {
        super.finalize();
        instace = this;
    }

到了這裏,相信大家已經能夠弄明白這 2 個算法的區別了吧?反正對於對象之間循環引用的情況,引用計數算法無法回收這 2 個對象,而可達性是從 GC Roots 開始搜索,所以能夠正確的回收。

不同引用類型的回收狀態

強引用

Object strongReference = new Object()

如果一個對象具有強引用,那垃圾回收器絕不會回收它,當內存空間不足, Java 虛擬機寧願拋出 OOM 錯誤,使程序異常 Crash ,也不會靠隨意回收具有強引用的對象來解決內存不足的問題.如果強引用對象不再使用時,需要弱化從而使 GC 能夠回收,需要:

strongReference = null; //等 GC 來回收

還有一種情況,如果:

public void onStrongReference(){
    Object strongReference = new Object()
}

在 onStrongReference() 內部有一個強引用,這個引用保存在 java 棧 中,而真正的引用內容 (Object)保存在 java 堆中。當這個方法運行完成後,就會退出方法棧,則引用對象的引用數爲 0 ,這個對象會被回收。

但是如果 mStrongReference 引用是全局時,就需要在不用這個對象時賦值爲 null ,因爲 強引用 不會被 GC 回收。

軟引用 (SoftReference)

如果一個對象只具有軟引用,則內存空間足夠,垃圾回收器就不會回收它;如果內存空間不足了,就會回收這些對象的內存,只要垃圾回收器沒有回收它,該對象就可以被程序使用。軟引用可用來實現內存敏感的高速緩存。

軟引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果軟引用所引用的對象被垃圾回收器回收, java 虛擬機就會把這個軟引用加入到與之關聯的引用隊列中。

注意: 軟引用對象是在 jvm 內存不夠的時候纔會被回收,我們調用 System.gc() 方法只是起通知作用, JVM 什麼時候掃描回收對象是 JVM 自己的狀態決定的。就算掃描到了 str 這個對象也不會回收,只有內存不足纔會回收。

弱引用 (WeakReference)

弱引用與軟引用的區別在於: 只具有弱引用的對象擁有更短暫的生命週期。在垃圾回收器線程掃描它所管轄的內存區域的過程中,一旦發現了只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存。不過由於垃圾回收器是一個優先級很低的線程,因此不一定會很快發現那些只具有弱引用的對象。

弱引用可以和一個引用隊列聯合使用,如果弱引用所引用的對象被垃圾回收,Java 虛擬機就會把這個弱引用加入到與之關聯的引用隊列中。

可見 weakReference 對象的生命週期基本由 GC 決定,一旦 GC 線程發現了弱引用就標記下來,第二次掃描到就直接回收了。

注意這裏的 referenceQueuee 是裝的被回收的對象。

虛引用 (PhantomReference)

    @Test
    public void onPhantomReference()throws InterruptedException{
        String str = new String("123456");
        ReferenceQueue queue = new ReferenceQueue();
        // 創建虛引用,要求必須與一個引用隊列關聯
        PhantomReference pr = new PhantomReference(str, queue);
        System.out.println("PhantomReference:" + pr.get());
        System.out.printf("ReferenceQueue:" + queue.poll());
    }

虛引用顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用並不會決定對象的生命週期。如果一個對象僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收。

虛引用主要用來跟蹤對象被垃圾回收器回收的活動。虛引用與軟引用和弱引用的一個區別在於: 虛引用必須和引用隊列 (ReferenceQueue) 聯合使用。當垃圾回收器準備回收一個對象時,如果發現它還有虛引用,就會在回收對象的內存之前,把這個虛引用加入到與之關聯的引用隊列中。

總結

引用類型 調用方式 GC 是否內存泄漏
強引用 直接調用 不回收
軟引用 .get() 視內存情況回收
弱引用 .get() 回收 不可能
虛引用 null 任何時候都可能被回收,相當於沒有引用一樣

分析內存常用工具

工具很多,掌握原理方法,工具隨意挑選使用。

top/procrank

meinfo

Procstats

DDMS

MAT

Finder - Activity

LeakCanary

LeakInspector

內存泄漏

產生的原因: 一個長生命週期的對象持有一個短生命週期對象的引用,通俗點講就是該回收的對象,因爲引用問題沒有被回收,最終會產生 OOM。

下面我們來利用 Profile 來檢查項目是否有內存泄漏

怎麼利用 profile 來查看項目中是否有內存泄漏

  1. 在 AS 中項目以 profile 運行

  1. 在 MEMORY 界面中選擇要分析的一段內存,右鍵 export

**Allocations:** 動態分配對象個數

**Deallocation:** 解除分配的對象個數

**Total count:** 對象的總數

**Shalow Size:** 對象本身佔用的內存大小

**Retained Size:** GC 回收能收走的內存大小
  1. 轉換 profile 文件格式

    • 將 export 導出的 dprof 文件轉換爲 Mat 的 dprof 文件

    • cd /d 進入到 Android sdk/platform-tools/hprof-conv.exe

      //轉換命令 hprof-conv -z src des
      D:\Android\AndroidDeveloper-sdk\android-sdk-windows\platform-tools>hprof-conv -z D:\temp_\temp_6.hprof D:\temp_\memory6.hprof
      
      
  2. 下載 Mat 工具

  3. 打開 MemoryAnalyzer.exe 點擊左上角 File 菜單中的 Open Heap Dupm

  1. 查看內存泄漏中的 GC Roots 強引用

這裏我們得知是一個 ilsLoginListener 引用了 LoginView,我們來看下代碼最後怎麼解決的。

代碼中我們找到了 LoginView 這個類,發現是一個單例中的回調引起的內存泄漏,下面怎麼解決勒,請看第七小點。
  1. 2種解決單例中的內存泄漏

    1. 將引用置爲 null

      /**
           * 銷燬監聽
           */
          public void unRemoveRegisterListener(){
              mMessageController.unBindListener();
          }
          public void unBindListener(){
              if (listener != null){
                  listener = null;
              }
          }
      
      
    2. 使用弱引用

      //將監聽器放入弱引用中
      WeakReference<IBinderServiceListener> listenerWeakReference = new WeakReference<>(listener);
      
      //從弱引用中取出回調
      listenerWeakReference.get();
      
      
  2. 通過第七小點就能完美的解決單例中回調引起的內存泄漏。

Android 中常見的內存泄漏經典案例及解決方法

  1. 單例

    示例 :

    public class AppManager {
    
        private static AppManager sInstance;
        private CallBack mCallBack;
        private Context mContext;
    
        private AppManager(Context context) {
            this.mContext = context;
        }
    
        public static AppManager getInstance(Context context) {
            if (sInstance == null) {
                sInstance = new AppManager(context);
            }
            return sInstance;
        }
    
        public void addCallBack(CallBack call){
            mCallBack = call;
        }
    }
    
    
    1. 通過上面的單列,如果 context 傳入的是 Activity , Service 的 this,那麼就會導致內存泄漏。

      以 Activity 爲例,當 Activity 調用 getInstance 傳入 this ,那麼 sInstance 就會持有 Activity 的引用,當 Activity 需要關閉的時候需要 回收的時候,發現 sInstance 還持有 沒有用的 Activity 引用,導致 Activity 無法被 GC 回收,就會造成內存泄漏

    2. addCallBack(CallBack call) 這樣寫看起來是沒有毛病的。但是當這樣調用在看一下勒。

      //在 Activity 中實現單例的回調
      AppManager.getInstance(getAppcationContext()).addCallBack(new CallBack(){
          @Override
          public void onStart(){
      
          }
      });
      
      

      這裏的 new CallBack() 匿名內部類 默認持有外部的引用,造成 CallBack 釋放不了,那麼怎麼解決了,請看下面解決方法

    解決方法:

    1. getInstance(Context context) context 都傳入 Appcation 級別的 Context,或者實在是需要傳入 Activity 的引用就用 WeakReference 這種形式。

    2. 匿名內部類建議大家單獨寫一個文件或者

      public void addCallBack(CallBack call){
              WeakReference<CallBack> mCallBack= new WeakReference<CallBack>(call);
          }
      
      
  2. Handler

    示例:

    //在 Activity 中實現 Handler
    class MyHandler extends Handler{
        private Activity m;
        public MyHandler(Activity activity){
            m=activity;
        }
    
    //    class.....
    }
    
    

    這裏的 MyHandler 持有 activity 的引用,當 Activity 銷燬的時候,導致 GC 不會回收造成 內存泄漏。

    解決方法:

    1.使用靜態內部類 + 弱引用
    2.在 Activity onDestoty() 中處理  removeCallbacksAndMessages() 
        @Override
        protected void onDestroy() {
            super.onDestroy();
        if(null != handler){
              handler.removeCallbacksAndMessages(null);
              handler = null;
        }
     }
    
    
  3. 靜態變量

    示例:

    public class MainActivity extends AppCompatActivity {
    
        private static Police sPolice;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            if (sPolice != null) {
                sPolice = new Police(this);
            }
        }
    }
    
    class Police {
        public Police(Activity activity) {
        }
    }
    
    

    這裏 Police 持有 activity 的引用,會造成 activity 得不到釋放,導致內存泄漏。

    解決方法:

    //1\. sPolice 在 onDestory()中 sPolice = null;
    //2\. 在 Police 構造函數中 將強引用 to 弱引用;
    
    
  4. 非靜態內部類

    參考 第二點 Handler 的處理方式

  5. 匿名內部類

    示例:

    public class MainActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
          	new Thread(){
                 @Override
                 public void run() {
                    super.run();
                            }
                        };
        }
    }
    
    

    很多初學者都會像上面這樣新建線程和異步任務,殊不知這樣的寫法非常地不友好,這種方式新建的子線程ThreadAsyncTask都是匿名內部類對象,默認就隱式的持有外部Activity的引用,導致Activity內存泄露。

    解決方法:

    //靜態內部類 + 弱引用
    //單獨寫一個文件 + onDestory  = null;
    
    
  6. 未取消註冊或回調

    示例:

    public class MainActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            registerReceiver(mReceiver, new IntentFilter());
        }
    
        private BroadcastReceiver mReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                // TODO ------
            }
        };
    }
    
    

    在註冊觀察則模式的時候,如果不及時取消也會造成內存泄露。比如使用Retrofit + RxJava註冊網絡請求的觀察者回調,同樣作爲匿名內部類持有外部引用,所以需要記得在不用或者銷燬的時候取消註冊。

    解決方法:

    //Activity 中實現 onDestory()反註冊廣播得到釋放
        @Override
        protected void onDestroy() {
            super.onDestroy();
            this.unregisterReceiver(mReceiver);
        }
    
    
  7. 定時任務

    示例:

    public class MainActivity extends AppCompatActivity {
    
        /**模擬計數*/
        private int mCount = 1;
        private Timer mTimer;
        private TimerTask mTimerTask;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            init();
            mTimer.schedule(mTimerTask, 1000, 1000);
        }
    
        private void init() {
            mTimer = new Timer();
            mTimerTask = new TimerTask() {
                @Override
                public void run() {
                    MainActivity.this.runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            addCount();
                        }
                    });
                }
            };
        }
    
        private void addCount() {
          mCount += 1;
        }
    }
    
    

    當我們Activity銷燬的時,有可能Timer還在繼續等待執行TimerTask,它持有Activity 的引用不能被 GC 回收,因此當我們 Activity 銷燬的時候要立即cancelTimerTimerTask,以避免發生內存泄漏。

    解決方法:

    //當 Activity 關閉的時候,停止一切正在進行中的定時任務,避免造成內存泄漏。
        private void stopTimer() {
            if (mTimer != null) {
                mTimer.cancel();
                mTimer = null;
            }
            if (mTimerTask != null) {
                mTimerTask.cancel();
                mTimerTask = null;
            }
        }
    
        @Override
        protected void onDestroy() {
            super.onDestroy();
            stopTimer();
        }
    
    
  8. 資源未關閉

    示例:

    ArrayList,HashMap,IO,File,SqLite,Cursor 等資源用完一定要記得 clear remove 等關閉一系列對資源的操作。
    
    

    解決方法:

    用完即刻銷燬
    
    
  9. 屬性動畫

    示例:

    動畫同樣是一個耗時任務,比如在 Activity 中啓動了屬性動畫 (ObjectAnimator) ,但是在銷燬的時候,沒有調用 cancle 方法,雖然我們看不到動畫了,但是這個動畫依然會不斷地播放下去,動畫引用所在的控件,所在的控件引用 Activity ,這就造成 Activity 無法正常釋放。因此同樣要在Activity 銷燬的時候 cancel 掉屬性動畫,避免發生內存泄漏。
    
    

    解決方法:

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //當關閉 Activity 的時候記得關閉動畫的操作
        mAnimator.cancel();
    }
    
    
  10. Android 源碼或者第三方 SDK

**示例:**

```
//如果在開發調試中遇見 Android 源碼或者 第三方 SDK 持有了我們當前的 Activity 或者其它類,那麼現在怎麼辦了。

```

**解決方法**:

```
//當前是通過 Java 中的反射找到某個類或者成員,來進行手動 = null 的操作。

```

內存抖動

什麼是內存抖動

內存頻繁的分配與回收,(分配速度大於回收速度時) 最終產生 OOM 。

也許下面的錄屏更能解釋什麼是內存抖動

可以看出當我點擊了一下 Button 內存就頻繁的創建並回收(注意看垃圾桶)。

那麼我們找出代碼中具體那一塊出現問題了勒,請看下面一段錄屏


mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                imPrettySureSortingIsFree();
            }
        });

/**
     *&emsp;排序後打印二維數組,一行行打印
     */
    public void imPrettySureSortingIsFree() {
        int dimension = 300;
        int[][] lotsOfInts = new int[dimension][dimension];
        Random randomGenerator = new Random();
        for (int i = 0; i < lotsOfInts.length; i++) {
            for (int j = 0; j < lotsOfInts[i].length; j++) {
                lotsOfInts[i][j] = randomGenerator.nextInt();
            }
        }

        for (int i = 0; i < lotsOfInts.length; i++) {
            String rowAsStr = "";
            //排序
            int[] sorted = getSorted(lotsOfInts[i]);
            //拼接打印
            for (int j = 0; j < lotsOfInts[i].length; j++) {
                rowAsStr += sorted[j];
                if (j < (lotsOfInts[i].length - 1)) {
                    rowAsStr += ", ";
                }
            }
            Log.i("ricky", "Row " + i + ": " + rowAsStr);
        }
    }

最後我們之後是 onClick 中的 imPrettySureSortingIsFree() 函數裏面的 rowAsStr += sorted[j]; 字符串拼接造成的 內存抖動 ,因爲每次拼接一個 String 都會申請一塊新的堆內存,那麼怎麼解決這個頻繁開闢內存的問題了。其實在 Java 中有 2 個更好的 API 對 String 的操作很友好,相信應該有人猜到了吧。沒錯就是將 此處的 String 換成 StringBuffer 或者 StringBuilder,就能很完美的解決字符串拼接造成的內存抖動問題。

修改後

        /**
         *&emsp;打印二維數組,一行行打印
         */
        public void imPrettySureSortingIsFree() {
            int dimension = 300;
            int[][] lotsOfInts = new int[dimension][dimension];
            Random randomGenerator = new Random();
            for(int i = 0; i < lotsOfInts.length; i++) {
                for (int j = 0; j < lotsOfInts[i].length; j++) {
                    lotsOfInts[i][j] = randomGenerator.nextInt();
                }
            }

            // 使用StringBuilder完成輸出,我們只需要創建一個字符串即可,				不需要浪費過多的內存
            StringBuilder sb = new StringBuilder();
            String rowAsStr = "";
            for(int i = 0; i < lotsOfInts.length; i++) {
                // 清除上一行
                sb.delete(0, rowAsStr.length());
                //排序
                int[] sorted = getSorted(lotsOfInts[i]);
                //拼接打印
                for (int j = 0; j < lotsOfInts[i].length; j++) {
                    sb.append(sorted[j]);
                    if(j < (lotsOfInts[i].length - 1)){
                        sb.append(", ");
                    }
                }
                rowAsStr = sb.toString();
                Log.i("jason", "Row " + i + ": " + rowAsStr);
            }
        }

這裏可以看見沒有垃圾桶出現,說明內存抖動解決了。

注意: 實際開發中如果在 LogCat 中發現有這些 Log 說明也發生了 內存抖動 (Log 中出現 concurrent copying GC freed …)

回收算法

ps:我覺得這個只是爲了應付面試,那麼可以參考這裏,我也只瞭解概念這裏就不用在多寫了,點擊看這個帖子吧

也可以參考掘金的這一篇 GC 回收算法

標記清除算法 Mark-Sweep

複製算法 Copying

標記壓縮算法 Mark-Compact

分代收集算法

總結 (只要養成這樣的習慣,至少可以避免 90 % 以上不會造成內存異常)

  1. 數據類型: 不要使用比需求更佔用空間的基本數據類型

  2. 循環儘量用 foreach ,少用 iterator, 自動裝箱也儘量少用

  3. 數據結構與算法的解度處理 (數組,鏈表,棧樹,樹,圖)

    • 數據量千級以內可以使用 Sparse 數組 (Key爲整數),ArrayMap (Key 爲對象) 雖然性能不如 HashMap ,但節約內存。
  4. 枚舉優化

    缺點:

    • 每一個枚舉值都是一個單例對象,在使用它時會增加額外的內存消耗,所以枚舉相比與 Integer 和 String 會佔用更多的內存
    • 較多的使用 Enum 會增加 DEX 文件的大小,會造成運行時更多的 IO 開銷,使我們的應用需要更多的空間
    • 特別是分 Dex 多的大型 APP,枚舉的初始化很容易導致 ANR

    優化後的代碼:可以直接限定傳入的參數個數

    public class SHAPE {
        public static final int TYPE_0=0;
        public static final int TYPE_1=1;
        public static final int TYPE_2=2;
        public static final int TYPE_3=3;
    
        @IntDef(flag=true,value={TYPE_0,TYPE_1,TYPE_2,TYPE_3})
        @Target({ElementType.PARAMETER,ElementType.METHOD,ElementType.FIELD})
        @Retention(RetentionPolicy.SOURCE)
        public @interface Model{
    
        }
    
        private @Model int value=TYPE_0;
        public void setShape(@Model int value){
            this.value=value;
        }
        @Model
        public int getShape(){
            return this.value;
        }
    }
    
    
    
  5. static , static final 的問題

    • static 會由編譯器調用 clinit 方法進行初始化
    • static final 不需要進行初始化工作,打包在 dex 文件中可以直接調用,並不會在類初始化申請內存

    基本數據類型的成員,可以全寫成 static final

  6. 字符串的拼接儘量少用 +=

  7. 重複申請內存問題

    • 同一個方法多次調用,如遞歸函數 ,回調函數中 new 對象
    • 不要在 onMeause() onLayout() ,onDraw() 中去刷新UI(requestLayout)
  8. 避免 GC 回收將來要重新使用的對象 (內存設計模式對象池 + LRU 算法)

  9. Activity 組件泄漏

    • 非業務需要不要把 activity 的上下文做參數傳遞,可以傳遞 application 的上下文
    • 非靜態內部類和匿名內部內會持有 activity 引用(靜態內部類 或者 單獨寫文件)
    • 單例模式中回調持有 activity 引用(弱引用)
    • handler.postDelayed() 問題
      • 如果開啓的線程需要傳入參數,用弱引接收可解決問題
      • handler 記得清除 removeCallbacksAndMessages(null)
  10. Service 耗時操作儘量使用 IntentService,而不是 Service

最後思維導圖做一個總結:

推薦閱讀:2020最新Android大廠高頻面試題解析大全(BAT TMD JD 小米)

2020最新BAT Android高端技術面試145題詳解
2019年鴻洋大神最新整理一線互聯網公司Android中高級面試題總結(附答案解析)
2017-2020歷年字節跳動Android面試真題解析(累計下載1082萬次,持續更新中)

作者:DevYK
鏈接:https://juejin.im/post/5cd82a3ee51d456e781f20ce
來源:掘金

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