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,欢迎进行补充

时间: 2024-11-05 18:53:29

Android爬坑之旅之不易发现的BUG的相关文章

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

前言 开发做得久了,总免不了会遇到各种坑.而在Android开发的路上,『软键盘挡住了输入框』这个坑,可谓是一个旷日持久的巨坑--来来来,我们慢慢看. 入门篇 Base 最基本的情况,如图所示:在页面底部有一个EditText,如果不做任何处理,那么在软键盘弹出的时候,就有可能会挡住EditText.对于这种情况的处理其实很简单,只需要在AndroidManifest文件中对activity设置:android:windowSoftInputMode的值adjustPan或者adjustResi

Android M(6.0) 权限爬坑之旅

坑一:用Android5.0编译的apk,在Android6.0上运行完全没有问题. 在Android6.0以上才需要在运行时请求权限,在旧Android版本上保留原有逻辑,安装时授予权限. 用旧版本sdk编译的apk,都使用旧版本权限方式,安装时授予权限.(也就是说:兼容旧版本) 用Android6.0(targetSdkVersion 23)编译apk才需要处理新的权限问题. 还有一个蛋疼的问题:程序运行当中,用户关闭了权限,会发生什么?(还未知) 坑二:蓝牙扫描需要位置权限 Bluetoo

【转】Android M(6.0) 权限爬坑之旅

原文网址:https://yanlu.me/android-m6-0-permission-chasm/ 有一篇全面介绍Android M 运行时权限文章写的非常全面:Android M 新的运行时权限开发者需要知道的一切,但是实施过程中还是遇到一些坑. 坑一:用Android5.0编译的apk,在Android6.0上运行完全没有问题. 在Android6.0以上才需要在运行时请求权限,在旧Android版本上保留原有逻辑,安装时授予权限. 用旧版本sdk编译的apk,都使用旧版本权限方式,安

移动端浏览器爬坑之旅

以前总觉得移动端页面也很容易写,最近才下笔,简直五步一坑,十步一雷.查阅了些资料,整理出了部分浏览器兼容性bug. 有问题就跟浏览器客服提出来,一般他们是会处理的.那么就以二师兄展开本次分享. QQ浏览器X5内核问题汇总 https://www.qianduan.net/qqliu-lan-qi-x5nei-he-wen-ti-hui-zong/ 微信浏览器 因为微信浏览器屏蔽了一部分链接图片,所以需要引导用户去打开新页面,可以用以下方式判断微信浏览器的ua function is_weixn(

SpringBoot + SpringCloud的爬坑之旅

1,application.yaml中配置没有生效问题解决 如果配置文件确认没有错误但是没有生效首先是要到编译目录去查看是否被编译过去了,如果没有,请先将项目clean在重启 但是idea启动项目时也会先build,又有可能配置文件没有被编译过去,真实坑爹! 另外,yaml文件中的那些坑: (1)冒号:后面必须有空格,下级属性缩进一格(只支持空格不支持制表符tab) (2)保证不能有重复的一级节点. (3)如果参数是以空格开始或结束的字符串,应使用单引号把他包进来.如果一个字符串参数包含特殊字符

重新开始爬坑之旅

以前接触4种Linux发行版本,ubuntu centos redhat freebsd,感觉每一个都是大坑,好不容易跳出来  能开开心心写会代码.昨天又被老大一脚揣进了debian的深渊.得益于我朝宇宙第一的局域网,必须有一批人因为网络死在入门Linux的道路上. debian用wget或者apt-get都可以安装软件,wget还好 直接下载,手动安装,出问题了大不了删了 重来.apt-get是自动的出问题就悲催了,apt-get remove namexxx 并不能清理残留,手动又太痛苦.不

dotNet程序员的Java爬坑之旅(一)

仔细想了下还是转java吧,因为后期不管是留在北京也好还是回老家也好,java的工作都会好找一点.现在的工作主要还是写.net,目标是下一次离职的时候可以找到一份全职的java工作,我一直都觉得实践才是学习的最好方法. 现在这些的文章内容,主要是来自月慕课网的教学视频和自己的一些总结整理,我计划从一个小白做起,一步一步的去了解熟悉java这门语言. 从java的基础语法开始到现在看的spring,已经花了一定的时间和精力,现在我感觉不管是java还是.net在很多地方都能找到相似之处,现在对我来

dotNet程序员的Java爬坑之旅(二)

囉里囉唆的寫了一大堆,最後還是全刪除了.哎~ 言歸正傳,最近因爲發生了很多事情,所以更新的有嗲晚了,最近也一直在學習,但是感覺效率什麽的不是很高,這是不對的,反思一下,從這篇博文開始,打起精神吧. Mybatis批量新增數據: 批量插入:儅我們有批量插入數據的需求時,比如插入100條數據,傳統的做法是利用for循環100次,但是在這種方式存在嚴重的效率問題,需要頻繁的獲取連接. 此時比較好的解決辦法就是使用Mybatis支持批量插入的配置和語法. 需要在數據庫連接字符串処設置allowMulti

Vue爬坑之旅(二):vue单页面二级套嵌路由

在一个单页面应用里使用二级套嵌路由 目录结构如下: 其中main.js为全局配置文件,App.vue为项目入口. main.js中路由配置如下 import Vue from 'vue'//引入vue import App from './App'//引入主模板 import Router from 'vue-router'// 引入router路由 // 引入项目的模块组件 import licai from './components/licai' import home from './c