編輯:關於Android編程
本篇主要是對 google推出的性能優化典范 進行一個通篇的整理… 主要在於一些具體的優化技巧、至於 60fps、掉幀、gc、內存抖動、阈值…等等這些性能術語的概念裡面不做多概括,請自行查閱…
本篇從以下幾點延伸擴展…
內存對象的洩漏,會導致一些不再使用的對象無法及時釋放,這樣一方面占用了寶貴的內存空間,很容易導致後續需要分配內存的時候,空閒空間不足而出現OOM。
顯然,這還使得每級Generation的內存區域可用空間變小,gc就會更容易被觸發,容易出現內存抖動,從而引起性能問題。
最新的LeakCanary開源控件,可以很好的幫助我們發現內存洩露的情況,更多關於LeakCanary的介紹,請看這裡 https://github.com/square/leakcanary(中文使用說明。另外也可以使用傳統的MAT工具查找內存洩露,請參考這裡(便捷的中文資料)
注意Activity的洩漏
內部類引用導致Activity的洩漏:
最典型的場景是Handler導致的Activity洩漏,如果Handler中有延遲的任務或者是等待執行的任務隊列過長,都有可能因為Handler繼續執行而導致Activity發生洩漏。此時的引用關系鏈是Looper -> MessageQueue -> Message -> Handler -> Activity。為了解決這個問題,可以在UI退出之前,執行remove Handler消息隊列中的消息與runnable對象。或者是使用Static + WeakReference的方式來達到斷開Handler與Activity之間存在引用關系的目的。Activity Context被傳遞到其他實例中,這可能導致自身被引用而發生洩漏:
內部類引起的洩漏不僅僅會發生在Activity上,其他任何內部類出現的地方,都需要特別留意!我們可以考慮盡量使用static類型的內部類,同時使用WeakReference的機制來避免因為互相引用而出現的洩露。 考慮使用Application Context而不是Activity Context; 注意臨時Bitmap對象的及時回收注意緩存容器中的對象洩漏:
有時候,我們為了提高對象的復用性把某些對象放到緩存容器中,可是如果這些對象沒有及時從容器中清除,也是有可能導致內存洩漏的。例如,針對2.3的系統,如果把drawable添加到緩存容器,因為drawable與View的強應用,很容易導致activity發生洩漏。而從4.0開始,就不存在這個問題。解決這個問題,需要對2.3系統上的緩存drawable做特殊封裝,處理引用解綁的問題,避免洩漏的情況。注意WebView的洩漏:
Android中的WebView存在很大的兼容性問題,不僅僅是Android系統版本的不同對WebView產生很大的差異,另外不同的廠商出貨的ROM裡面WebView也存在著很大的差異。更嚴重的是標准的WebView存在內存洩露的問題,看這裡WebView causes memory leak - leaks the parent Activity。所以通常根治這個問題的辦法是為WebView開啟另外一個進程,通過AIDL與主進程進行通信,WebView所在的進程可以根據業務的需要選擇合適的時機進行銷毀,從而達到內存的完整釋放。注意Cursor對象是否及時關閉:
在程序中我們經常會進行查詢數據庫的操作,但時常會存在不小心使用Cursor之後沒有及時關閉的情況。這些Cursor的洩露,反復多次出現的話會對內存管理產生很大的負面影響,我們需要謹記對Cursor對象的及時關閉。謹慎使用large heap:
Android設備根據硬件與軟件的設置差異而存在不同大小的內存空間,他們為應用程序設置了不同大小的Heap限制阈值。你可以通過調用getMemoryClass()
來獲取應用的可用Heap大小。在一些特殊的情景下,你可以通過在manifest的application標簽下添加largeHeap=true的屬性來為應用聲明一個更大的heap空間。然後,你可以通過getLargeMemoryClass()來獲取到這個更大的heap size阈值。然而,聲明得到更大Heap阈值的本意是為了一小部分會消耗大量RAM的應用(例如一個大圖片的編輯應用)。不要輕易的因為你需要使用更多的內存而去請求一個大的Heap Size。只有當你清楚的知道哪裡會使用大量的內存並且知道為什麼這些內存必須被保留時才去使用large heap。因此請謹慎使用large heap屬性。使用額外的內存空間會影響系統整體的用戶體驗,並且會使得每次gc的運行時間更長。在任務切換時,系統的性能會大打折扣。另外, large heap並不一定能夠獲取到更大的heap。在某些有嚴格限制的機器上,large heap的大小和通常的heap size是一樣的。因此即使你申請了large heap,你還是應該通過執行getMemoryClass()來檢查實際獲取到的heap大小。
綜合考慮設備內存阈值與其他因素設計合適的緩存大小:
例如,在設計ListView或者GridView的Bitmap LRU緩存的時候,需要考慮的點有:onLowMemory()與onTrimMemory()
:
Android用戶可以隨意在不同的應用之間進行快速切換。為了讓background的應用能夠迅速的切換到forground,每一個background的應用都會占用一定的內存。Android系統會根據當前的系統的內存使用情況,決定回收部分background的應用內存。如果background的應用從暫停狀態直接被恢復到forground,能夠獲得較快的恢復體驗,如果background應用是從Kill的狀態進行恢復,相比之下就顯得稍微有點慢。
onTrimMemory(int):Android系統從4.0開始還提供了onTrimMemory()的回調,當系統內存達到某些條件的時候,所有正在運行的應用都會收到這個回調,同時在這個回調裡面會傳遞以下的參數,代表不同的內存使用情況,收到onTrimMemory()回調的時候,需要根據傳遞的參數類型進行判斷,合理的選擇釋放自身的一些內存占用,一方面可以提高系統的整體運行流暢度,另外也可以避免自己被系統判斷為優先需要殺掉的應用。下圖介紹了各種不同的回調參數:
TRIM_MEMORY_UI_HIDDEN
:你的應用程序的所有UI界面被隱藏了,即用戶點擊了Home鍵或者Back鍵退出應用,導致應用的UI界面完全不可見。這個時候應該釋放一些不可見的時候非必須的資源 當程序正在前台運行的時候,可能會接收到從onTrimMemory()中返回的下面的值之一:TRIM_MEMORY_RUNNING_MODERATE
:你的應用正在運行並且不會被列為可殺死的。但是設備此時正運行於低內存狀態下,系統開始觸發殺死LRU Cache中的Process的機制。 TRIM_MEMORY_RUNNING_LOW
:你的應用正在運行且沒有被列為可殺死的。但是設備正運行於更低內存的狀態下,你應該釋放不用的資源用來提升系統性能。 TRIM_MEMORY_RUNNING_CRITICAL
:你的應用仍在運行,但是系統已經把LRU Cache中的大多數進程都已經殺死,因此你應該立即釋放所有非必須的資源。如果系統不能回收到足夠的RAM數量,系統將會清除所有的LRU緩存中的進程,並且開始殺死那些之前被認為不應該殺死的進程,例如那個包含了一個運行態Service的進程。
當應用進程退到後台正在被Cached的時候,可能會接收到從onTrimMemory()中返回的下面的值之一:
TRIM_MEMORY_BACKGROUND
: 系統正運行於低內存狀態並且你的進程正處於LRU緩存名單中最不容易殺掉的位置。盡管你的應用進程並不是處於被殺掉的高危險狀態,系統可能已經開始殺掉LRU緩存中的其他進程了。你應該釋放那些容易恢復的資源,以便於你的進程可以保留下來,這樣當用戶回退到你的應用的時候才能夠迅速恢復。 TRIM_MEMORY_MODERATE
: 系統正運行於低內存狀態並且你的進程已經已經接近LRU名單的中部位置。如果系統開始變得更加內存緊張,你的進程是有可能被殺死的。
TRIM_MEMORY_COMPLETE
: 系統正運行於低內存的狀態並且你的進程正處於LRU名單中最容易被殺掉的位置。你應該釋放任何不影響你的應用恢復狀態的資源。
請注意:當系統開始清除LRU緩存中的進程時,雖然它首先按照LRU的順序來執行操作,但是它同樣會考慮進程的內存使用量以及其他因素。占用越少的進程越容易被留下來。
Try catch某些大內存分配的操作:
在某些情況下,我們需要事先評估那些可能發生OOM的代碼,對於這些可能發生OOM的代碼,加入catch機制,可以考慮在catch裡面嘗試一次降級的內存分配操作。例如decode bitmap的時候,catch到OOM,可以嘗試把采樣比例再增加一倍之後,再次嘗試decode。資源文件需要選擇合適的文件夾進行存放
謹慎使用static對象:謹慎使用“抽象”編程
很多時候,開發者會使用抽象類作為”好的編程實踐”,因為抽象能夠提升代碼的靈活性與可維護性。然而,抽象會導致一個顯著的額外內存開銷:他們需要同等量的代碼用於可執行,那些代碼會被mapping到內存中,因此如果你的抽象沒有顯著的提升效率,應該盡量避免他們。謹慎使用依賴注入框架
使用類似Guice或者RoboGuice等框架注入代碼,在某種程度上可以簡化你的代碼。謹慎使用多進程:
使用多進程可以把應用中的部分組件運行在單獨的進程當中,這樣可以擴大應用的內存占用范圍,但是這個技術必須謹慎使用,絕大多數應用都不應該貿然使用多進程,一方面是因為使用多進程會使得代碼邏輯更加復雜,另外如果使用不當,它可能反而會導致顯著增加內存。當你的應用需要運行一個常駐後台的任務,而且這個任務並不輕量,可以考慮使用這個技術。 一個典型的例子是創建一個可以長時間後台播放的Music Player。如果整個應用都運行在一個進程中,當後台播放的時候,前台的那些UI資源也沒有辦法得到釋放。類似這樣的應用可以切分成2個進程:一個用來操作UI,另外一個給後台的Service。使用ProGuard來剔除不需要的代碼
ProGuard能夠通過移除不需要的代碼,重命名類,域與方法等等對代碼進行壓縮,優化與混淆。使用ProGuard可以使得你的代碼更加緊湊,這樣能夠減少mapping代碼所需要的內存空間。謹慎使用第三方libraries:
很多開源的library代碼都不是為移動網絡環境而編寫的,如果運用在移動設備上,並不一定適合。即使是針對Android而設計的library,也需要特別謹慎,特別是在你不知道引入的library具體做了什麼事情的時候。例如,其中一個library使用的是nano protobufs, 而另外一個使用的是micro protobufs。這樣一來,在你的應用裡面就有2種protobuf的實現方式。這樣類似的沖突還可能發生在輸出日志,加載圖片,緩存等等模塊裡面。另外不要為了1個或者2個功能而導入整個library,如果沒有一個合適的庫與你的需求相吻合,你應該考慮自己去實現,而不是導入一個大而全的解決方案。一個處於完全工作狀態的無線電會大量消耗電量,因此需要學習如何在不同能量狀態下進行過渡,當無線電沒有工作時,節省電量,當需要時嘗試最小化與無線電波供電有關的延遲。
典型的 3G 無線電網絡有三種能量狀態:
Full power:當無線連接被激活的時候,允許設備以最大的傳輸速率進行操作。 Low power:一種中間狀態,對電量的消耗差不多是 Full power 狀態下的50%。Standby:最小的能量狀態,沒有被激活或者需求的網絡連接。
low power 到 Full power 大概需要花費1.5秒;
Standby 到 full power 大概需要花費2秒 ;
Full power到 low power 大概需要花費5秒;
low power 到 Standby 大概需要花費12秒;
優先在無線網絡狀態下做耗時操作(視頻、文件下載…):
使用無線電量消耗低於使用移動流量使用預取(prefetching)與捆綁(bundle)的方式進行數據的傳輸,這些操作都是為了最小化電量的消耗。
如:
充電狀態或者電量高於20%直接播放視頻 當電池低於20%時 給出提示“請充電…” ….高效的保留更多的電量與不斷促使用戶使用你的App會消耗電量,這是矛盾的選擇題。不過我們可以使用一些更好的辦法來平衡兩者。
假設你的手機裡面裝了大量的社交類應用,即使手機處於待機狀態,也會經常被這些應用喚醒用來檢查同步新的數據信息。Android會不斷關閉各種硬件來延長手機的待機時間,首先屏幕會逐漸變暗直至關閉,然後CPU進入睡眠,這一切操作都是為了節約寶貴的電量資源。但是即使在這種睡眠狀態下,大多數應用還是會嘗試進行工作,他們將不斷的喚醒手機。一個最簡單的喚醒手機的方法是使用PowerManager.WakeLock的API來保持CPU工作並防止屏幕變暗關閉。這使得手機可以被喚醒,執行工作,然後回到睡眠狀態。知道如何獲取WakeLock是簡單的,可是及時釋放WakeLock也是非常重要的,不恰當的使用WakeLock會導致嚴重錯誤。例如網絡請求的數據返回時間不確定,導致本來只需要10s的事情一直等待了1個小時,這樣會使得電量白白浪費了。這也是為何使用帶超時參數的wakelock.acquice()方法是很關鍵的。
但是僅僅設置超時並不足夠解決問題,例如設置多長的超時比較合適?什麼時候進行重試等等?解決上面的問題,正確的方式可能是使用非精准定時器。通常情況下,我們會設定一個時間進行某個操作,但是動態修改這個時間也許會更好。例如,如果有另外一個程序需要比你設定的時間晚5分鐘喚醒,最好能夠等到那個時候,兩個任務捆綁一起同時進行,這就是非精確定時器的核心工作原理。我們可以定制計劃的任務,可是系統如果檢測到一個更好的時間,它可以推遲你的任務,以節省電量消耗。
這正是JobScheduler API所做的事情。它會根據當前的情況與任務,組合出理想的喚醒時間,例如等到正在充電或者連接到WiFi的時候,或者集中任務一起執行。我們可以通過這個API實現很多免費的調度算法。
Battery Historian是Android 5.0開始引入的新API。通過下面的指令,可以得到設備上的電量消耗信息:
$ adb shell dumpsys batterystats > xxx.txt //得到整個設備的電量消耗信息
$ adb shell dumpsys batterystats > com.package.name > xxx.txt //得到指定app相關的電量消耗信息
得到了原始的電量消耗數據之後,我們需要通過Google編寫的一個python腳本把數據信息轉換成可讀性更好的html文件:
$ python historian.py xxx.txt > xxx.html
打開這個轉換過後的html文件,可以看到類似TraceView生成的列表數據,這裡的數據信息量很大,這裡就不展開了。
由於時間關系先寫到這裡,待續…
之前自己的編程完全是在PC上進行的,而且主要是在算法和數據結構上。由於某些需要加之認識到Android的重要性,且大學走到現在基本上沒什麼課了,空閒時間很多,於是就開始學
Heap Viewer,Memory Monitor和Allocation Tracker是用來可視化你的app使用內存的補充工具。使用Memory Monitor To
經過上一篇的實驗,我門只是僅僅對View的事件的傳遞進行了分析,但是還有一個比較厲害的ViewGroup我們肯定是要說一下的,ViewGroup的二叉視圖分析 我們能看到
Android作為一個偉大的系統,自然提供了設置默認打開程序的實現.在這篇文章中,我會介紹如何在Android系統中設置默認的程序. 在設置默認程序之前,無非有兩種情況,