前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >iOS 是如何获取夜间模式启动图的?

iOS 是如何获取夜间模式启动图的?

作者头像
酷酷的哀殿
发布2020-12-02 11:44:51
1.1K0
发布2020-12-02 11:44:51
举报
文章被收录于专栏:酷酷的哀殿酷酷的哀殿

百度APP技术团队曾经发布过一篇深夜暗坑 - iOS启动图异常修复方案

该文章分享了一些关于启动图的研究,但是遗留了一个很重要的问题,iOS 是如何获取夜间模式启动图的?

本文将通过分析系统内部文件解决一下这个问题。

方案一:通过启动文件名进行分析

我们首先对原文提供的信息进行初步的分析。

原文提供了以下2个信息:

  • 缓存启动图的文件名具有规则,但其规则我们不得而知
  • 4 张启动图的文件名
代码语言:javascript
复制
├── 1FFD332B-EBA0-40C9-8EEE-BEC9AEF7C41A@3x.ktx
├── 96920D11-6312-4D69-BBDB-AFBB52DBDDB3@3x.ktx
├── 98F7B5B1-5B3B-478B-93A8-ED3DE6492AD1@3x.ktx
└── D9D48845-8565-42CE-A834-479CC9CC8BAD@3x.ktx

通过 4 个文件名,我们可以发现4张图片的命名都符合以下规则:

代码语言:javascript
复制
xxxxxxxx-xxxx-4xxx-xxxx-xxxxxxxxxxxx

再结合苹果官方文档 NSUUID[1] 的内容:

代码语言:javascript
复制
`NSUUID`conform to RFC 4122 version 4 and are created with random bytes.

我们可以得到以下结论:

  • 4 个文件名的都是通过 NSUUID 动态生成
  • 文件名只包含版本 4,不再包含其它有效的信息

方案二:通过系统文件进行分析

方案一失败后,我们猜测 iOS 是通过其它方式保存夜间模式启动图的路径。

经过一系列的测试,作者发现了 applicationState.db 文件。

applicationState.db

系统会通过 applicationState.db 保存程序状态等各类信息,当然,也会包括夜间模式启动图的路径。

本文分析的文件位于 ~/Library/Developer/CoreSimulator/Devices/1F9B22C5-E446-4881-AFE4-3373E3513C59/data/Library/FrontBoard/applicationState.db

其中,1F9B22C5-E446-4881-AFE4-3373E3513C59代表 iOS 模拟器的设备ID。

模拟器的完整ID列表可以通过命令 plutil -p ~/Library/Developer/CoreSimulator/Devices/device_set.plist 查看

测试环境

为了方便对系统文件进行分析,本文以 iOS 14 模拟器为目标进行分析。

版本信息如下所示:

代码语言:javascript
复制
(lldb) platform status
  Platform: ios-simulator
    Triple: x86_64h-apple-macosx
OS Version: 10.15.6 (19G2021)
    Kernel: Darwin Kernel Version 19.6.0: Thu Jun 18 20:49:00 PDT 2020; root:xnu-6153.141.1~1/RELEASE_X86_64
  Hostname: 127.0.0.1
WorkingDir: /
  SDK Path: "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk"
No devices are available.

另外,后续的代码会假定 Bundle identifiertest.SplashTest

解析 applicationState.db

先尝试通过 file命令获取 applicationState.db文件类型:

代码语言:javascript
复制
file ~/Library/Developer/CoreSimulator/Devices/1F9B22C5-E446-4881-AFE4-3373E3513C59/data/Library/FrontBoard/applicationState.db

输出:

代码语言:javascript
复制
SQLite 3.x database, last written using SQLite version 3032003

测试成功,通过 file 命令的输出可以看到文件类型是 SQLite 3.x

db 结构

下面,再通过 SQLite 相关的工具对该文件进行dump,我们可以得到以下信息:

代码语言:javascript
复制
sqlite> .schema
CREATE TABLE schema(version INT NOT NULL);
CREATE TABLE key_tab (id INTEGER PRIMARY KEY, key TEXT NOT NULL, UNIQUE(key));
CREATE TABLE application_identifier_tab (id INTEGER PRIMARY KEY, application_identifier TEXT NOT NULL, UNIQUE(application_identifier));
CREATE TABLE kvs (    id INTEGER PRIMARY KEY,    application_identifier INT REFERENCES application_identifier_tab(id),    key INT REFERENCES key_tab(id),    value BLOB,    UNIQUE(application_identifier, key));
CREATE INDEX kvs_keys ON kvs(key);
CREATE INDEX kvs_application_identifiers ON kvs(application_identifier);
CREATE VIEW kvs_debug AS     SELECT application_identifier_tab.application_identifier, key_tab.key, value FROM application_identifier_tab, key_tab, kvs WHERE         kvs.application_identifier=application_identifier_tab.id         AND kvs.key=key_tab.id
/* kvs_debug(application_identifier,"key",value) */;
  • application_identifier_tab 保存了设备安装的应用列表( id 是主键,application_identifier 是 APP 的Bundle identifier)
  • key_tab 负责记录常量字符串。 经过测试,夜间模式启动图的路径属于 XBApplicationSnapshotManifest
代码语言:javascript
复制
sqlite> .schema key_tab
CREATE TABLE key_tab (id INTEGER PRIMARY KEY, key TEXT NOT NULL, UNIQUE(key));
sqlite> .width 2 50
sqlite> select * from key_tab;
id  key
--  --------------------------------------------------
1   SBLaunchImageIngestionInfo
2   XBApplicationSnapshotManifest
3   _SBScenes
4   SBApplicationShortcutItems
5   compatibilityInfo
6   SBApplicationRecentlyUpdated
7   SBApplicationRecentlyUpdatedTimerStartDate
  • kvs 负责在前面两个表的基础上新增存储一份 value BLOB
  • kvs_debug 负责串联上面的3个表:
代码语言:javascript
复制
classDiagram


kvs_debug --|> application_identifier_tab :  application_identifier
kvs_debug --|> key_tab :  key
kvs_debug --|> kvs :  value


key_tab : int id
key_tab : text key

application_identifier_tab : int id
application_identifier_tab : text application_identifier

kvs : int id
kvs : int application_identifier
kvs : int key
kvs : BLOB value
 
kvs_debug :  application_identifier
kvs_debug :  key
kvs_debug :  value
代码语言:javascript
复制
kvs.application_identifier=application_identifier_tab.id
kvs.key=key_tab.id

XBApplicationSnapshotManifest

通过 test.SplashTest,可以获取到4个结果,其中第2负责保存快照相关信息

代码语言:javascript
复制
sqlite> .width 15 32 8
sqlite> SELECT * FROM kvs_debug WHERE application_identifier = 'test.SplashTest';
application_ide  key                               value
---------------  --------------------------------  --------
test.SplashTest  _SBScenes                         bplist00
test.SplashTest  XBApplicationSnapshotManifest     bplist00
test.SplashTest  SBApplicationRecentlyUpdated      0
test.SplashTest  compatibilityInfo                 bplist00

导出XBApplicationSnapshotManifest

经过一番研究,我们发现 XBApplicationSnapshotManifest 对应的value 就是 SplashBoardXBApplicationSnapshotManifestImpl 类的持久化结果。

所以,我们可以通过通过以下代码,对 value 的内容进行 dump。

代码语言:javascript
复制
+(void)load {    
    void *lib  =  dlopen("/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/SplashBoard.framework/SplashBoard", RTLD_NOW);
    printf("%p", lib);
    [self dump:@"/Users/test/Library/Developer/CoreSimulator/Devices/1F9B22C5-E446-4881-AFE4-3373E3513C59/data/Library/FrontBoard/XBApplicationSnapshotManifest.plist"];
}

+(void)dump:(NSString *)path {
    NSData *data0 = [NSData dataWithContentsOfFile:path];
    NSPropertyListFormat f = -1;
    NSError *error = nil;
    NSData *data1 = [NSPropertyListSerialization propertyListWithData:data0 options:NSPropertyListReadStreamError format:&f error:&error];
    if (f==kCFPropertyListXMLFormat_v1_0) {
        NSLog(@"kCFPropertyListXMLFormat_v1_0");
    }
    id obj = [NSKeyedUnarchiver unarchiveTopLevelObjectWithData:data1 error:&error];
    NSLog(@"%@",obj);
}

dump 结果:

代码语言:javascript
复制
<XBApplicationSnapshotManifestImpl: 0x600000050d80; clientCount: 0> {
    containerIdentity = 0x0;
    snapshots = {
        <XBApplicationSnapshotGroup: 0x600002e65ea0; identifier: test.SplashTest - {DEFAULT GROUP}> {
            <XBApplicationSnapshot: 0x7fcb53f04d90; identifier: CE275D00-5732-4AFD-88FD-00BAE541EC12; launchInterfaceIdentifier: __from_UILaunchStoryboardName__; contentType: GeneratedDefault; referenceSize: {375, 812}; interfaceOrientation: LandscapeLeft; userInterfaceStyle: Dark> {
                creationDate = September 27, 2020 at 6:08:50 PM GMT+8;
                keepsImageAccessUntilExpiration = NO;
                hasGenerationContext = NO;
                context = {
                    contentType = GeneratedDefault;
                    fullScreen = YES;
                    referenceSize = {375, 812};
                    interfaceOrientation = LandscapeLeft;
                    userInterfaceStyle = Dark;
                    additionalContext = {
                        statusBarSettings = <XBStatusBarSettings: 0x600002c580f0; hidden: YES; style: 0x0; backgroundActivityEnabled: NO>;
                    }
                }
                imageContext = {
                    scale = 3.0;
                    opaque = YES;
                    fileRelativeLocation = default;
                    fileFormat = png;
                }
            };
            <XBApplicationSnapshot: 0x7fcb57004830; identifier: B9DAB53E-29D9-47D2-873E-5772DE9220D1; launchInterfaceIdentifier: __from_UILaunchStoryboardName__; contentType: GeneratedDefault; referenceSize: {375, 812}; interfaceOrientation: Portrait; userInterfaceStyle: Light> {
                creationDate = September 27, 2020 at 6:08:50 PM GMT+8;
                lastUsedDate = September 27, 2020 at 6:08:50 PM GMT+8;
                keepsImageAccessUntilExpiration = NO;
                hasGenerationContext = NO;
                context = {
                    contentType = GeneratedDefault;
                    fullScreen = YES;
                    referenceSize = {375, 812};
                    interfaceOrientation = Portrait;
                    userInterfaceStyle = Light;
                    additionalContext = {
                        statusBarSettings = <XBStatusBarSettings: 0x600002c5c1a0; hidden: NO; style: 0x0; backgroundActivityEnabled: NO>;
                    }
                }
                imageContext = {
                    scale = 3.0;
                    opaque = YES;
                    fileRelativeLocation = default;
                    fileFormat = png;
                }
            };
            <XBApplicationSnapshot: 0x7fcb57004b60; identifier: 6B84614D-0867-4048-BE04-8E22E6742DDF; launchInterfaceIdentifier: __from_UILaunchStoryboardName__; contentType: GeneratedDefault; referenceSize: {375, 812}; interfaceOrientation: Portrait; userInterfaceStyle: Dark> {
                creationDate = September 27, 2020 at 6:08:50 PM GMT+8;
                keepsImageAccessUntilExpiration = NO;
                hasGenerationContext = NO;
                context = {
                    contentType = GeneratedDefault;
                    fullScreen = YES;
                    referenceSize = {375, 812};
                    Portrait;
                    userInterfaceStyle = Dark;
                    additionalContext = {
                        statusBarSettings = <XBStatusBarSettings: 0x600002c5c2d0; hidden: NO; style: 0x0; backgroundActivityEnabled: NO>;
                    }
                }
                imageContext = {
                    scale = 3.0;
                    opaque = YES;
                    fileRelativeLocation = default;
                    fileFormat = png;
                }
            };
            <XBApplicationSnapshot: 0x7fcb57005140; identifier: D3E8D00C-EE33-466B-98A6-7E60865D8001; launchInterfaceIdentifier: __from_UILaunchStoryboardName__; contentType: GeneratedDefault; referenceSize: {375, 812}; interfaceOrientation: LandscapeLeft; userInterfaceStyle: Light> {
                creationDate = September 27, 2020 at 6:08:50 PM GMT+8;
                keepsImageAccessUntilExpiration = NO;
                hasGenerationContext = NO;
                context = {
                    contentType = GeneratedDefault;
                    fullScreen = YES;
                    referenceSize = {375, 812};
                    interfaceOrientation = LandscapeLeft;
                    userInterfaceStyle = Light;
                    additionalContext = {
                        statusBarSettings = <XBStatusBarSettings: 0x600002c5c400; hidden: YES; style: 0x0; backgroundActivityEnabled: NO>;
                    }
                }
                imageContext = {
                    scale = 3.0;
                    opaque = YES;
                    fileRelativeLocation = default;
                    fileFormat = png;
                }
            };
        };
    }
}

SplashBoard 部分类图

通过类信息,整理如下所示(只包含关键属性):

代码语言:javascript
复制
classDiagram
XBApplicationSnapshotManifestImpl *-- XBApplicationSnapshotGroup : _snapshotGroupsByID.value
XBApplicationSnapshotManifestImpl *-- NSString : _snapshotGroupsByID.key


XBApplicationSnapshotManifestImpl : _snapshotGroupsByID
XBApplicationSnapshotGroup : NSString identifier
XBApplicationSnapshotGroup : NSMutableSet _snapshots
XBApplicationSnapshotGroup *-- XBApplicationSnapshot : _snapshots

XBApplicationSnapshot : NSString _identifier

XBApplicationSnapshot : NSString _groupID
XBApplicationSnapshot : NSString _relativePath
XBApplicationSnapshot : NSDate _creationDate
XBApplicationSnapshot : int _userInterfaceStyle
XBApplicationSnapshot : XBStatusBarSettings statusBarSettings
XBApplicationSnapshot *-- XBStatusBarSettings:statusBarSettings

结论

通常上面的内容,我们可以对 iOS 获取夜间模式启动的流程进行合理的猜测

启动时,会先通过 XBApplicationSnapshotGroupidentifier 获取test.SplashTest - {DEFAULT GROUP} 的启动图列表

再通过 XBApplicationSnapshotuserInterfaceStyle = Dark;interfaceOrientation = Portrait;等信息,判断启动时应该使用

代码语言:javascript
复制
<XBApplicationSnapshot: 0x7fcb57004b60; identifier: 6B84614D-0867-4048-BE04-8E22E6742DDF; launchInterfaceIdentifier: __from_UILaunchStoryboardName__; contentType: GeneratedDefault; referenceSize: {375, 812}; interfaceOrientation: Portrait; userInterfaceStyle: Dark>

最后再通过 XBApplicationSnapshot_relativePath 拼接启动图的真实路径

代码语言:javascript
复制
~/Library/Developer/CoreSimulator/Devices/1F9B22C5-E446-4881-AFE4-3373E3513C59/data/Containers/Data/Application/FA902232-17D2-495F-B23E-410349A9921C/Library/SplashBoard/Snapshots/test.SplashTest - {DEFAULT GROUP}/6B84614D-0867-4048-BE04-8E22E6742DDF@3x.ktx

总结

本文通过对 applicationState.db 进行一系列的分析,最终实现了获取夜间模式启动图的诉求。

参考资料

[1]

NSUUID: https://developer.apple.com/documentation/foundation/nsuuid?language=objc

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-11-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 酷酷的哀殿 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 方案一:通过启动文件名进行分析
  • 方案二:通过系统文件进行分析
    • applicationState.db
      • 测试环境
        • 解析 applicationState.db
          • db 结构
            • XBApplicationSnapshotManifest
              • 导出XBApplicationSnapshotManifest
                • SplashBoard 部分类图
                  • 结论
                  • 总结
                    • 参考资料
                    领券
                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档