Android實際開發中的bug總結與解決方法(一)


                                                                             Android實際開發中的bug總結與解決方法(一)

Android開發中有很多bug,我們是完全可以在線下避免的,不要等到線上報的BUG的再去修復。下面是我在實際開發中遇到過的bug和解決方法。

BUG 1: 
java.lang.RuntimeException: Unable to start activity ComponentInfo {com.netease.caipiao.ssq/com.netease.caipiao.ssq.ExpertListActivity}: 
 android.support.v4.app.Fragment$InstantiationException:  Unable to instantiate fragment  com.netease.caipiao.ssq.tab.ExpertsListFragment: 
  make sure class name exists, is public, and has an empty constructor that is public
 
 復現:當app啟動后,進入異常頁面,然后使其進入后台進程(按home鍵),接着改變系統設置如字體大小等方法,目的上讓app被系統殺死后恢復重現,這時候再點擊app進入應用,拋出異常。
問題描述:包含有fragment的Activity在異常被銷毀(如系統內存不足等)后,再進入恢復activity時,重新實例化fragment時拋出異常出錯。異常的原因就是因為使用的fragment沒有public的empty constructor。
查看源代碼知:fragment在還原狀態中調用FragmentState#instantitae()->Fragment#instantitae()拋出異常。
具體Android源碼中拋出的異常代碼如下:
   
   
   
   
           
  1. [java] view plain copy
  2. /**
  3. * Create a new instance of a Fragment with the given class name. This is
  4. * the same as calling its empty constructor.
  5. */
  6. public static Fragment instantiate(Context context, String fname, Bundle args) {
  7. try {
  8. Class<?> clazz = sClassMap.get(fname);
  9. if (clazz == null) {
  10. // Class not found in the cache, see if it's real, and try to add it
  11. clazz = context.getClassLoader().loadClass(fname);
  12. sClassMap.put(fname, clazz);
  13. }
  14. Fragment f = (Fragment)clazz.newInstance();
  15. if (args != null) {
  16. args.setClassLoader(f.getClass().getClassLoader());
  17. f.mArguments = args;
  18. }
  19. return f;
  20. } catch (ClassNotFoundException e) {
  21. throw new InstantiationException("Unable to instantiate fragment " + fname
  22. + ": make sure class name exists, is public, and has an"
  23. + " empty constructor that is public", e);
  24. } catch (java.lang.InstantiationException e) {
  25. throw new InstantiationException("Unable to instantiate fragment " + fname
  26. + ": make sure class name exists, is public, and has an"
  27. + " empty constructor that is public", e);
  28. } catch (IllegalAccessException e) {
  29. throw new InstantiationException("Unable to instantiate fragment " + fname
  30. + ": make sure class name exists, is public, and has an"
  31. + " empty constructor that is public", e);
  32. }
  33. }


上述代碼片的關鍵,其實就是通過java的反射機制進行實例化Fragment。實例化是調用的是Fragment f = (Fragment)clazz.newInstance();無參構造函數。
另外,如果需要傳參數的話,注意到實例化方法 public static Fragment instantiate(Context context, String fname, Bundle args)第三個構造函數,恢復時在代碼中用無參構造方法實例化fragment,然后判斷Bundle args是否為空,將參數加載到f.mArguments = args;因此在fragment的onCreate()方法中可以使用getArguments()將參數還原。

解決方案: 為了盡量的少的改動,提供新的靜態構造方法傳遞參數。

   
   
   
           
  1. [java] view plain copy
  2. public static ExpertsListFragment getInstance(int pageNo, String subClassId) {
  3. ExpertsListFragment mFragment = new ExpertsListFragment();
  4. Bundle args = new Bundle();
  5. args.putInt("pageNo", pageNo);
  6. args.putString("subClassId", subClassId);
  7. mFragment.setArguments(args);
  8. return mFragment;
  9. }

然后在在fragment的onCreate()方法中可以使用getArguments()將參數還原:
   
   
   
           
  1. [java] view plain copy
  2. public static ExpertsListFragment getInstance(int pageNo, String subClassId) {
  3. ExpertsListFragment mFragment = new ExpertsListFragment();
  4. Bundle args = new Bundle();
  5. args.putInt("pageNo", pageNo);
  6. args.putString("subClassId", subClassId);
  7. mFragment.setArguments(args);
  8. return mFragment;
  9. }


總結:當系統因為內存緊張殺死非前台進程(並非真正的殺死),然后用戶將被系統殺掉的非前台app帶回前台,如果這個時候有UI是呈現在Fragment中,那么會因為restore造成fragment需要通過反射實例對象,從而將之前save的狀態還原,而這個反射實例對象就是fragment需要Public的empty constructor的關鍵所在。這樣的BUG同時也出現在TrendsChartActivity和NewsListFragment中,使用同樣的方法修復。

BUG2:
java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState 
 
復現:在異常頁面在MainActivity中ft.commit()之前調用onstop()方法,讓MainActivity調用onSaveInstanceState和onRestoreInstanceState恢復

問題描述:根據FragmentTransaction的源碼中調用的流程是 ft.commit() -> return commitInternal(false) ->   commitInternal(boolean allowStateLoss) -> mManager.enqueueAction(this, allowStateLoss) -> checkStateLoss() -> 拋出異常。
Android源碼中拋出的異常代碼如下:
 
   
   
   
           
  1. [java] view plain copy
  2. private void checkStateLoss() {
  3. if (mStateSaved) {
  4. throw new IllegalStateException(
  5. "Can not perform this action after onSaveInstanceState");
  6. }
  7. if (mNoTransactionsBecause != null) {
  8. throw new IllegalStateException(
  9. "Can not perform this action inside of " + mNoTransactionsBecause);
  10. }
  11. }


解決方法一:將commit()改成commitAllowingStateLoss();源碼中調用流程:ft.commitAllowingStateLoss() -> return commitInternal(true) ->   commitInternal(boolean allowStateLoss) -> mManager.enqueueAction(this, allowStateLoss)  allowStateLoss為true不執行checkStateLoss()沒有異常拋出

但這樣的方法:commit()函數和commitAllowingStateLoss()函數的唯一區別就是當發生狀態丟失的時候,后者不會拋出一個異常。通常不應該使用這個函數,因為它意味可能發生狀態丟失。
解決方法二 :更好的解決方案是讓 commit()函數確保在 Activity的 狀態保存之前調用,這樣會有一個好的用戶體驗。可用一個狀態標志位 isSaved 來判斷,在onSaveInstanceState(),onStop()等方法中將 isSaved 設置為true即可。這樣在ft.commit()之前先判斷 isSaved ,若為false執行ft.commit(),為假執行。

BUG 3:java.lang.IndexOutOfBoundsException:

復現:下拉刷新加載上時,點擊了LIstView中在UI線程中clean了的Items,然后調用getItem(position)就會拋異常IndexOutOfBoundsException。

問題描述:由刷新機制引起的。下拉刷新加載上時,點擊了沒有在UI線程clean完的Items,然后調用getItem(position)就會拋異常IndexOutOfBoundsException。

Android源碼中拋出的異常代碼如下:

   
   
   
           
  1. [java] view plain copy
  2. public Object getItem(int position) {
  3. // Header (negative positions will throw an IndexOutOfBoundsException)
  4. int numHeaders = getHeadersCount();
  5. if (position < numHeaders) {
  6. return mHeaderViewInfos.get(position).data;
  7. }
  8. // Adapter
  9. final int adjPosition = position - numHeaders;
  10. int adapterCount = 0;
  11. if (mAdapter != null) {
  12. adapterCount = mAdapter.getCount();
  13. if (adjPosition < adapterCount) {
  14. return mAdapter.getItem(adjPosition);
  15. }
  16. }
  17. // Footer (off-limits positions will throw an IndexOutOfBoundsException)
  18. return mFooterViewInfos.get(adjPosition - adapterCount).data;
  19. }


解決方法:原來是刷新是數據被清除,網絡請求完成后再刷新載加載數據。如果網速不好的話,會用一段空白期。現在的機制是,在網絡請求完成后,刷新數據時,不清除數據先,當網絡數據返回 時判斷Items.size() > 0 來確定是否Items.clear()。在NewFragmentList,ExpertsListFragment,ExpertColumnActivity中都有這樣的問題。

  Android實際開發中的bug總結與解決方法(二)


 解決bug中的總結:Fragment Transactions 和Activity狀態丟失
 
  Fragment transactions用於在一個Activity上添加、移除或者替換fragment。大多數時候,fragment transaction會在activity的onCreate()方法中執行,也可能在與用戶交互中響應。
  然而,BUG是當恢復一個activity時,fragment transaction被執行了,應用就可能發生下面的下崩潰:
 
[java] view plain copy
  1. java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState  
  2.     at android.support.v4.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:1327)  
  3.     at android.support.v4.app.FragmentManagerImpl.enqueueAction(FragmentManager:1338)  
  4.     at android.support.v4.app.BackStackRecord.commitInternal(BackStackRecord.java:595)  
  5.     at android.support.v4.app.BackStackRecord.commit(BackStackRecord:574)  
  6.     at android.support.v4.app.DialogFragment.show(DialogFragment:127)  


 

原因:不管何時,如果一個FragmentActivity放在后台,對應FragmentMangerImpl中mStateSaved的flag就會設置為true。這個flag是用來檢查是否有state loss。

當試圖執行一個transaction時,如果這個flag為true,那么就首先會拋出IllegalStateException異常。

那為什么會拋出這個異常呢?這個問題源於這樣的事實,Bundle對象代表一個Activity在調用onSaveInstanceState()方法的一個瞬間快照。

這個transaction將不會被記住,因為它沒有在第一時間記錄為這個Activity的狀態的一部分。Android不惜一切代價避免狀態的丟失。

這意味着,當你在onSaveInstanceState()方法調用后會調用FragmentTransaction的commit方法。因此,在有些時候,都將簡單的拋出一個IllegalStateException異常。


Honeycomb之前的版本                 更新版本
Activities會在onPause()調用前被結束? NO NO
Activities會在onStop()調用前被結束? YES NO
onSaveInstanceState(Bundle)會在哪些方法調用前被執行? onPause() onStop()

作為Activity生命周期已做的細微改變的結果,Fragment的Support Library有時候需要根據平台的版本來改變它的行為。

                      Honeycomb之前的版本               更新版本
commit()在onPause()前被調用 OK OK
commit()在onPause()和onStop()執行中間被調用 STATE LOSS OK
commit()在onStop()之后被調用 EXCEPTION EXCEPTION

建議一

不要在讓transactions在其他的Activity生命周期函數提交,如onActivityResult()onStart()onResume(),事情將會變得微妙。

例如,你不應該在FragmentActivity的onResume()方法中提交transactions。因為有些時候這個函數可以在Activity的狀態恢復前被調用。

如果你的應用要求在除onCreate()函數之外的其他Activity生命周期函數中提交transaction,你可以在FragmentActivity的onResumeFragments()函數或者Activity的onPostResume()函數中提交。

這兩個函數確保在Activity恢復到原始狀態之后才會被調用,從而避免了狀態丟失的可能性。

nResume和onResumeFragments的區別是什么呢?下面是官方文檔 對FragmentActivity.onResume的解釋:

將onResume() 分發給fragment。注意,為了更好的和舊版本兼容,這個方法調用的時候,依附於這個activity的fragment並沒有到resumed狀態。

意味着在某些情況下,前面的狀態可能被保存了,此時不允許fragment transaction再修改狀態。

從根本上說,你不能確保activity中的fragment在調用Activity的OnResume函數后是否是onresumed狀態,

因此你應該避免在執行fragment transactions直到調用了onResumeFragments函數。

建議二

避免在異步回調函數中提交transactions。包括常用的方法,比如AsyncTask的onPostExecute方法和LoaderManager.LoaderCallbacks的onLoadFinished方法。

在這些方法中執行transactions的問題是,當他們被調用的時候,他們完全沒有Activity生命周期的當前狀態。例如,考慮下面的事件序列:

  1. 一個Activity執行一個AsyncTask。
  2. 用戶按下“Home”鍵,導致Activity的onSaveInstanceState()onStop()方法被調用。
  3. AsyncTask完成並且onPostExecute方法被調用,而它沒有意識到Activity已經結束了。
  4. 在onPostExecute函數中提交的FragmentTransaction,導致拋出一個異常。

一般來說,避免這種類型異常的最好辦法就是不要在異步回調函數中提交transactions。

如果你的應用需要在這些回調函數中執行transaction,而沒有簡單的方法可以確保這個回調函數不好在onSaveInstanceState()之后調用。

那么,可能需要使用commitAllowingStateLoss方法,並且處理可能發生的狀態丟失。

建議三

作為最后的辦法,使用commitAllowingStateLoss()函數。commit()函數和commitAllowingStateLoss()函數的唯一區別就是當發生狀態丟失的時候,后者不會拋出一個異常。

當然,更好的解決方案是commit函數確保在Activity的狀態保存之前調用,這樣會有一個好的用戶體驗。除非狀態丟失的可能無可避免,否則就不應該使用commitAllowingStateLoss()函數。

 
Android實際開發中的bug總結與解決方法(三)

解決bug中的總結:Bitmap 內存優化相關

  XXXXX項目中相關的bug有2個:
1) 在生成圓角圖片的RoundImageView的onDraw()方法中 :bug: bitmap size exceeds VM budget .
2) 在SSQSplashActivity的onCreate()方法中加載歡迎界面的圖片時 bug: OutOfMemoryError.
 
Bitmap 內存優化:
 
1)   要及時回收Bitmap的內存
Bitmap類的構造方法都是私有的,所以不能直接new出一個Bitmap對象,只能通過BitmapFactory類的各種靜態方法來實例化一個Bitmap。
查看BitmapFactory的源代碼可以看到,生成Bitmap對象最終都是通過JNI調用方式實現的。所以,加載 Bitmap到內存里以后,是包含兩部分內存區域的。簡單的說,一部分是Java部分的,一部分是C部分的。
這個Bitmap對象是由Java部分分配的,不用的時候系統就會自動回收了,但是那個對應的C可用的內存區域,虛擬機是不能直接回收的,這個只能調用底層的功能釋放。所以需要調用 recycle()方法來釋放C部分的內存。
從Bitmap類的源代碼也可以看到,recycle()方法里也的確是調用了JNI方法了的。

對於第一個bug:
RoundImageView控件中采用的生成圓角圖片的方法是setXfermode(Mode.SRC_IN)+canvas來實現的,其中在onDraw()方法中共在創建了3個bitmap對象,其中一個是bitmap = b.copy(Bitmap.Config.ARGB_8888, true)的拷貝其實現實是調用的JNI的方法在C底層實現,但是在最后僅僅是將bitmap賦值為null了,如bmp = null;這樣的話,可能存在Android系統對java層的bitmap做了回收,而沒有用 recycle()方法調用JNI的來徹底回收C部分的內存。

解決辦法 生成圓角圖片有一個更好的實現方法是:BitmapShader(Bitmap bitmap,Shader.TileMode tileX,Shader.TileMode tileY)。調用這個方法來產生一個畫有一個位圖的渲染器(Shader)。該方法實現簡單高效,節約內存開銷。查看球神的源碼中,就是用的這種方法。於是我們將球神中的CircleImageView控件,替換了之前的RoundImageView控件。

2)捕獲異常
因為Bitmap是內存消耗大戶,為了避免應用在分配Bitmap內存的時候出現OutOfMemory異常以后Crash掉,需要特別注意實例化Bitmap部分的代碼。
通常,在實例化Bitmap的代碼中,一定要對OutOfMemory異常進行捕獲。

3) 對ImageView等圖片的資源的操作

盡量不要使用setImageBitmap或setImageResource或BitmapFactory.decodeResource來設置一張大圖,
因為這些函數在完成decode后,最終都是通過java層的createBitmap來完成的,需要消耗更多內存。

然而,可以改用先通過BitmapFactory.decodeStream方法,創建出一個bitmap,再將其設為ImageView的 source,
decodeStream最大的不同之處在於其直接調用JNI>>nativeDecodeAsset()來完成decode,無需再使用java層的createBitmap,從而節省了java層的空間。
但是,decodeStream有這么一個缺點:
decodeStream直接拿的圖片來讀取字節碼了, 不會根據機器的各種分辨率來自動適應,使用了decodeStream之后,需要在hdpi和mdpi,ldpi中配置相應的圖片資源, 
否則在不同分辨率機器上都是同樣大小(像素點數量),顯示出來的大小就不對了。
 
Bitmap,占用內存的算法如下:
圖片的width*height*Config。如果Config設置為ARGB_8888,那么上面的Config就是4。一張480*320的圖片占用的內存就是480*320*4 byte。

對於第二個bug:
在歡迎界面加載的圖片是720*1280,位深度是24,大小189kb. 在Android中圖片占用的內存就是720*1280*4byte. 所以還是比較大的。建議在保證高清線度的前提下,盡量減小的圖片資源。
 
4) 壓縮圖片

使用BitmapFactory.Options設置inSampleSize就可以縮小圖片。屬性值inSampleSize表示縮略圖大小為原始圖片大小的幾分之一。如果知道圖片的像素過大,就可以對其進行縮小。

那么如何才知道圖片過大呢?

方法是:使用BitmapFactory.Options設置inJustDecodeBounds為true后,再使用decodeFile()等方法,並不會真正的分配空間,即解碼出來的Bitmap為null,但是可計算出原始圖片的寬度和高度,即options.outWidth和 options.outHeight。通過這兩個值,就可以知道圖片是否過大了。在實際項目中,先獲取圖片真實的寬度和高度,然后判斷是否需要跑縮小。如果不需要縮小,設置inSampleSize的值為 1。如果需要縮小,則動態計算並設置inSampleSize的值,對圖片進行縮小。需要注意的是,在下次使用BitmapFactory的 decodeFile()等方法實例化Bitmap對象前,別忘記將opts.inJustDecodeBound設置回false。否則獲取的 bitmap對象還是null。


BUG 、使用actionProvider時出現的問題

bug復現:

解決方案:

1
2
//import android.support.v4.view.ActionProvider;
import  android.view.ActionProvider;

換一種import的方式即可。tmd,這就是一個坑。  

 

BUG : 背景牆設置失效

采用XUTILS的圖片緩存技術做了個小米電視的app,加了一個配置圖片倉庫和圖片數量的對話框。如果配置完,程序重啟什么都ok,但是一旦關機就恢復初始狀態,原因是自己

在寫程序的時候大意了。

復制代碼
 1    String tmpBucketName = LocalDataDeal.readBucketNameFromLocalData();
 2   String tmpBucketNum = LocalDataDeal.readBucketNumFromLocalData();
 3    String tmpBucketWaterMark = LocalDataDeal.readBucketWaterMarkFromLocalData();
 4    
5 if(tmpBucketName != null && tmpBucketName != "" && tmpBucketNum != "" && tmpBucketNum != null && tmpBucketWaterMark != null && tmpBuckeWaterMark != "" ) 6 { 7 if(Integer.parseInt(tmpBucketNum) > 1) 8 { 9 QiNiuBucketName = tmpBucketName; 10 QiNiuBucketNumber = Integer.parseInt(tmpBucketNum); 11 QiNiuBucketWaterMark = tmpBucketWaterMark; 12 } 13 QiNiuBucketName = LocalDataDeal.readBucketNameFromLocalData(); 14 }
復制代碼

問題出在了對第五行對waterMark的處理,因為允許設置是否顯示水印,而水印不存在的時候就是tmpBuckerWaterMark為null的時候,所以對於沒有設置水印的倉庫配置,是永遠不會顯示的。

還有一點,就是在對字符串比較的時候,除了和null對比可以直接用==符號,其余比較都得用equal方法進行對比。







免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM