线上出了一次事故, 在退出登录时, 正常的用户在退出登录时会清除成功userDefault中的数据,一般只会crash一次,再次打开就会正常,但是有个用户比较特殊, 出现了连续闪退, 主要原因就是userDefault中的数据没有清除成功, 下次再启动app, 从userDefault中获取数据还认为是登录中的状态, 然后再次crash, 这样就陷入了死循环,导致只要打开app就会crash, 只有卸载重装才能解决问题.

先来说说为什么这个用户比较特殊, 这是一个公司内部的用户, app一直都是覆盖安装, 随着版本的迭代,迭代了3,4年, userDefault中数据越来越多, 查看此用户的沙盒文件, 系统默认的userDefault大小竟然有20M,文件太大导致退出登录时,清除某个key太慢, 此时发生了crash,userDefault中的数据抹除失败, 陷入死循环.

自己写了一个demo试了一下, 当数据量很大时,确实会出现这样的情况, 前2个key写入成功, 第三个key还是原来的旧值, 即使加上了-synchronize也不管用.

iOS 应用挂起太久重新加载 ios应用老是重新加载_ios

确认了问题, 除了正常解决crash之外, 还想到了2种方式来补救.

NSUserDefault 用法优化

userdefault中不适合存较大数据, 但是业务上也确实需要这么多key来标记用户的某些状态, 比如当天是否签到, 比如是否展示过引导图, ...

发现系统在NSUserDefault中提供其他的初始化方法, 这样就可以按照不同的业务线或者按照功能来区分, 不要一股脑的都往默认的standardUserDefaults写入数据.

- (instancetype)initWithSuiteName:(NSString *)suitename NS_DESIGNATED_INITIALIZER;

比如

// 设置值
    NSUserDefaults *userDefault = [[NSUserDefaults alloc] initWithSuiteName:@"abc"];
    [userDefault setObject:@"1" forKey:@"1"];

// 另一个方法中获取值
    NSUserDefaults *userDefault = [[NSUserDefaults alloc] initWithSuiteName:@"abc"];
    NSString *value = [userDefault objectForKey:@"1"];
    NSLog(@"SuiteName:abc -- %@",value);

这个方法会在和默认的standardUserDefaults平级目录下记录一个文件, 其他用法和standardUserDefaults一模一样. 这样就达到了减少standardUserDefaults大小的目的了.

iOS 应用挂起太久重新加载 ios应用老是重新加载_数据_02

 连续闪退的优化

这个的做法就是检测到发生连续闪退时, 手动抹掉沙盒路径下的所有文件, 把所有的文件抹除后, 这个app其实就和刚从商店下载的一样了.

首先我们需要一种比较可靠的方式,可以在 app 启动时判断上次是否发生了启动 crash。介绍一个可行的思路。

如何检测连续闪退

连续闪退包含两个元素,闪退和连续。只有这两个元素同时具备时,才会判定为YES。闪退的定义可以简单为连续2次启动的间隔小于5s

app 本次启动时间 -  app上次启动时间 <= 5s (或者其他 threshold)

连续的定义为,至少接连出现两次或者以上。一般 2 次就够了,很多时候用户连续经历两次闪退,就会放弃尝试。

我们可以通过记录若干个特殊的时间点 timestamp 来试图还原 App crash 场景下的生命周期。

  • App 启动 timestamp,定义为 launchTs
    App 每次启动时,记录当前时间,写入时间数组。
  • App 正常退出 timestamp,定义为 terminateTs
    App 在接收到 UIApplicationWillTerminateNotification 通知时,记录当前时间戳,写入时间数组。注意,还有很多种 App 退出行为的时间戳是无法被准确记录的。

之所以要记录 terminateTs,是为了排除一种特殊情况,即用户启动 App 之后立即手动 kill app。如果我们正确记录了上面的时间戳,那么我们可以得到一个与 App crash 行为相关的时间线。比如:

launchTs => launchTs => launchTs

或者

launchTs => launchTs => terminateTs => launchTs

第一种情况下, 如果launch3-launch2<5 && launch2-launch1<5  , 那就判定为触发了2次闪退.

第二种情况, 由于launch2和launch3之间有一个terminateTs, 判定为只触发了一次闪退, 不满足连续闪退的条件.

这个时间线只是记录的 App 启动和退出行为,还有很多特殊的时间点没有记录,比如 App 在 前台发生 out of memory(FOOM),App 在前台 main thread 卡住被系统 Watch Dog 杀掉,iOS 系统升级时 App 被强杀,App 从 AppStore 升级时被强杀等等,这些特殊的时间点都没有记录,不过这些并不影响我们的 App 连续闪退检测,所以可以忽略。

这里指的注意的是,因为启动时要从 disk 读取时间线记录,涉及磁盘读写,会对 App 的启动时间产生影响,一个优化点是有一个独立的USerDefault存贮这些信息,同时在每次写入时间点移除掉较老的 timestamp,比如只记录最近 5 个时间戳。

有了这个判定之后, 在didFinishLaunchingWithOptions中就可以处理连续闪退, 可以弹窗让用户选择,或者到一个新页面进行处理都可以.

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    if (连续发生闪退) {
        // 弹窗让用户选择是否要抹除app的所有数据,
        // 如果用户选择了YES, 清理沙盒下的所有数据, 并上报服务器
        exit(0);
    }
    // 正常的业务代码

    return YES;
}

最后, 清除沙盒数据只能是解决本地数据导致的问题, 如果是代码本身的问题, 那就是只有通过发版来解决了. 

bugly本身也提供了类似的方法,连续crash3次的会返回YES,  3次感觉有点多了, 可以使用2种方式相结合作为触发连续crash的条件. 如果我们的判断失效, bugly 的方法也可以作为连续闪退修复的触发场景

iOS 应用挂起太久重新加载 ios应用老是重新加载_数据_03