前言

最近项目中碰到了一个问题:由于编辑器占用内存过多,导致APP进入后台之后,很快就会被杀掉。这个时候,如果用户正在使用编辑器,那么编辑器里面的内容就会丢失了。针对这个问题,我想到了几个解决方案。

  1. 通过后台播放无声音乐,请求定位等方式让APP在后台长时间留存。不过跟群友交流过之后,发现很大的可能不会过审,放弃。(后来又有群友说后台播放无声音乐是可以通过审核的,没有证实。)
  2. 添加草稿功能。实时本地(云端)保存编辑器中的内容,重新进入编辑器之后加载缓存。这样就又有几个问题。首先,是否需要本地缓存APP被杀掉时的页面,在APP重新启动时,根据缓存的路径,进入页面。这样的话就需要对页面的路径进行配置、判断,想想就很麻烦;第二,是项目中有多个编辑器,编辑器中内容可能非常多,使用轻量级的plist、NSUserDefaults、NSKeyedAchiever等可能不是很合适,使用数据库又觉得没必要。

在这个时候,偶然发现 Apple 针对本身的伪后台系统,提供了一套API,让我们来恢复在后台被杀掉的APP的状态。那就是UIStateRestoration

APP State Restoration简介

由于iOS系统的设计,APP在后台停留时间过长,就很有可能被杀掉。然而,为了更好的体验,我们是希望APP可以一直停留在后台的。为了弥补这一点,在iOS 6.0版本之后,为我们一共了一套API,让我们可以在APP在后台被杀掉之后,重新启动APP时,可以恢复到APP被杀掉之前的状态。

API介绍(部分。。。)

@protocol UIViewControllerRestoration  //必须实现。
//这个方法使用来创建或者找到相关的viewController。如果返回为nil,那么就不会恢复该viewController以及它的子视图。
+ (nullable UIViewController *) viewControllerWithRestorationIdentifierPath:(NSArray<NSString *> *)identifierComponents coder:(NSCoder *)coder;
@end

//这个协议是用来恢复UITableVie和UICollectionView的数据
@protocol UIDataSourceModelAssociation
- (nullable NSString *) modelIdentifierForElementAtIndexPath:(NSIndexPath *)idx inView:(UIView *)view;
- (nullable NSIndexPath *) indexPathForElementWithModelIdentifier:(NSString *)identifier inView:(UIView *)view;
@end

#pragma mark -- State Restoration object protocols and methods --

@protocol UIObjectRestoration;

@protocol UIStateRestoring <NSObject>
@optional

@property (nonatomic, readonly, nullable) id<UIStateRestoring> restorationParent;

@property (nonatomic, readonly, nullable) Class<UIObjectRestoration> objectRestorationClass;

//这两个方式是用来保存和恢复当前页面中需要保存的数据的。
- (void) encodeRestorableStateWithCoder:(NSCoder *)coder;
- (void) decodeRestorableStateWithCoder:(NSCoder *)coder;

//在所有的数据都恢复之后执行。
- (void) applicationFinishedRestoringState;
@end

@protocol UIObjectRestoration
+ (nullable id<UIStateRestoring>) objectWithRestorationIdentifierPath:(NSArray<NSString *> *)identifierComponents coder:(NSCoder *)coder;
@end

###API使用
在实际的使用中,分为两种情况。

  1. 以UINavigationController为根控制器。因为UINavigationController会自动记录子视图的层级,因此非常容易实现,这里不多说。Apple官方的demo也是基于UINavigationController的。
  2. UITableViewController为根控制器。UITableViewController不会记录子视图的层级,因此需要对自己来定义。

在这里,我们基于UITableViewControllerUINavigationController为构架的普通APP为例子实现APP状态的恢复。

一、首先需要实现APPDelegate的两个方法。
/**
该方法在APP进入后台,或者在前台被用户主动杀掉时执行。(如果被系统杀掉,或者用户在后台杀掉,则不会执行)。返回`YES`,通知APPDelegate,在APP进入后台时,需要保存
State。
*/
- (BOOL)application:(UIApplication *)application shouldSaveApplicationState:(NSCoder *)coder {
    return YES;
}

/**
如果APP是异常关闭(后台被杀等),那么在下一次启动时,会执行这个方法。返回`YES`,通知APPDelegate,保存的状态需要被恢复。执行顺序为:
[AppDelegate application:willFinishLaunchingWithOptions:]
[AppDelegate application:shouldRestoreApplicationState:]
[AppDelegate application:didFinishLaunchingWithOptions:]
*/
- (BOOL)application:(UIApplication *)application shouldRestoreApplicationState:(NSCoder *)coder {
    return YES;
}
二、需要在[AppDelegate application:didFinishLaunchingWithOptions:]方法中进行处理,并实现[AppDelegate application:willFinishLaunchingWithOptions:]方法。
- (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    
    self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
    
    return YES;
}


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

    if (!self.window.rootViewController) {
        TabBarController *tabBarC = [[TabBarController alloc] init];
        self.window.rootViewController = tabBarC;
    }
    [self.window makeKeyAndVisible];
    
    return YES;
}
三、根控制器UITableViewController遵守并实现协议。
#import "TabBarController.h"

@interface TabBarController ()<UIViewControllerRestoration>

@end

@implementation TabBarController

+ (UIViewController *)viewControllerWithRestorationIdentifierPath:(NSArray<NSString *> *)identifierComponents coder:(NSCoder *)coder {

    TabBarController *tabBarC = [[TabBarController alloc] init];
    [[UIApplication sharedApplication] delegate].window.rootViewController = tabBarC;
    
    return tabBarC;
}

@end
四、UINavigationController准守并实现协议。
#import "NavigationController.h"
#import "TabBarController.h"
#import "HomeController.h"
#import "DynamicController.h"
#import "MineController.h"

@interface NavigationController ()<UIViewControllerRestoration>

@end

@implementation NavigationController

- (instancetype)initWithRootViewController:(UIViewController *)rootViewController {
    if (self = [super initWithRootViewController:rootViewController]) {
        
        UIViewController *viewController = self.viewControllers.firstObject;
        if ([viewController isKindOfClass:[HomeController class]]) {
            self.restorationIdentifier = @"home";
        }
        if ([viewController isKindOfClass:[DynamicController class]]) {
            self.restorationIdentifier = @"dynamic";
        }
        if ([viewController isKindOfClass:[MineController class]]) {
            self.restorationIdentifier = @"mine";
        }
        
        self.restorationClass = [self class];
    }
    return self;
}


+ (UIViewController *)viewControllerWithRestorationIdentifierPath:(NSArray<NSString *> *)identifierComponents coder:(NSCoder *)coder {

    TabBarController *tabBarC = (TabBarController *)[[UIApplication sharedApplication] delegate].window.rootViewController;
    
    NSString *identifier = identifierComponents.lastObject;
    NavigationController *navigationC = nil;
    if ([identifier isEqualToString:@"home"]) {
        navigationC = (NavigationController *)tabBarC.viewControllers[0];
    }
    else if ([identifier isEqualToString:@"dynamic"]) {
        navigationC = (NavigationController *)tabBarC.viewControllers[1];
    }
    else if ([identifier isEqualToString:@"mine"]) {
        navigationC = (NavigationController *)tabBarC.viewControllers[2];
    }
    
    return navigationC;
}

@end
五、UINavigationController的根控制器遵守并实现协议
#import "HomeController.h"

@interface HomeController ()<UIViewControllerRestoration>

@end

@implementation HomeController

- (instancetype)init {
    if (self = [super init]) {
        self.restorationIdentifier = NSStringFromClass([self class]);
        self.restorationClass = [self class];
    }
    return self;
}


+ (UIViewController *)viewControllerWithRestorationIdentifierPath:(NSArray<NSString *> *)identifierComponents coder:(NSCoder *)coder {
//    UITabBarController *rootViewController = (TabBarController *)[UIApplication sharedApplication].delegate.window.rootViewController;

    TabBarController *tabBarC = (TabBarController *)[[UIApplication sharedApplication] delegate].window.rootViewController;
    NavigationController *navigationC = (NavigationController *)tabBarC.viewControllers[0];
    HomeController *homeC = [navigationC.viewControllers firstObject];
    
    return homeC;
}


- (void)encodeRestorableStateWithCoder:(NSCoder *)coder {
    [super encodeRestorableStateWithCoder:coder];
    
}


- (void)decodeRestorableStateWithCoder:(NSCoder *)coder {
    [super decodeRestorableStateWithCoder:coder];
    
}

@end
六、要恢复的视图遵守并实现协议
#import "HomeDetailController.h"

@interface HomeDetailController ()<UIViewControllerRestoration>

@end

@implementation HomeDetailController

- (instancetype)init {
    if (self = [super init]) {
        self.restorationIdentifier = NSStringFromClass([self class]);
        self.restorationClass = [self class];
    }
    return self;
}


+ (UIViewController *)viewControllerWithRestorationIdentifierPath:(NSArray<NSString *> *)identifierComponents coder:(NSCoder *)coder {
    return [[self alloc] init];
}


- (void)encodeRestorableStateWithCoder:(NSCoder *)coder {
    [super encodeRestorableStateWithCoder:coder];
}


- (void)decodeRestorableStateWithCoder:(NSCoder *)coder {
    [super decodeRestorableStateWithCoder:coder];
}


- (void)applicationFinishedRestoringState {
}

到这里,基本上就实现了APP状态的恢复功能。不过还有一些小问题。如果是自定义了UINavigationController,在[UINavigationController pushViewController:animated:]方法中『自定义导航条返回按钮』,设置『跳转时隐藏底部导航条(hidesBottomBarWhenPushed)』等,那么在恢复APP状态之后,这些都会失效。因为在恢复状态的过程中,并没有调用[UINavigationController pushViewController:animated:]方法。目前我没有太好的解决办法,只能在自定义的基类中代码实现再次实现『自定义导航条返回按钮』等功能。

赶时间的碰到看到这里就可以停止了。接下来主要是根据我的理解,解析一下恢复状态的过程。有兴趣的朋友可以看一下。

参考文章

如何实现UITabbarController 的State Restoration?

另类的"APP常驻"——UIStateRestoration

iOS的App实现状态恢复

保存和恢复App的状态