一 、为什么会存在堆空间
堆空间的存在主要是为了延长对象的生命周期,并使得对象的生命周期可控。
- 如果试图用栈空间取代堆空间,显然是不可行的。栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,如果申请的空间超过栈的剩余空间时,将出现栈溢出,发生未知错误。因此,能从栈获得的空间较小。而堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。 但是栈空间比堆空间响应速度更快,所以一般类似int、NSInteger等占用内存比较小的通常放在栈空间,对象一般放在堆空间。
- 如果试图用数据区(全局区)取代堆空间,显然也是不可行的。因为全局区的生命周期会伴随整个应用而存在,比较消耗内存,生命周期不向在堆空间那样可控,堆空间中可以随时创建和销毁。
- 代码区就不用想了,如果能够轻易改变代码区,一个应用就无任何安全性可言了。
二 、Tagged Pointer 是什么?
从 64bit 开始,iOS 引入了Tagged Pointer技术,用于优化NSNumber、NSDate、NSString等小对象的存储。在没有使用Tagged Pointer之前, NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值;使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中。当指针不够存储数据时,会使用动态分配内存的方式来存储数据。
三 、iOS平台跨域访问漏洞?
UIWebView 默认开启了WebKitAllowUniversalAccessFromFileURLs 和 WebKitAllowFileAccessFromFileURLs 属性。利用这个漏洞给某个 App 下发一个 HTML 文件,当 UIWebView 使用 file 协议打开这个 HTML 文件, HTML 文件中含有一段窃取用户数据的 JS 代码,就会导致用户数据泄露。
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"index" ofType:@"html"];
_webView = [[UIWebView alloc] initWithFrame:self.view.bounds];
[_webView loadRequest:[NSURLRequest requestWithURL:[NSURL fileURLWithPath:filePath]]];
上面代码可以读取出手机端 /etc/passwd 的文件。这个漏洞访问其他应用的数据,而不必需要用户的许可。但WKWiebView的 WebKitAllowUniversalAccessFromFileURLs 和 WebKitAllowFileAccessFromFileURLs 默认是关闭的(可以手动控制),不会存在这样的风险。
补充:针对 https 请求UIWebView需要做额外处理,借助NSURLConnection做证书验证,而WKWebView无需做过多额外处理。
四 、iOS 9 以后通知不再需要手动移除
通知 NSNotification 在注册者被回收时需要手动移除,是一直以来的使用准则。原因是在 MRC 时代,通知中心持有的是注册者的 unsafe_unretained 指针,在注册者被回收时若不对通知进行手动移除,则指针指向被回收的内存区域,变为野指针。此时发送通知会造成 crash 。而在 iOS 9 以后,通知中心持有的是注册者的 weak 指针,这时即使不对通知进行手动移除,指针也会在注册者被回收后自动置空。因为向空指针发送消息是不会有问题的。
五 、NSUserDefaults 存储字典的一个坑
NSDictionary *dict = @{@1: @"1",
@2: @"2",
@3: @"3",
@4: @"4"};
[[NSUserDefaults standardUserDefaults] setObject:dict forKey:@"key"];
[[NSUserDefaults standardUserDefaults] synchronize];
执行上述代码会报如下错误:
[User Defaults] Attempt to set a non-property-list object {
3 = "3";
2 = "3";
1 = "1";
4 = "4";
} as an NSUserDefaults/CFPreferences value for key `key`
The value parameter can be only property list objects: NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary. For NSArray and NSDictionary objects, their contents must be property list objects.
......
And although NSDictionary and CFDictionary objects allow their keys to be objects of any type, if the keys are not string objects, the collections are not property-list objects.
苹果官网有上述这样一段话,能往 NSUserDefaults 里存储的对象只能是 property list objects,包括 NSData,NSString, NSNumber, NSDate, NSArray, NSDictionary,且对于 NSArray 和 NSDictionary 这两个容器对象,它们所包含的内容也必需是 property list objects。重点看最后一句话,虽然 NSDictionary 和 CFDictionary 对象的 Key 可以为任何类型(只要遵循 NSCopying 协议即可),但是如果当Key 不为字符串 string 对象时,此时这个字典对象就不能算是property list objects了,所以不能往 NSUserDefaults 中存储,不然就会报错。
六 、performSelector:afterDelay:的坑
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
NSLog(@"1");
[self performSelector:self withObject:@selector(test) afterDelay:.0];
NSLog(@"3");
});
- (void)test{
NSLog(@"2");
}
上述代码的执行结果并非 1 2 3 ,而是 1 3。原因是performSelector: withObject: afterDelay:的本质是往 RunLoop中添加定时器,而子线程默认是没有启动RunLoop。performSelector: withObject: afterDelay:接口虽然和performSelector:系列接口长得很类似。但前者存在于RunLoop相关文件,后者存在于NSObject相关文件。
七 、@autoreleasepool
autoreleasepool 使用
每次遍历的时候生成了很多占内存大的对象,如果交于默认的 autoreleasepool 去管理生命周期,会有因为内存飙升产生crash的风险,遍历过程中,可在适当的位置上去使用@autoreleasepool,一旦出了@autoreleasepool作用域,该作用域内的变量会立马释放。如:
for(int i = 0; i < 10000; i++){
@autoreleasepool {
}
}
但并不是所有的遍历方法都要加上@autoreleasepool,比如enumerateObjectsUsingBlock:方法,仔细阅读苹果官方文档,可发现该方法内部已经添加过@autoreleasepool处理。
autoreleasepool 底层
autoreleasepool 底层是个C++结构体,创建和销毁的时候分别会调用构造函数和析构函数。
struct __AtAutoreleasePool {
__AtAutoreleasePool() { // 构造函数,在创建结构体的时候调用
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
~__AtAutoreleasePool() { // 析构函数,在结构体销毁的时候调用
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
void * atautoreleasepoolobj;
};
系统默认 autoreleasepool
iOS 中有个默认的autoreleasepool,主线程的 Runloop 中注册了 2 个 Observer:
- 第1个Observer监听kCFRunLoopEntry事件,会调系统默认autoreleasepool的 objc_autoreleasePoolPush() ;
- 第2个Observer:
监听kCFRunLoopBeforeWaiting事件,会调系统默认autoreleasepool的 objc_autoreleasePoolPop()、objc_autoreleasePoolPush();
监听了kCFRunLoopBeforeExit事件,会调系统默认autoreleasepool的objc_autoreleasePoolPop();
autorelease和autoreleasepool
内存管理中调用alloc、new、copy、mutableCopy方法返回对象,在不需要这个对象时,要调用 release 或autorelease 来释放它,MRC 中通常会使用 release 和 autorelease。autorelease 一般是仅用在 MRC 中。
八 、如何对 NSMutableArray 进行 KVO
一般情况下只有通过调用 set 方法对值进行改变才会触发 KVO。但是在调用NSMutableArray的 addObject或removeObject 系列方法时,并不会触发它的 set 方法。所以为了实现NSMutableArray的 KVO,官方为我们提供了如下方法:
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key
在增删元素时,使用上述方法来获取要操作的可变数组,然后再执行添加或删除元素的操作,便能实现 KVO 机制。如:
@property (nonatomic, strong) NSMutableArray *arr;
//添加元素操作
[[self mutableArrayValueForKey:@"arr"] addObject:item];
//移除元素操作
[[self mutableArrayValueForKey:@"arr"] removeObjectAtIndex:0];
九、被忽略的UIViewController两对API
如何判断一个页面的viewWillAppear方法是 push 或 present 进来是调用的,还是 pop 或 dismiss 是调用的?一种比较笨拙的方法是通过添加属性标记是进入还是返回调用viewWillAppear方法。还有一种最简单的方法,是直接调用苹果提供的两对 API 。
针对 Push 和 Pop 或 add childViewController 和 remove childViewController 的 API:
@property(nonatomic, readonly, getter=isMovingToParentViewController) BOOL movingToParentViewController NS_AVAILABLE_IOS(5_0);
@property(nonatomic, readonly, getter=isMovingFromParentViewController) BOOL movingFromParentViewController NS_AVAILABLE_IOS(5_0);
针对 Present 和 Dismiss 的 API
@property(nonatomic, readonly, getter=isBeingPresented) BOOL beingPresented NS_AVAILABLE_IOS(5_0);
@property(nonatomic, readonly, getter=isBeingDismissed) BOOL beingDismissed NS_AVAILABLE_IOS(5_0);
十、抗压缩优先级
两个水平布局的label,两边间隔分别是12,中间间隔为8(懂意思就行)。如果两个label 都不设置宽度,则左边 label 会拉长,右边 label 自适应。
UILabel *label1 = [[UILabel alloc] initWithFrame:CGRectZero];
label1.backgroundColor = [UIColor redColor];
label1.text = @"我是标题";
[self.view addSubview:label1];
[label1 mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.equalTo(self.view);
make.left.equalTo(@(12));
}];
UILabel *label2 = [[UILabel alloc] initWithFrame:CGRectZero];
label2.backgroundColor = [UIColor redColor];
label2.text = @"我是描述";
[self.view addSubview:label2];
[label2 mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.equalTo(label1);
make.left.equalTo(label1.mas_right).offset(8);
make.right.equalTo(self.view).offset(-12);
}];
如果想让左边 label 自适应,右边 label 拉升,可以设置控件拉升阻力(即抗拉升),拉升阻力越大越不容易被拉升。所以只要 label1 的拉升阻力比 label2 的大就能达到效果。
//UILayoutPriorityRequired = 1000
[label1 setContentHuggingPriority:UILayoutPriorityRequired
forAxis:UILayoutConstraintAxisHorizontal];
// //UILayoutPriorityDefaultLow = 250
[label2 setContentHuggingPriority:UILayoutPriorityDefaultLow
forAxis:UILayoutConstraintAxisHorizontal];
Content Hugging Priority:拉伸阻力,即抗拉伸。值越大,越不容易被拉伸。
Content Compression Resistance Priority:压缩阻力,即抗压缩。值越大,越不容易被压缩。
十一、为什么会有深拷贝和浅拷贝之分
上图中观察可知只有不可变 + 不可变组合的时候才出现浅拷贝,其他三种情况都是深拷贝。原因在于,两个不可变对象内容一旦确定都是不可变的,所以不会彼此干扰,为了节省内容空间,两个对象可以指向同一块内存。而其他三种情况,都有可变对象的存在,为了避免两个对象之间的彼此干扰,所有会开辟额外的空间。
十一、为什么交叉方法出现"死循环"
因为交换了方法的实现 IMP ,如果alert_replaceInitWithString方法内部调用initWithString会出现真正的死循环。下面代码的死循环只是一个假象。
@implementation NSAttributedString (Exception)
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
@autoreleasepool {
[objc_getClass("NSConcreteAttributedString") swizzleMethod:@selector(initWithString:) swizzledSelector:@selector(alert_replaceInitWithString:)];
}
});
}
-(instancetype)alert_replaceInitWithString:(NSString*)aString{
if (!aString) {
NSString *string = [NSString stringWithFormat:@"[%s:%d行]",[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String],__LINE__];
[[[ExceptionAlert alloc]init]showAlertWithString:string];
;
return nil;
}
return [self alert_replaceInitWithString:aString];
}
@end