五年 Android 開發,讓我 “刻骨銘心” 的那些坑
前言
這篇文章是本人對在開發過程中踩坑經歷的一次總結;分為系統API的坑、使用不當導致的坑、開源項目中的坑等幾個方面,知識面有限,認知難免會有偏頗,如發現有問題還請指正。
1.系統API的坑
-
Android library中的資源ID在R.java中不是final類型:
問題現象:在library中使用switch語句區分不同的資源ID時,IDE會報錯;
原因分析:這個問題在Android Studio Project Site (http://tools.android.com/tips/non-constant-fields) 有提及,在ADT14及以上的版本中,library所對應的R.java中所有ID不再是final類型,所以不能將ID作為switch語句中的case分支屬性值。這個問題和IDE無關,在Eclipse和AS中都存在。
解決方案:如果涉及到區分多個ID的情況(比如監聽回調事件、初始化通過xml給自定義View設置的屬性值等)應該使用if...else if...else代替switch語句;
-
同一個程序內的多個進程之間使用SharedPreferences不安全:
問題現象:在同一個程序內使用多進程時,在不同進程間使用SharedPreferences操作數據會導致SF中的數據隨機丟失的情況(獲取到的值為空);
原因分析:雖然API中提供了Context.MODE MULTI PROCESS模式打開SF文件,但在官方文檔 (https://developer.android.com/reference/android/content/SharedPreferences.html) 中已有說明:“currently this class does not support use across multiple processes”,因為SF在多進程間操作是不安全的,并行操作時會導致寫沖突。
解決方案:Github上有個開源項目Tray (https://github.com/grandcentrix/tray) ,專門針對在多進程中使用SF不安全的問題提供的解決方案。
-
Typeface初始化自定義字體慢:
問題現象:在使用自定義字體的頁面,進入慢;
原因分析:使用Typeface初始化字體很耗時,至少需要100ms(不同文件耗時不一樣)以上的時間。
解決方案:如果在Activity的onCreate方法中初始化Typeface,會導致進入Activity慢,出現黑屏/白屏現象,所以應該盡量在非UI線程中做自定義字體的初始化操作。
-
Activity在沒有完全顯示/已退出的情況下顯示PopupWindow異常:
問題現象:進入Activity界面直接報錯,log異常顯示為:"Unable to add window -- token null is not valid";
原因分析:原因是在Activity的onCreate方法中直接顯示了PopupWindow導致,PopupWindow的顯示是依附在某一個View上面的(showAtLocation方法第一個參數為需要依附的view),在Activity沒有完全顯示時,PopupWindow無法依附在該View上,如果在此時顯示PopupWindow會導致上面的異常,同樣在退出Activity后也不能正常顯示PopupWindow。
解決方案:在開發過程中需要考慮通過異步顯示PopupWindow,避免PoupWindow顯示報異常的問題。
-
Activity的onDestory方法調用時機不確定:
問題現象:連續進入、退出某一個Activity,會出現Activity Crash掉的現象;
原因分析:在Activity的onCreate做的初始化操作(打開文件),在onDestory做的銷毀操作(關閉文件);退出Activity后onDestory并沒有立即調用,再次快速進入該Activity時,該Activity是另外一個實例,并且首先調用了新Activity的onCreate方法之后再才調用上個Activity實例的onDestory方法,導致文件剛被打開就關閉了,在程序使用數據時Crash掉;
解決方案:準確來講只要是系統方法,調用時機都不確定。對于這種問題只能盡量不要在Activity的系統回調方法中做資源初始化和釋放的操作,比如涉及到IO操作的情況,在使用的時候才打開,使用完后立即關閉;
-
透明主題導致Activity生命周期回調的變化:
問題現象:從當前Activity跳轉到其它Activity時,當前Activity的onStop方法并沒有調用;
原因分析:給當前Activity設置為透明主題導致,通過添加打印跟蹤發現,從該Activity跳轉到其它Activity時,該Activity的onStop方法不會執行;
解決方案:謹慎使用透明主題,如果必須要為Activity設置為透明主題,不要在onStop方法中做任何操作,因為該方法并不會被調用。透明主題存在很多問題,比如在設置為透明主題的界面按Home按鍵時,會存在界面刷不干凈的情況。
-
不要通過Bundle傳遞很大塊的數據:
問題現象:從目錄界面跳轉到內容顯示界面,出現隨機崩潰的現象,報的異常是:TransactionTooLargeException;
原因分析:跟蹤發現如果通過Bundle傳遞太大塊(>1M)的數據會在程序運行時報TransactionTooLargeException異常,具體原因可以上官網查看,大致意思是傳遞的序列化數據不能超過1M,否則會報TransactionTooLargeException異常;之所以隨機是因為每次傳遞的數據大小不一樣。
解決方案:如果你在不同組件之間傳遞的數據太大,甚至超過了1M,為了提高效率和程序的穩定性,建議通過持久化的方式傳遞數據,即在傳遞方寫文件,在接收方去讀取這個文件;
-
不要在Application類中緩存數據:
問題現象:程序從后臺切換到前臺,直接崩了;
原因分析:程序在后臺時,為了給正在運行的程序提供更多可使用的內存,Application中的數據可能會被清理掉,如果在Application中緩存了數據,并且在程序重新回到前臺時沒有做好恢復工作,程序會出現不可預見的情況(比如數據錯亂、崩潰等),具體可以參照這篇文章Don't Store Data in the Application Object;
解決方案:不要在Application中緩存數據。
-
使用AsyncTask無法避開的坑:
問題現象:使用AsyncTask異步執行的任務并沒有立即執行;
原因分析:AsyncTask這個類的實現可謂一波三折,方案修改了好幾個版本,初次引入這個類時,所有的Task是放在一個獨立的后臺線程中執行的,也就是如果有多個Task同時被調用也是順序執行的;從1.6開始,改為通過線程池可以支持并行執行多個Task;但從3.0開始,又改回只有一個獨立的后臺線程執行所有Task,主要是為了避免多個Task并行執行導致的程序錯誤,但為了讓AsyncTask能夠支持多個Task并行執行,從3.0起,增加了executeOnExecutor方法,調用者自行實現線程池可以達到并行多個Task的效果。
解決方案:如果在某個地方需要同時執行多個異步任務,強烈建議使用線程池;
-
數據庫升級中的坑:
問題現象:在數據庫的某個表中增加/修改了某個字段后,程序在運行時崩潰掉了;或者在增加字段時修改了數據庫的版本號,但程序升級后,原來的數據丟失了;
原因分析:SQlite數據庫升級時需要修改OpenHelper中的版本號,并且數據庫升級會刪掉原來數據庫中的數據,需要手動將原數據庫中的數據拷貝到高版本的數據庫中;
解決方案:做好數據庫升級的恢復工作,避免出現崩潰、數據丟失的情況。
-
程序在未啟動的情況下,靜態注冊的廣播無法收到消息:
問題現象:程序添加了對開機廣播的監聽,但無法接收到;
原因分析:這個問題只有在程序安裝但沒有啟動時才會出現,只要程序啟動過一次后就不會有這個問題。并且只有在Android 3.1及以上的版本才會出現,具體原因是:從Android3.1開始,新安裝的程序會被置于"stopped"狀態,并且只有在至少手動啟動這個程序一次后該程序才會改變狀態,能夠正常接收到指定的廣播消息。Android這樣做的目的是防止廣播無意或者不必要地開啟未啟動的APP后臺服務。也就是說在Android3.1及以上的版本,程序在未啟動的情況下通過應用自身完成一些操作是不可能的,但Android提供了一種借助其它應用發送指定Flag廣播的方式,達到應用在未啟動的情況下仍然能夠收到消息的效果。從Android 3.1開始,系統給Intent定義了兩個新的Flag,分別為FLAG INCLUDE STOPPED PACKAGES(表示包含未啟動的App)和FLAG EXCLUDE STOPPED PACKAGES(表示不包含未啟動的App),用來控制Intent是否要對處于停止狀態的App起作用。
解決方案:只能借助其它應用給自己發送帶FLAG_INCLUDE STOPPED PACKAGES標志的廣播才能實現在程序未啟動的情況下接收到廣播;
-
android:windowBackground導致的過渡繪制問題:
問題現象:界面的布局已無法進一步優化,但仍然存在過渡繪制的問題;
原因分析:window存在默認的背景,會增加過渡繪制的可能。Activity是依附在Window上的,如果給Activity設置了背景,并且沒有去掉window的背景,很容易導致過渡繪制;這里還有一個坑,有的應用為了避免程序冷啟動時出現黑屏/白屏的問題,在主題中給window設置了背景,并且在Activity的布局中給Activity也設置了背景,這會導致當前界面存在兩個背景,占用了雙倍的內存,并且還會有過渡繪制的問題。程序啟動黑屏應該去優化性能問題,而不是采用給window設置背景的方式;
解決方案:可以通過給Activity自定義主題,在主題中去掉window的默認背景,即: @null ;
-
類的finalize方法調用時機不確定:
問題現象:程序隨機崩潰;
原因分析:多個地方用到了同一個類,該類用于對數據的IO操作,打開文件后并沒有立即關閉,也沒有釋放資源的public方法,主要通過類的finalize方法關閉文件,釋放資源;
解決方案:finalize方法的調用時機是不確定的,不要指望通過該方法釋放與類相關的資源,避免出現隨機的bug;
-
Fragment isAdded:
問題現象:程序隨機崩潰;
原因分析:跟蹤異常log發現,是因為Fragment沒有完全顯示或者已經離開Fragment的情況下,導致的異常,這類異常的主要原因是:使用Fragment時,通過異步操作(比如回調、非UI線程等)更新Fragment的狀態,但此時Fragment沒有完全顯示或者已經離開Fragment;
解決方案:在調用Fragment的方法之前,強烈建議調用isAdded方法判斷Fragment是否依附在Activity上,避免出現異常。
-
Fragment hide、show被調用時,生命周期不會回調:
問題現象:同一界面不同Fragment之間切換時,并沒有觸發一些動態效果,比如播報音頻、顯示切換動畫等;
原因分析:Fragment hide、show被調用時,系統并不會調用Fragment的生命周期回調;
解決方案:不同Fragment之間切換時,主動調用各個Fragment的生命周期回調;
2.使用不當造成的坑
-
9圖不要用tinypng壓縮:
問題現象:使用壓縮工具壓縮9圖后,顯示變形;
原因分析:9圖除了圖片信息外,還存儲一些Android在顯示9圖過程中需要用到的必要信息,通過壓縮工具壓縮圖片會改變文件的信息,9圖被壓縮后程序能顯示,但顯示的效果無法達到預期,因為拉伸信息丟失了。
解決方案:9圖文件本身就不大,沒必要壓縮;
-
同一設備上,相同程序的圖片放在不同drawable文件夾下,占用內存不一樣:
問題現象:程序剛啟動就占用了很高的內存;
原因分析:圖片放置位置不合理導致的,程序在不同的設備中運行時,會根據設備的分辨率和屏幕密度去從與之分辨率匹配的資源文件夾中取圖片,如果沒有對應分辨率的文件夾,則從相近分辨率的文件夾中取,但圖片會被拉伸到當前設備屏幕的寬高,所以會存在圖片被放大或者縮小的問題,導致占用內存會隨之變化,具體可以查看這篇博客關于Android中圖片大小、內存占用與drawable文件夾關系的研究與分析 (http://blog.csdn.net/zhaokaiqiang1992/article/details/49787117) ;
解決方案:為了減少UI的工作量,并且減少APK的內存占用的方法是讓UI出一套高分辨率版本的圖片,放在hdpi文件夾下。
-
Adapter ViewHolder緩存導致顯示錯亂的坑:
問題現象:ListView每一項在滑動的過程中內容顯示錯亂;
原因分析:在Adapter的getView方法中通過position更新每一項的內容時,對于根據判斷條件給每一項設置屬性的情況,每個判斷條件下都需要給每一項的每個屬性賦值,否則在滑動ListView或GridView時會導致內容錯亂;
解決方案:在getView方法里面,給每一項都要設置對應的屬性,比如給每一項的頭像設置圖片,如果某一項沒有頭像,不能不設置,應該設置為透明,否則會錯亂。
-
Toast連續顯示時長時間不消失:
問題現象:多個Toast同時顯示時,Toast一直顯示不消失,退出程序了仍然顯示;
原因分析:看Toast的源碼可以發現,同時顯示多個toast時是排隊顯示的,所以才會出現同時顯示多個Toast時很久都不消失的情況;
解決方案:這屬于體驗問題,很多應用都存在。建議定義一個全局的Toast對象,這樣可以避免連續顯示Toast時不能取消上一次Toast消息的情況(如果你有連續彈出Toast的情況,避免使用Toast.makeText);
-
build.gradle中的versionName和versionCode:
問題現象:從Eclipse轉到AS的項目,在機器上運行時報版本比之前APK版本低的錯誤;
原因分析:從Eclipse轉到AS的過程中,如果你是通過AS直接新創建的一個工程,注意模板會在build.gradle中給程序設置默認versionName和versionCode為1,如果AndroidManifest.xml中的versionCode、versionName比build.gradle中的更高,會導致因為版本問題安裝不上的情況(報INSTALL_FAILED VERSION DOWNGRADE錯誤);
解決方案:只在build.gradle中設置版本名和版本號;
-
AS中依賴包的動態更新:
問題現象:依賴包頻繁更新,因為AS編譯有緩存,每次更新都需要修改依賴包的版本號,特別麻煩,特別是依賴關系比較復雜的情況下;
解決方案:在AS中,如果你想動態同步一個依賴包的更新,可以在依賴包的最后面寫上“+”,比如:compile 'com.android.support:appcompat-v7:23.0.+' ,但這種方法需要謹慎使用,否則會因為依賴包的變動導致你的項目不穩定: Don't use dynamic versions for your dependencies (https://link.zhihu.com/?target=http%3A//blog.danlew.net/2015/09/09/dont-use-dynamic-versions-for-your-dependencies/) ;
-
AS中同一個工程module太多導致編譯慢:
問題現象:編譯一個工程要好幾分鐘,特別是clean的時候,經常10分鐘以上;
原因分析:其實這個很好理解,每個module中都有一個build.gradle,編譯的時候,每個module的build.gradle中的task都需要執行,所以編譯時間會很長。
解決方案:要解決這個問題很簡單,將不經常變動的module打包成aar,主工程依賴aar而不是module,這樣避免了每次都需要重新編譯module的情況。
-
頻繁的GC操作導致程序卡頓:
問題現象:通過AS Monitor觀察應用運行過程中的內存抖動厲害,通過GPU呈現模式觀察每一幀的曲線差別很大,整體感受程序運行時不流暢;
原因分析:在2.3之前GC操作是不能并發進行的,也就是系統正在進行GC程序就只能阻塞住等待GC結束,在2.3之后GC操作改成了并發的方式進行,GC過程中不會影響程序的正常運行,但在GC操作的開始和結束還是會短暫阻塞一段時間,所以頻繁的GC會導致使用應用的過程中卡頓。
解決方案:為了應用在使用過程中更流暢,需要盡量減少觸發GC操作,這涉及到性能優化,對于靜態代碼的分析,AS已經很強大了,可以使用Android Studio的Analyze→Inspect Code...進行分析;
-
TextView 的setText方法,如果傳入一個數字會直接當作字符串資源ID處理:
問題現象:程序運行時報“NotFoundException”異常;
原因分析:TextView.setText(int value)的傳值有問題,在xml文件中沒有找到id對應的字符串;
解決方案:給TextView設置文本的時候一定要轉成String或者Charsequence類型,避免TextView將setText中的參數當做字符串資源ID處理,去加載字符串資源,因為字符串在xml文件中不存在導致程序運行時崩潰。
-
通過反射訪問方法和字段的效率大不一樣:
問題現象:程序運行卡、慢;
原因分析:在一個循環中使用到了反射,并且是調用的反射方法,改成反射字段后,卡、慢的現象得到明顯的改善;
解決方案:通過反射修改或者獲取類中的某個屬性時,強烈建議使用訪問字段的方式,不要使用訪問方法的方式,這兩者之間的效率相差很大,親測訪問方法是訪問字段耗時的1.5倍,具體情況和類的復雜度有關。
-
.nomedia文件的使用:
問題現象:程序中的緩存文件在相冊、音樂播放器中顯示;
原因分析:相冊、音樂播放器等多媒體應用是讀取媒體庫中的數據,而程序的緩存文件被緩存到了媒體數據庫中;
解決方案:如果你希望自己應用生成的數據不被媒體庫掃描到,應該在生成數據的文件夾下創建一個名為".nomedia"的隱藏文件,避免出現一些無意義的文件也被媒體庫掃描到的情況,比如APP的緩存圖片在相冊中顯示、宣傳視頻在視頻播放器中顯示、音效在音樂播放器中顯示等。
-
循環動畫:
問題現象:在不待機的情況下,長時間處于一個界面時,手機發燙;
原因分析:界面中存在循環動畫,CPU、GPU一直在工作;
解決方案:循環動畫會導致界面一直在刷新,CPU、GPU持續工作,會有功耗問題,建議拒掉這種視覺呈現效果。
-
謹慎使用aaptOptions.cruncherEnabled = false;aaptOptions.useNewCruncher = false;
問題現象:編譯生成的APK文件特別大,超過了正常的大小;
原因分析:解壓APK發現,主要是圖片資源導致,將APK中的res文件夾和源碼下的res文件夾對比,發現多了很多圖片文件;跟蹤原因發現最新的buildtools對資源文件的檢測很嚴格,對于Eclipse轉AS的項目,很多時候都是因為圖片問題導致在AS上編譯不過,比如將jpg強轉為png在AS上就編譯不過,在項目中可以在build.gradle中加上這兩句:aaptOptions.cruncherEnabled = false;aaptOptions.useNewCruncher = false,屏蔽掉aapt對圖片的嚴格檢測。但需要謹慎使用這兩個屬性,否則可能會導致編譯生成的APK特別大(解壓生成后的APK發現,對于有問題的圖片,每個drawable文件夾下都會拷貝一份);
解決方案:去掉屬性設置,解決編譯問題。
3.開源項目中的坑
-
FancyCoverFlow:這個控件在API高于16的設備中,滑動的過程中會強制刷新一遍,導致切換和初始化的時候都很卡,當時覺得這個效果挺好,后來用上之后這個控件成了性能瓶頸;

-
Fresco:這個控件用起來特別爽,唯一的缺陷的相比于相同功能的其它開源項目(Glide、Picasso),體積過大;
-
ActiveAndroid:這個輕量級的數據庫框架也挺好用,但缺陷是初始化耗時,可以看一下這篇文章:在Android中使用反射到底有多慢?
-
JXL:一個讀寫Excel文件的開源庫,用起來很方便,但有個問題:文件大小超過5M直接掛掉;
-
JPinyin:漢字轉拼音的一個工具庫,APK加密后這個庫不能正常使用,后來查出是因為項目中數據的問題,加密后數據的內容變化了,最后只能自己改造,將數據按照我們自己的方式處理。
結束語
在工作過程中肯定會遇到很多問題,雖然網絡發達,但親力親為去解決問題會讓自己對各個知識點的理解更深刻,工作經驗就是一個一個坑填過來的,上面的總結只是冰山一角,強烈推薦看一看 StackOverflow 和 Android Issue Tracker 上關于android標簽的熱點問題,里面都是開發過程中可能會遇到的問題,非常值得一讀。
來自:http://mp.weixin.qq.com/s?__biz=MzIwNjQ1NzQxNA==&mid=2247483658&idx=1&sn=451a063ef5bf3f3689e5af6153762fcd&scene=1&srcid=081912jNN9TJLf5BeZgdjTvl#rd