Android開發中有很多bug,我們是完全可以在線下避免的,不要等到線上報的BUG的再去修復。下面是我在實際開發中遇到過的bug和解決方法。復現:當app啓動後,進入異常頁面,然後使其進入後臺進程(按home鍵),接着改變系統設置如字體大小等方法,目的上讓app被系統殺死後恢復重現,這時候再點擊app進入應用,拋出異常。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
問題描述:包含有fragment的Activity在異常被銷燬(如系統內存不足等)後,再進入恢復activity時,重新實例化fragment時拋出異常出錯。異常的原因就是因爲使用的fragment沒有public的empty constructor。查看源代碼知:fragment在還原狀態中調用FragmentState#instantitae()->Fragment#instantitae()拋出異常。具體Android源碼中拋出的異常代碼如下:/** * Create a new instance of a Fragment with the given class name. This is * the same as calling its empty constructor. */ public static Fragment instantiate(Context context, String fname, Bundle args) { try { Class<?> clazz = sClassMap.get(fname); if (clazz == null) { // Class not found in the cache, see if it's real, and try to add it clazz = context.getClassLoader().loadClass(fname); sClassMap.put(fname, clazz); } Fragment f = (Fragment)clazz.newInstance(); if (args != null) { args.setClassLoader(f.getClass().getClassLoader()); f.mArguments = args; } return f; } catch (ClassNotFoundException e) { throw new InstantiationException("Unable to instantiate fragment " + fname + ": make sure class name exists, is public, and has an" + " empty constructor that is public", e); } catch (java.lang.InstantiationException e) { throw new InstantiationException("Unable to instantiate fragment " + fname + ": make sure class name exists, is public, and has an" + " empty constructor that is public", e); } catch (IllegalAccessException e) { throw new InstantiationException("Unable to instantiate fragment " + fname + ": make sure class name exists, is public, and has an" + " empty constructor that is public", e); } }
上述代碼片的關鍵,其實就是通過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()將參數還原。解決方案: 爲了儘量的少的改動,提供新的靜態構造方法傳遞參數。public static ExpertsListFragment getInstance(int pageNo, String subClassId) { ExpertsListFragment mFragment = new ExpertsListFragment(); Bundle args = new Bundle(); args.putInt("pageNo", pageNo); args.putString("subClassId", subClassId); mFragment.setArguments(args); return mFragment; }
然後在在fragment的onCreate()方法中可以使用getArguments()將參數還原:public static ExpertsListFragment getInstance(int pageNo, String subClassId) { ExpertsListFragment mFragment = new ExpertsListFragment(); Bundle args = new Bundle(); args.putInt("pageNo", pageNo); args.putString("subClassId", subClassId); mFragment.setArguments(args); return mFragment; }
總結:當系統因爲內存緊張殺死非前臺進程(並非真正的殺死),然後用戶將被系統殺掉的非前臺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源碼中拋出的異常代碼如下:private void checkStateLoss() { if (mStateSaved) { throw new IllegalStateException( "Can not perform this action after onSaveInstanceState"); } if (mNoTransactionsBecause != null) { throw new IllegalStateException( "Can not perform this action inside of " + mNoTransactionsBecause); } }
解決方法一:將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源碼中拋出的異常代碼如下:public Object getItem(int position) { // Header (negative positions will throw an IndexOutOfBoundsException) int numHeaders = getHeadersCount(); if (position < numHeaders) { return mHeaderViewInfos.get(position).data; } // Adapter final int adjPosition = position - numHeaders; int adapterCount = 0; if (mAdapter != null) { adapterCount = mAdapter.getCount(); if (adjPosition < adapterCount) { return mAdapter.getItem(adjPosition); } } // Footer (off-limits positions will throw an IndexOutOfBoundsException) return mFooterViewInfos.get(adjPosition - adapterCount).data; }
解決方法:原來是刷新是數據被清除,網絡請求完成後再刷新載加載數據。如果網速不好的話,會用一段空白期。現在的機制是,在網絡請求完成後,刷新數據時,不清除數據先,當網絡數據返回 時判斷Items.size() > 0 來確定是否Items.clear()。在NewFragmentList,ExpertsListFragment,ExpertColumnActivity中都有這樣的問題。