iOS开发遇坑总结

由使用Associated Objects不当导致app闪退总结

前段时间项目新增一个需求需要接入一个第三方的sdk,我们需要使用这个第三方sdk提供的一个model类MMdata数据,具体我们的需要实现的是:在sdk进行网络请求回来的数据回调中[MMdata]数组通过OC中runtime获取MMdata.m的一些私有属性及其变量的值然后将这些数据上传到我们自己服务器审核这些数据,服务器返回[MMdata]数组中哪些可用哪些不可用,然后我们对些可用的数据添加时间戳及其它属性以来满足我们那业务的需求。刚做这个的需求想到了好几个方案:一是使用继承,写一个继承MMdata的子类并且添加自己需要的属性值;二是重写一个本地类将MateriaMeta的数据全部给重写过来,在添加自己所想添加的;三是使用对象关联技术使用分类添加需要扩展的属性。经过一段小组之间的讨论,最后决定使用第三种方案。方法一二相比,明显第二种方法解耦合了,我们新建model类不依赖于SDK第三方了,而且在赋值的时候继承对于它父类的私有属性不好处理你还是得写一次,所以方法二优于方法一。然后比较二三方法,方法三更具有优势,因为只需要在我们服务器审核成功之后对MMdata的分类MMdata+Extension 的属性赋值即可。

我的写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
MMdata+Extension.h
/*
@property (nonatomic, assign) NSTimeInterval timestamps;
*/


MMdata+Extension.m
- (NSTimeInterval)timestamps {
return [objc_getAssociatedObject(self, _cmd) doubleValue];
}

- (void)setTimestamps:(NSTimeInterval)timestamps {
objc_setAssociatedObject(self, @selector(timestamps), [NSNumber numberWithDouble:timestamps], OBJC_ASSOCIATION_ASSIGN);
}

最后奔溃就在第九行,在使用timestamps的时候报了野指针错误,然而这个问题在上线前测试这边都没有出现过问题,上线后大量用户反馈闪退,以及通过后台闪退数据的抓取到就是这里的问题

事后分析原因,可能是在setTimestamps:方法中我使用的是OBJC_ASSOCIATION_ASSIGN修饰导致的。
首先,我查看了runtime的官方文档对对象关联的修饰符介绍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* Associative References */

/**
* Policies related to associative references.
* These are options to objc_setAssociatedObject()
*/
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, /**< Specifies a weak reference to the associated object. */
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object.
* The association is not made atomically. */
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, /**< Specifies that the associated object is copied.
* The association is not made atomically. */
OBJC_ASSOCIATION_RETAIN = 01401, /**< Specifies a strong reference to the associated object.
* The association is made atomically. */
OBJC_ASSOCIATION_COPY = 01403 /**< Specifies that the associated object is copied.
* The association is made atomically. */
};

错误信息:

1
-[CFNumber retain]: message sent to deallocated instance 0x604001620360

通过Xcode的lldb调试工具watchpoint后面找出原因是就是使用OBJC_ASSOCIATION_ASSIGN修饰的原因:timestramps是一个弱引用,所以当出了这个括号timestramps所指向的对象(也就是[NSNumber numberWithDouble:timestamps])就被释放了,然而timestramps仍然有值,还是保存了原对象的地址。所以当使用objc_getAssociatedObject去取值的时候就会Crash。所以解决方案就是将OBJC_ASSOCIATION_ASSIGN改为OBJC_ASSOCIATION_RETAIN_NONATOMIC

通过进一步了解到,在分类使用对象关联添加的属性(property),他不是存放在类对象的内存中,因为在运行期间对象的内存布局已经确定。它存在于一个AccociationManager对象的AssociationHashMap中;AssociationHashMapkey是关联类对象的地址,valueObjectAssociationMapObjectAssociationMap的key是传递进来的 const void * _Nonnull key,value 是ObjcAssociation包括关联策略policy和关联对象value。

NSInteger 和 NSUInteger 进行比较

看下面的比较,你认为会输出多少?

1
2
3
4
5
6
7
NSInteger i = -1;
NSArray *arr = @[];
if (i < arr.count) {
NSLog(@"<");
} else {
NSLog(@">");
}

结果是控制台打印>,你可能会想一看不对呀,i==-1.arr.count==0,不应该是输出<么;带我细细讲来,这个问题我们小组好几个人都踩过这个坑,一般是在if条件判断数组个数的时候,或者for循环条件判断的时候。比如上面这种形式,结果走了else语句。

我们知道两个不同类型的比较,会进行隐式转换,即低精度会向高精度的数字转换,然后再进行比较,那上面代码的例子来说,if比较的NSInteger类型与NSUInteger类型,就是NSInteger类型会转化为NSUInteger类型,然后再比较。
所以有符号整型-1与无符号整型0的比较,会先将有符号的整形-1转化为无符号的-1。那有符号整型-1在计算机中是通过二进制补码的形式进行存储的,且最前面一位是符号为0正1负,补码=原码取反加一。以64位iPhoneX为例,-1的存储图位,有符号的-1存储在机器上时64个1;所当变成无符号的时候,其值就是2的64次方=18446744073709551615。

1
2
3
4
位数    63   62   62   ……   2    1    0
原码 1 0 0 0 0 0 1
反码 1 1 1 1 1 1 0
补码 1 1 1 1 1 1 1

大家可以在Xcode断点调试验证下。这里也说明了swift的安全严谨性,他在编译阶段就能够让你察觉这个错误。