Android爬坑之旅之不易发现的BUG

Posted 香辣牛肉面

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android爬坑之旅之不易发现的BUG相关的知识,希望对你有一定的参考价值。

android的app开发过程中,除了机型适配等问题,常常还会出一些特殊的bug,这些bug往往需要特殊的场景情况下才会发生,这里罗列了一些平时项目中遇到的问题及注意点。


App打包apk安装后重复启动根界面的问题

这个问题很特殊,一般情况下很难被发现,是Android系统一直以来的一个Bug。

当我们把app打包成apk安装程序,通过点击apk文件进行安装时,会启动安装界面,
并在安装成功后会跳转安装完成界面,
如图:

技术分享

我们点击图中的 打开按钮,此时会启动我们的app

这里为了让大家更容易理解一些,

我们假设app有两个界面
* 启动界面SplashActivity
* 主界面MainActivity
* app启动后打开SplashActivity,3秒后自动跳转MainActivity,界面不做强制finish

接下来,我们需要了解下Task任务栈和Back Stack返回栈,
如果有同学对这两个概念还不熟悉的,
可以看一下官方文档,讲得很详细:

Android任务和返回栈官方文档

这里我们引用官方文档的一句话:

The device Home screen is the starting place for most tasks. When the user touches an icon in the application launcher (or a shortcut on the Home screen), that application’s task comes to the foreground. If no task exists for the application (the application has not been used recently), then a new task is created and the “main” activity for that application opens as the root activity in the stack.

当我们点击home界面的应用启动图标时(安装完成界面点击打开同理)

如果没有对应Task任务栈存在,则会创建一个新的任务栈,
并且把应用启动的首页面作为根Activity放到任务栈中。

如果存在对应的Task任务栈,则会直接调用对应的Task任务栈到前台,并将栈顶的界面显示给用户,

那么当我们的app启动后打开SplashActivity并跳转主界面MainActivity后,我们app的任务栈应该如图所示:

技术分享

此时,当我们点击Home键退回到桌面,
app的Task任务栈进入后台,然后我们点击桌面上的启动图标,

正常情况下,app应该会把它对应的Task任务栈调到前台,并显示刚刚栈顶的MainActivity界面,

正常流程

技术分享

然而,实际情况是,app会把它的Task任务栈调用到前台,

并在任务栈上重新创建新的SplashActivity ,再跳转到MainActivity,

在不重新加载application的情况下,它又重新走了一遍启动的流程,这个时候,我们会发现任务栈中的Activity重复了,SplashActivity跟MainActivity都变成了两个

为了更清晰的让大家理解,这里画了两个图,
* 错误的bug流程
* 错误状态下的Task任务栈

bug流程

技术分享

新调用的SplashActivity会被置于该app的task栈顶

技术分享

多出了两个Activity

当然这个bug一般用户也很难注意到,它的产生必须满足下面的条件:
* 点击apk文件安装app
* 安装完成界面点击打开按钮
* 点击Home键,进入系统桌面,此时app退到后台
* 再点击桌面上启动图标

那么对于这种问题我们如何来处理呢?

**按照上文的举例,
在正常流程下启动app进入MainActivity界面时的任务栈**:

技术分享

bug情况下,会调起任务栈到前台并添加根Acitivy SplashActivity到栈顶,此时的任务栈:

技术分享

我们可以看到,在bug情况下启动app时,SplashActivity(app的根Activity)再次创建并叠加到Task任务栈上了

理应只会出现在栈底的SplashActivity出现在了其他位置,所以这里我们直接判断了app根Activity SplashActivity的位置

在app的SplashActivity(app的根Activity)的onCreate方法中通过 isTaskRoot() 方法来判断是否是任务栈中的根Activity,如果是就不做任何处理,如果不是则直接finish掉;

public class SplashActivity extends BaseActivity {
@Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        setTheme(R.style.AppTheme_NoActionBar);
        super.onCreate(savedInstanceState);

        if (!isTaskRoot()) {
            finish();
            return;
        }
    }

}

这样栈顶的SplashActivity在还未执行其他代码的情况下就finish()掉了,此时会显示栈顶的MainActivity。


Android包含Fragment界面的Activity界面,在app被系统释放后,重新回到前台时,重建Activity造成Fragment重叠

随着功能需求的多样化,Fragment的应用场景也是越来越广,其中我们的首页底栏可能是最常见的场景了。

那我们这里说的app在被系统释放后,重回前台Activity时,重建造成Fragment重叠又是怎么回事呢?

我们知道,要使用Fragment的Activity必须继承v7的AppCompatActivity,
而AppCompatActivity继承自FragmentActivity

当我们的app退到后台处于容易被系统回收的状态时,会触发我们的onSaveInstanceState方法,

而使用Fragment的Activity会调用到父类FragmentActivity的onSaveInstanceState方法,

这里我截取FragmentActivity中onSaveInstanceState的关键代码:

/**
     * Save all appropriate fragment state.
     */
    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        Parcelable p = mFragments.saveAllState();//获取FragmentManager保存的所有Fragments
        if (p != null) {
            outState.putParcelable(FRAGMENTS_TAG, p);//Fragment不为空,执行保存操作
        }
       ...
        }
    }

我们看到,这里的代码把Fragment的状态保存了下来,
而在FragmentActivity的onCreate方法中,又将这些Fragment重建了:

 /**
     * Perform initialization of all fragments and loaders.
     */
    @SuppressWarnings("deprecation")
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        mFragments.attachHost(null /*parent*/);

        super.onCreate(savedInstanceState);

        ...
        if (savedInstanceState != null) {
            Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);
            mFragments.restoreAllState(p, nc != null ? nc.fragments : null);

        ...   
        }

   ...
    }

也就是说,界面因为被系统释放后重建,重新触发了Activity的onCreate方法,

如果开发人员没有判断onCreate的saveInstance变量调整创建逻辑,直接执行了Fragment的创建代码,那新建的Fragment就会跟系统恢复的重叠。

这个问题一方面因为内存不足的极端情况下才会触发(红米等低端设备属于常态,经常会释放app),

另一方面由于部分开发的Fragment界面不是透明的,因此即使叠加了也不一定能发现这个问题。

那对于这样的问题,我们如何处理呢,这里给出了三种处理方案:

1.在Activity的onCreate中判断savedInstanceState变量是否为null,
如果savedInstanceState为null说明是界面是新建,则执行完整的fragment tab初始化工作;
如果savedInstanceState不为null,说明Activity是被释放重建,那就不执行Fragment的创建,执行相关逻辑代码,

代码如下:

@Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        if (savedInstanceState != null) {
        //界面正常情况下create时的逻辑
            initTab();
        }
        else {
        //界面在内存不足情况下被强制回收后重新create的逻辑
        }
}

2.这个方法我称之为懒人做法

使用了Fragment的Activity在调用onCreate方法时会首先调用super.onCreate()

而super.onCreate最终又会执行FragmentActivity的onCreate方法,

从上文截取的代码中,我们看到,FragmentActivity的onCreate方法会判断saveInstanceState里的Fragment是否为空,不为空就恢复保存的Fragment

if (savedInstanceState != null) {
            Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);
            mFragments.restoreAllState(p, nc != null ? nc.fragments : null);

        ...   
        }

也就是说,我们在执行到这段代码前把FRAGMENTS_TAG对应的值清空,那样就不会触发系统重建的恢复了

那么我们只需要在使用Fragment的Activity的onCreate方法添加以下代码就可以了:

@Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        if (savedInstanceState != null) {
            savedInstanceState.putParcelable("android:support:fragments", null);//清空保存Fragment的状态数据
        }
        super.onCreate(savedInstanceState);
}

这样,在执行到FragmentActivity的onCreate前,FRAGMENTS_TAG对应的数据就已经清空了。

3.同样是懒人方法,直接重写onSaveInstanceState方法,注释掉super.onSaveInstanceState,这样就不会保存Fragment的数据了,不过副作用也是非常明显,就是onSaveInstanceState就完全失去作用了,
所以并不太推荐大家这么去做,仅做参考:

  @Override
    public void onSaveInstanceState(Bundle outState, PersistableBundle outPersistentState) {
//        super.onSaveInstanceState(outState, outPersistentState);
    }

关于模拟app被释放的场景,这里介绍个小方法,就是在app运行之后,按home键退到后台,然后打开电脑命令行工具,运行:

  adb shell am kill 包名packagename

此时app就会被释放,接着通过任务管理器或者启动图标打开app,这个时候刚刚的界面就会重建走onRestoreInstanceState了。


app调用系统相机后,拍照返回崩溃

一般情况下,我们大部分情况是通过传递uri的方式来调用系统相机的:

Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
mTakePhotoUri = FileUtils.getOutputMediaFileUri(FileUtils.MEDIA_TYPE_IMAGE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, mTakePhotoUri);
                startActivityForResult(intent, CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE);

这种通过指定uri存储路径的方式调用系统相机的方式

在onActivityResult的时候,返回的intent会没有数据

因此我们一般都是在onActivityResult里获取之前保留的uri(例子中的mTakePhotoUri,这个变量是个全局变量)变量来获取具体图片文件。

正式因为这个问题,导致不管调用系统相机导致app退到后台被释放
还是三星之类的手机调用相机时的自动旋转
都会导致调用相机的界面被释放并重建,从而使得Activity界面的全局变量值丢失。

如果没有在onSaveInstanceState里保存这个全局变量,在onRestoreInstanceState取回mTakePhotoUri的值,那重建之后的界面变量就丢失了,因此onActivityResult中取到的mTakePhotoUri就为null了,从而导致获取图片路径变量的时候报null。

经过测试,经过这样的处理后,大部分相机的崩溃问题都得以解决。

其实不仅是相机,很多功能在实际开发过程中都可能遇到因界面被释放导致变量数据丢失的情况,所以我们需要在onSaveInstanceState方法中根据实际情况来保存需要的变量,在onRestoreInstanceState方法中取回变量。

当然如果觉得太麻烦,这里给大家推荐一个懒人库,可以自动保存我们的变量,非常方便

https://github.com/frankiesardo/icepick


在Android 4.1等设备上使用EventBus报caused by: java.lang.ClassNotFoundException: Didn’t find class “android.os.PersistableBundle” on path: DexPathList

这个问题我只在Android 4.1的设备上发生过,在其他设备上均未报错

而造成这个错误的原因是我在无意中重写了 onSaveInstanceState(Bundle outState, PersistableBundle outPersistentState) 这个方法
(正常情况下应该重写 onSaveInstanceState(Bundle outState)

如果你的手头没有4.1的设备,这个问题可能一直发现不了


引入图片框架fresco后,出现is 32-bit instead of 64-bit的错误

这个问题主要由于Android系统对于so文件的加载机制造成的

不同CPU架构的手机加载时会在libs下找自己对应的目录,从对应的目录下寻找需要的.so文件;如果没有对应的目录,就会去armeabi下去寻找,如果已经有对应的目录,但是如果没有找到对应的.so文件,也不会去armeabi下去寻找了。 

我的项目只引用armeabi和 x86架构的so文件,这里我们假设为lib.so文件

当我使用一台arm64-v8架构的手机时,因为找不到arm64-v8对应的目录,因此系统会降级到armeabi中去查找lib.so文件。

而fresco图片框架因为考虑到了so的兼容性,compile引入编译的时候自带了arm64-v8的so文件,因此产生了一个arm64-v8的目录。

当项目打包编译安装后,arm64-v8架构的手机因为查找到了arm64-v8的目录,因此所有的so文件都会到arm64-v8的目录下查找,不会再去查找armeabi目录,而在arm64-v8的目录下,我并没有配置对应的lib.so文件,所以找不到lib.so文件,随即抛出is 32-bit instead of 64-bit的错误。

那我们如何解决了,这里介绍三种方法:

  1. 为项目已经引用的so库添加对应arm64-v8架构的so库,对于没有源码的情况下很难去配置编译对应版本的so文件;

  2. 删除引用的库的arm64-v8目录的so文件;

  3. 在gradle的defaultConfig中设置

ndk {
    // 设置支持的 SO 库构架,注意这里要根据你的实际情况来设置
    abiFilters ‘armeabi‘ , ‘x86‘
}

这样就固定只会打包armeabi和x86目录的so文件了,这么做可以防止在使用不熟悉的库的时候不小心引入了其他目录的so文件造成app报错


Android app的实际开发过程中还有各种各样奇怪的问题,如果你也遇到了一些特殊或者奇葩的bug,欢迎进行补充

























以上是关于Android爬坑之旅之不易发现的BUG的主要内容,如果未能解决你的问题,请参考以下文章

Android M(6.0) 权限爬坑之旅

移动端浏览器爬坑之旅

Android爬坑之旅:软键盘挡住输入框问题的终极解决方案

Android爬坑之旅:软键盘挡住输入框问题的终极解决方案

安卓易学,爬坑不易——腾讯老司机的RecyclerView局部刷新爬坑之路

我的Android进阶之旅关于Android平台获取文件的mime类型:为啥不传小写后缀名就获取不到mimeType?为啥android 4.4系统获取不到webp格式的mimeType呢?(代码片段