一.通用規範:
1.避免對反射的資源進行混淆
說明:需要進行反射的資源不能混淆,無論是字段、方法還是類(這種情況一般多出現在使用Parcelable的場景)。
案例:進入過電話界面之後,進入設置--應用程序,清除電話的數據,手機彈出文件停止運行
02-28 11:56:30.651 9720 9843 E AndroidRuntime: Caused by: android.os.BadParcelableException: Parcelable protocol requires a Parcelable.Creator object called CREATOR on class com.android.documentsui.model.RootInfo 原因分析:android 7.1上,系統app被混淆導致DocumentsUI崩潰。詳見STD1616 [B170228-338]
2.同一個功能如果需要方便的打開或者關閉,需提煉出一個總開關,確保只需修改一處
說明:無
案例:相機修改一個水印開關需修改6個類,有些類還需要修改多處,如下圖
邏輯調整後只需修改一個默認值即可(修改res/values/strings_nottranslat.xml裏面的默認值)滿足功能快速開關,1s完成切換。
3.使用某個函數的返回值,如果不能肯定該函數不會返回null的話,要加判空處理
說明:使用某個函數的返回值,而該函數有可能返回null
案例:
Bitmap bitmap = Bitmap.createBitmap(…); int width = bitmap.getWidth();
4.直接使用傳入的參數,要對參數進行檢查
說明:
1)對傳入的參數進行檢查; 2)字符串比較時,將常量寫在前面
案例:
String state = intent.getStringExtra(“key”); if(state.equals(“ready”)) { }
5.要小心識別初始化時的運行態,避免在重要對象初始化時加判斷條件或延時處理
說明:
對於包含handler的對象,需要特別注意該對象的初始化,當該對象在線程中初始化時可能會出現問題。特別的,由於存在調用與被調用的關係,有些初始化是很隱藏的。
建議:
1)使用一個成員變量前,先檢查它的初始化的地方,保證它是賦值過的。 2)在構造函數中,如果成員變量耗時/耗內存不多,儘可能初始化它的默認值 3)避免延遲初始化變量,如: new Handler().postDelayed( { mService = …… }, 1000);
6.延遲一段時間執行操作時,要重新檢查變量的有效性
說明:
案例:異步處理變量,在執行該變量的操作之前,又被別的線程賦值爲null
7.提前finish並return,會導致某些變量初始化或註冊等操作未執行完,引起後面空指針
說明:滿足某個條件return後,要檢查後面的代碼執行是否有影響,如未釋放資源等。
案例:
建議:插入這類代碼時,檢查其下方變量的初始化及其影響。
8.單例類中創建的對象要注意釋放
說明:把一個對象(如view、Context、Listener、callback等)傳給單例,而單例將這個對象保存起來,並且最終沒有釋放的話,就會引起內存泄漏。
案例:
二. 多線程
請參閱《Efficient Android Threading》,其中講解了Android中如何進行異步處理。 https://developer.android.com/training/articles/perf-anr.html
1.線程資源儘量通過線程池提供,不建議在應用中自行顯式創建線程。
說明:這樣的處理方式更加明確線程池的運行規則,規避資源耗盡的風險。
使用線程池的好處是減少在創建和銷燬線程上所花的時間以及系統資源的開銷,解決資源不足的問題。如果不使用線程池,有可能造成系統創建大量 同類線程而導致消耗完內存或者“過度切換”的問題。
2.system_server進程中儘量不要啓動新的Thread
說明:
公司手機上system_server中現有Thread已經很多,進程變的越來越龐大。如果自有業務都去增加Thread,system_server的負載會變的越來越重, 其實Google針對這個問題進行了很好的設計和思考,Android System _server引入了分類Thread(android.fg,android.bg,android.ui,androud.io)。
建議方案:
當你在system_server進程中計劃啓動HandlerThread或者Thread時候,看看能否複用已有的Thread進行。
參考代碼:
PrintManagerService.java: BackgroundThread.getHandler().post(new Runnable())... LocationManagerService.java: mLocationHandler =new LocationWorkerHandler(BackgroundThread.get().getLooper());
檢測工具: 暫無,後續可以用UT冒煙測試用例點檢。
3.獲取單例對象要線程安全,在單例對象裏面做操作也要保證線程安全。
說明:
資源驅動類、工具類、單例工廠類都需要注意。
4.多線程中的資源需保持同步。
說明:
多線程讀寫全局變量,需要加鎖;如果都是讀,可以不加鎖;對於多線程導致的空指針、數組越界問題, 建議將全局變量深度拷貝(clone爲淺拷貝)到局部變量,然後判斷使用該局部變量(對於集合類型不可以簡單地使用“=”局部化,需要這樣局局部化a = new set(b))。
案例:
(1)在應用的生命週期onPause裏面有個子線程發送一個250ms的延時消息執行屏幕色彩相關的操作setDefaultMode(),其中包括對象 ColorManager的使用。在生命週期onDestroy的主線程release()裏面釋放了對象ColorManager。 從代碼設計邏輯上看,是有較 明顯的缺陷的:release()很可能比setDefaultMode先執行,從而報空指針,應用程序崩潰。 反思:1)對象的申請以及其他相關的操作跟對象的釋放不在同一條線程中(初始化與釋放邏輯不在同一線程,可能會出現釋放出錯情況)。 2)延時消息具有不穩定性,應慎用。 改進建議: 1)對象的申請以及其他相關的操作跟對象的釋放需要在同一線程或者採用同步,以保證時序正確。 2)如果非得加延時,那麼對象的釋放操作也應該放在該延時裏面進行。
(2)對關鍵的資源閃光燈進行操作沒有做同步處理,導致重複操作閃光燈引起ANR,詳見: http://192.168.2.91:8000/rdms/qm/bug/bugAction.do?action=getBugDetail&id=1070294&popup=true
(3) 多線程操作全局變量
5.避免子線程和主線程持有同一把鎖執行耗時操作。
說明:
案例:系統用戶界面死鎖崩潰問題
1)原因分析:系統用戶界面應用的RecentHelper中mRecentTaskItems的列表更新與主線程使用了同步鎖。 2)解決方案:取消同步鎖,在子線程中使用局部變量承載數據庫列表,然後把局部變量值轉至主線程更新全局變量列表。
6.避免主線程的耗時操作。
說明:
通常的耗時操作有:網絡操作、IO操作、數據庫操作、Binder調用、反射調用、Bitmap操作及頻繁循環查詢等; Application的onCreate、Service的onCreate&onBind&...、Receiver的onReceive等,都默認運行在主線程; 在主線程通過Settings.System.putInt寫數據應減少調用次數,如加判斷在需要時才執行,建議putInt異步執行; 避免應用主線程調用getPhoneStorageState()等可能由於系統原因引起阻塞的接口。
案例:VoLTE導致ANR
(1)原因分析:一方面,SIM卡狀態變化、切換數據卡事件非常頻繁;另一方面,在主線程查詢短信中心號碼,需要從modem中查詢數據,較慢。 (2)反思:爲什麼發現不了這個問題? 1)開發階段——人員、需求變更頻繁,新的開發人員對模塊不夠熟悉,代碼風格和可讀性差,影響新的人員儘快熟悉代碼。 2)DR階段——評審時,主要關注需求是否滿足,不夠重視非功能特性(如性能等),沒有充分評估廣播的頻繁度,代碼執行時間。 3)測試階段——BG ANR問題的隱蔽性。 4)跟蹤階段——雲診斷數據突變很明顯,但組內沒有關注ANR的數據。
7.線程泄露
說明:
反例:相機線程泄露
1)原因分析:new AsyncTask<...>() {}.executeOnExecutor(Executors.newCachedThreadPool()); 因爲newCachedThreadPool會一直創建不同的對象導致線程泄露。 2)解決方案:Executors.newCachedThreadPool()用靜態常量代替,即換成AsyncTask.THREAD_POOL_EXECUTOR或者 定義一個static類型的對象Executors.newCachedThreadPool()作爲參數傳入進去: private static ExecutorService Cached_TASK_EXECUTOR = (ExecutorService) Executors.newCachedThreadPool();
8.CountDownLatch
說明:
使用CountDownLatch進行異步轉同步操作,每個線程退出前必須調用countDown方法; 線程執行代碼注意catch異常,確保countDown方法可以執行,避免主線程無法執行至countDown方法,直到超時才返回結果。 注意,子線程拋出異常堆棧,不能在主線程try-catch到。
9.HandlerThread在onDestroy或unregister等釋放操作時,要調用quit()退出
說明:
HandlerThread會創建一個線程,除非主動調用quit(),否則是不會退出的;詳見此鏈接
案例:相機中使用的HandleThread沒有主動quit,引發內存泄漏。
CameraActivity.mInitializeThread,多次進入相機然後按返回鍵,CameraActivity來回多次執行onCreate--onDestoty可復現。
Application
1.基本概念
說明:
Application可以說是單例 (singleton)模式的一個類,且application對象的生命週期是整個程序中最長的,它的生命週期就等於這個程序的生命週期。 因爲它是全局的單例的,所以在不同的Activity,Service中獲得的對象都是同一個對象,因此在安卓中我們可以避免使用靜態變量來存儲長久保存的值,而用Application。 可通過Application來進行一些數據傳遞、數據共享及數據緩存等操作。
使用場景:
通常Application全局對象是通過Context或者Activity的getApplicationContext()方法獲得的,在activity中這樣做: appContext = (AppContext)this.getApplicationContext(); 如果有Context對象,還可以:appContext = (AppContext)mContext.getApplicationContext(); 但是很多時候我們的代碼可能在activity之外,且沒有context對象的引用,但是又需要獲得AppContext對象,原始的做法 可能是想辦法將activity或者context傳遞到需要調用的地方,但是這樣代碼耦合度太高,可讀性差。 Application生命週期函數中的onCreate是被自動調用的,可以利用這點來獲得這個Application對象: private static AppContext instance; public static AppContext getInstance() { return instance; } @Override public void onCreate() { // TODO Auto-generated method stub super.onCreate(); instance = this; } 這樣我們就能在app工程的任何地方通過AppContext.getInstance()來獲得Application全局對象。比如我定義了一個工具類, 在工具類中我們需要使用Context的getExternalFilesDir()方法,但是這個工具類沒有直接的辦法獲取到context,於是我們可以: return AppContext.getInstance().getExternalFilesDir(null);
2.避免在Application的onCreate中執行比較耗時的操作
說明:
如果在Application的onCreate中執行比較耗時的操作,將直接影響的程序的啓動時間。
案例:
3.通過Application在兩個Activity間傳遞數據需防範內存泄露
說明:
在Application中創建一個HashMap<String,Object> ,以字符串爲索引,Object爲value這樣我們的HashMap就可以存儲任何類型的對象了。 在Activity A中把需要傳遞的對象放入這個HashMap,然後通過Intent或者其它途經再把這人索引的字符串傳遞給Activity B ,Activity B 就可以根據這個字符串在HashMap中取出這個對象了。只要再向下轉個型 ,就實現了對象的傳遞。
反思:
數據傳遞完成之後,把存放在application的HashMap中的數據remove掉,以免發生內存泄漏。
4.自定義了Application後,必須在manifest中修改application標籤屬性
說明:
爲了更好的利用Application的這一特性,比如我們需要Application來保存一些靜態值,需要自定義繼承於Application的類,然後在這個類中 定義一個變量來保存。在默認情況下應用系統會自動生成Application對象,但是如果我們自定義了Application,那就需要告知系統,實例化的時候, 是實例化我們自定義的,而非默認的。
Log使用注意事項
1.避免易被忽略的空指針問題
說明:Log.d(TAG, null); 這個會報空指針
案例:Log.d(TAG, e.getMessage()); e.getMessage()這個可能爲null進而導致空指針問題。