Android實際開發中的bug總結與解決方法(一)
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源碼中拋出的異常代碼如下:
[java] view plain copy
/**
* 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] view plain copy
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;
}
[java] view plain copy
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;
}
[java] view plain copy
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);
}
}
[java] view plain copy
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;
}
Android實際開發中的bug總結與解決方法(二)
原因:不管何時,如果一個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生命周期的當前狀態。例如,考慮下面的事件序列:
- 一個Activity執行一個AsyncTask。
- 用戶按下“Home”鍵,導致Activity的
onSaveInstanceState()
和onStop()
方法被調用。 - AsyncTask完成並且onPostExecute方法被調用,而它沒有意識到Activity已經結束了。
- 在onPostExecute函數中提交的FragmentTransaction,導致拋出一個異常。
一般來說,避免這種類型異常的最好辦法就是不要在異步回調函數中提交transactions。
如果你的應用需要在這些回調函數中執行transaction,而沒有簡單的方法可以確保這個回調函數不好在onSaveInstanceState()
之后調用。
那么,可能需要使用commitAllowingStateLoss方法,並且處理可能發生的狀態丟失。
建議三
作為最后的辦法,使用
commitAllowingStateLoss()
函數。commit()
函數和commitAllowingStateLoss()
函數的唯一區別就是當發生狀態丟失的時候,后者不會拋出一個異常。當然,更好的解決方案是commit函數確保在Activity的狀態保存之前調用,這樣會有一個好的用戶體驗。除非狀態丟失的可能無可避免,否則就不應該使用
commitAllowingStateLoss()
函數。
因為這些函數在完成decode后,最終都是通過java層的createBitmap來完成的,需要消耗更多內存。
然而,可以改用先通過BitmapFactory.decodeStream方法,創建出一個bitmap,再將其設為ImageView的 source,
decodeStream最大的不同之處在於其直接調用JNI>>nativeDecodeAsset()來完成decode,無需再使用java層的createBitmap,從而節省了java層的空間。
否則在不同分辨率機器上都是同樣大小(像素點數量),顯示出來的大小就不對了。
使用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方法進行對比。