iOS开发笔记(九)— 数据库、Crash、内存问题分析

96
落影loyinglin 595a1b60 08f6 4beb 998f 2bf55e230555
2018.08.26 18:33* 字数 1432

前言

分享iOS开发中遇到的问题,和相关的一些思考,本次内容包括:UIKit的iOS11问题、数据库问题定位、线上Crash处理、内存问题分析

正文

1、iOS 11的UITabbar的高度异常

问题描述:iOS 11+iPhone,在横竖屏切换的场景下,UITabbarViewController的底部栏UITabbar会出现高度异常。

问题定位:经过调试发现,从竖屏到横屏的时候,系统会改变UITabbar的高度;而我们的底部栏高度是自定义的值,故而会导致系统修改后的高度与自定义值不相同的情况。

解决方案,KVO:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {

        if (self.tabBar.height != KSTabBarHeight) {
            self.tabBar.height = KSTabBarHeight;
            self.tabBar.bottom = SCREEN_HEIGHT;
        }

Stackoverflow的类似情况

2、CoreData数据库升级时间长

问题描述:App在升级的时候会对CoreData数据库进行一次迁移,而某些用户反馈升级时间长达数分钟。

问题定位:CoreData数据库迁移使用的是系统提供的自动迁移,经过本地测试,确实存在数据库较大的情况下,升级时间较长的问题。
那么如何确定数据库是哪些表是瓶颈?
用户的数据库比较大,不可能进行整个数据库上传操作;而CoreData并不支持获取某个表的大小。

可以采取一种方案:用户上报数据库每张表的行数,本地通过工具求出每张表的平均值,用以估算每张表的大小。
找到可以导出沙盒本地沙盒的App活跃使用者(比如说运营、产品),用sqlite3_analyzer对数据库进行分析,得到每张表大小,再除以行数,得到每张表每行的平均值。
(不能通过行数直接判断数据库大小,因为表的列数不确定;也不能通过列大小*行数得到表体积,因为某些字段为空)

修复方案:

  • 对瓶颈的表进行行数和体积双重控制;
  • 对某些行数较多但表体积小的表建索引;

引用:
sqlite数据库分析
sqlite3_analyzer安装
Appropriate Uses For SQLite
sqlite索引
Customizing the Migration Process

3、objc_msgSend的Crash分析

问题描述:objc_msgSend是常见的一种Crash,这次的堆栈如下

objc_msgSend

这类由UIKit引起的Crash通常是在回调业务层时,对应的target已经被释放,于是在objc_msgSend的时候就会发生Crash。

寄存器和模块加载地址

问题定位:在本例中,查看上图知道,lr寄存器的地址是在第一个模块的加载区间内,以此作为线索。
用以下指定,进行手工符号化:
atos -o XXX arm64 0x000000010134d36c -l 0x1000fc000(XXX是二进制名字)

最终定位到问题,具体的代码类似:

[self.delegete remove];
self.data = ...

在这种情况下,self.delegate在remove掉之后self之后,self已经被释放,下面的self.data再进行赋值操作,就会出现异常情况。

解决方案:把 [self.delegete remove]; 放到最后一行。

后记:
该问题只出现在iOS 8。在iOS 11的机型上,通过调试我们可以获取到self.data=...这一行在执行时,关于self的内存引用情况:


autoreleasepool和thread都会持有self,保证self在本次执行过程中不释放。故此猜测该问题苹果已经发现,并且在iOS 8后续的版本已经修复。

4、内存相关问题

实际场景涉及到业务,所以抽象成代码来进行分析。

场景1

下面这段代码是否能够正常运行?
如果可以,结果是什么?
如果不可以,是为什么?

- (void)viewDidLoad {
    [super viewDidLoad];
    SInt16* buffer = NULL;
    SInt16* p = &buffer[15];
    NSLog(@"tmp %d", p);
}
场景2

下面这段代码是否能够正常运行?
如果可以,结果是什么?
如果不可以,是为什么?

- (void)viewDidLoad {
    [super viewDidLoad];
    
    char *pBuf = malloc(5);
    memcpy(pBuf, "aaabbbcccddddeeefff", 10);
   
    puts(pBuf);
}

分析:
场景1:
此处有两个trick:
1、p是否能够正常赋值,以及赋何值;
2、指针类型是SInt16*, 计算地址要注意;

[] 是下标运算符,根据操作数和偏移量,获取指定地址的值;
在此题之中,buffer[15]等于*(buffer + 15)
&buffer[15] 等于&(*(buffer + 15))

& 是取址运算符,返回操作数的内存地址;
&buffer[15] = &(*(buffer + 15)) = buffer + 15
所以p = &buffer[15] = buffer+15 = 0+15*sizeof(SInt16) = 30

故而答案是能正常运行,结果是"tmp 30"。

场景2:
申请了一块较大的内存,在memcpy的时候,偶然情况下会出现越界的情况。但是因为堆内存空间到栈内存空间的距离不固定,不一定会出现crash的情况。
上面的题目本质是堆内存访问越界。
故而上述代码大多数情况下输出aaabbbcccd,少数情况下不可预知。

总结

2018年的忙碌情况超过我想象,长时间不更新iOS开发笔记让我都忘了还有这个专题所在。
我有个习惯,开发中遇到问题,超过十分钟还没解决的时候,就会记录下来,这样是开发笔记专题的雏形。
而在加入新公司的第二个年头,我慢慢已经在iOS上的收获越来越少。
从笔记的新增情况来看,就可以发现:每天大多数是重复性劳动!
尝试看过一些iOS相关的书籍,但总感觉收获不大。
今年我选择把更多的业余学习时间分配给Metal,详见Metal入门教程总结

iOS开发随笔
Gupao