首页 > 代码库 > Android实际开发中的bug总结与解决方法(一)
Android实际开发中的bug总结与解决方法(一)
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总结与解决方法(二)
解决bug中的总结:Fragment Transactions 和Activity状态丢失 Fragment transactions用于在一个Activity上添加、移除或者替换fragment。大多数时候,fragment transaction会在activity的onCreate()方法中执行,也可能在与用户交互中响应。 然而,BUG是当恢复一个activity时,fragment transaction被执行了,应用就可能发生下面的下崩溃: [java] view plain copy - java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
- at android.support.v4.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:1327)
- at android.support.v4.app.FragmentManagerImpl.enqueueAction(FragmentManager:1338)
- at android.support.v4.app.BackStackRecord.commitInternal(BackStackRecord.java:595)
- at android.support.v4.app.BackStackRecord.commit(BackStackRecord:574)
- 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生命周期的当前状态。例如,考虑下面的事件序列:
- 一个Activity执行一个AsyncTask。
- 用户按下“Home”键,导致Activity的
onSaveInstanceState()
和onStop()
方法被调用。 - AsyncTask完成并且onPostExecute方法被调用,而它没有意识到Activity已经结束了。
- 在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复现:
解决方案:
12 //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方法进行对比。
来自为知笔记(Wiz)
- java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
- at android.support.v4.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:1327)
- at android.support.v4.app.FragmentManagerImpl.enqueueAction(FragmentManager:1338)
- at android.support.v4.app.BackStackRecord.commitInternal(BackStackRecord.java:595)
- at android.support.v4.app.BackStackRecord.commit(BackStackRecord:574)
- 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生命周期的当前状态。例如,考虑下面的事件序列:
- 一个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方法进行对比。
Android实际开发中的bug总结与解决方法(一)