简介
- 本篇主要内容是OC底层原理篇总结,分别从OC对象模型、Tagged Pointer、block三个方面,在此过程中还会涉及一些相关的系统底层MCU指针PC指针概念类比。
前言
-
由于篇幅问题,会分三篇描述对应不同内容。辅助工具,底层原理,开发中要注意问题三个方面,谈谈对应的总结。本人觉得书只是一个索引,特别对于技术类书籍,基本都是通过书籍引入一些观点,然后在通过其它第三方途径进行扩展。所以本文描述内容不一定就是书本内容,会与自身实践经验,还有部分内容精简和拓展。有些基础概念第三方描述已足够详细,实例也足够详细,本文仅仅提出一些个人总结理解,不在重复描述具体功能原理。
-
文字不如图片直观,所以先上一张本系列描述的观点的思维导图,梳理脉络。红色部分为本文内容梳理。
OC对象模型
1 isa指针 (类比8位MCU pc指针)
-
在Objective-C中,任何类的定义都是对象。类和类的实例(对象)没有任何本质上的区别。
-
isa:是一个Class 类型的指针. 每个实例对象有个isa的指针,他指向对象的类,而Class里也有个isa的指针, 指向meteClass(元类)。元类保存了类方法的列表。当类方法被调用时,先会从本身查找类方法的实现,如果没有,元类会向他父类查找该方法。同时注意的是:元类(meteClass)也是类,它也是对象。元类也有isa指针,它的isa指针最终指向的是一个根元类(root meteClass).根元类的isa指针指向本身,这样形成了一个封闭的内循环。
-
类比8位MCU的pc指针,我个人理解isa指针,在硬件层面上就是指向代码的运行地址可以试RAM地址或ROM地址,等同MCU汇编的PC指针,调用函数时,需要先把当前函数地址压入堆栈,燃后放置新地址到PC指针,然后退去取回原堆栈内的地址。但是OC isa 在这个基础上增加了继承的概念。
-
以下是实例引用流程:
-
每一个对象本质上都是一个类的实例。其中类定义了成员变量和成员方法的列表。对象通过对象的isa指针指向类。
-
每一个类本质上都是一个对象,类其实是元类(meteClass)的实例。元类定义了类方法的列表。类通过类的isa指针指向元类。
-
所有的元类最终继承一个根元类,根元类isa指针指向本身,形成一个封闭的内循环。
2 runtime 运行时,动态方法交换
-
指一个程序在运行(或者在被执行)的状态。也就是说,当你打开一个程序使它在电脑上运行的时候,那个程序就是处于运行时刻。在一些编程语言中,把某些可以重用的程序或者实例打包或者重建成为“运行库"。这些实例可以在它们运行的时候被连接或者被任何程序调用。
-
objective-c中runtime:是一套比较底层的纯C语言API, 属于1个C语言库, 包含了很多底层的C语言API。 在我们平时编写的OC代码中, 程序运行过程时, 其实最终都是转成了runtime的C语言代码。
-
RunTime基本常用法 (《iOS开发进阶》页码:220 - 225 ps:这里说得不够细)
//1 动态增加变量@property (nonatomic, assign) BOOL isNotIgnore;//runtime 动态绑定 属性- (BOOL)isNotIgnore{ //_cmd == @select(isIgnore); 和set方法里一致 return [objc_getAssociatedObject(self, _cmd) boolValue];}- (void)setIsNotIgnore:(BOOL)isNotIgnore{ // 注意BOOL类型 需要用OBJC_ASSOCIATION_RETAIN_NONATOMIC 不要用错,否则set方法会赋值出错 objc_setAssociatedObject(self, @selector(isNotIgnore), @(isNotIgnore), OBJC_ASSOCIATION_RETAIN_NONATOMIC);}//2 对象方法的交换/** * 对象方法的交换 * * @param anClass 哪个类 * @param method1Sel 方法1 * @param method2Sel 方法2 */+ (void)exchangeInstanceMethod:(Class)anClass method1Sel:(SEL)method1Sel method2Sel:(SEL)method2Sel { Method method1 = class_getInstanceMethod(anClass, method1Sel); Method method2 = class_getInstanceMethod(anClass, method2Sel); method_exchangeImplementations(method1, method2);}//3 动态类方法的交换/** * 类方法的交换 * * @param anClass 哪个类 * @param method1Sel 方法1 * @param method2Sel 方法2 */+ (void)exchangeClassMethod:(Class)anClass method1Sel:(SEL)method1Sel method2Sel:(SEL)method2Sel { Method method1 = class_getClassMethod(anClass, method1Sel); Method method2 = class_getClassMethod(anClass, method2Sel); method_exchangeImplementations(method1, method2);}//4 动态类方法的交换/** * 对象方法重置 * * @param anClass 哪个类 * @param method1Sel 方法1 */+ (void)setClassMethod:(Class)anClass oldMethodSel:(SEL) oldMethodSel newMethodSel:(SEL)newMethodSel { Method oldMethod = class_getInstanceMethod(anClass, oldMethodSel); Method newMethod = class_getInstanceMethod(anClass, newMethodSel); method_setImplementation(oldMethod, method_getImplementation(newMethod));}
Tagged Pointer
1 32/64位设备区别
-
这个概念其实对应现在已经有点老了,32为设备已在苹果放弃支持的日程表上了。但是对于一些基础还是有必要了解一下。简单就是在64位设备上,同一地址裂开2部分使用,那其中32位最基础值类保存,另外位数存指针,索引表之类的数据,复用一个地址上的存储空间。这点和mcu内存一个byte,低4位和高4分开存在2个0-16的数值,是类似的。
-
假设我们要存储一个NSNumber对象,其值是一个整数。正常情况下,如果这个整数只是一个NSInteger的普通变量,那么它所占用的内存是与CPU的位数有关,在32位CPU下占4个字节,在64位CPU下是占8个字节的。而指针类型的大小通常也是与CPU位数相关,一个指针所占用的内存在32位CPU下为4个字节,在64位CPU下也是8个字节。
-
普通的iOS程序,如果没有Tagged Pointer对象,从32位机器迁移到64位机器中后,虽然逻辑没有任何变化,但这种NSNumber、NSDate一类的对象所占用的内存会翻倍
-
为了改进上面提到的内存占用和效率问题,苹果提出了Tagged Pointer对象。由于NSNumber、NSDate一类的变量本身的值需要占用的内存大小常常不需要8个字节,拿整数来说,4个字节所能表示的有符号整数就可以达到20多亿(注:2^31=2147483648,另外1位作为符号位),对于绝大多数情况都是可以处理的。
Block
1 系统底层实现
- 这个说明说得比书本更详细。我简单总结一下,底层就是结构体存着函数地址起始,及其长度。在需要时调用该变量。但是中间过程会有变量的复制问题,和引用问题处理。
2 block中变量复制问题
-
__block修饰变量,在block中,使用能直接改变变量的值。
-
没有__block修饰变量,在block使用,等于原变量copy了一个新变量,改变其值不影响原值。
3 循环引用 (ARC下)
-
这个问题出现在block替换代理时,引用变量导致控制器不释放比较常见。因为copy导致原变量引用加一。
-
所以block引用self,必须加上弱引声明。
__weak typeof(BaseViewController) *weakSelf = self;
4
内存管理 (参考)
1 引用计数
-
本质原因是因为对象和其他数据类型在系统中的存储空间不一样,其它局部变量主要存放于栈中,而对象存储于堆中,当代码块结束时这个代码块中涉及的所有局部变量会被回收,指向对象的指针也被回收,此时对象已经没有指针指向,但依然存在于内存中,造成内存泄露。
-
每个OC对象都有自己的引用计数器,是一个整数表示对象被引用的次数,即现在有多少东西在使用这个对象。对象刚被创建时,默认计数器值为1,当计数器的值变为0时,则对象销毁。 在每个OC对象内部,都专门有4个字节的存储空间来存储引用计数器。判断对象要不要回收的唯一依据就是计数器是否为0,若不为0则存在。
-
block中,由于对象会copy,所有会做成原变量引用加入,所有会导致不释放。
2 ARC/MRC
- MRC就是手工管理对象引用问题,ARC就是编译器处理引用问题,自动插入释放引用代码。实际衔接用MRC的可能就只有第三方库,真实开发暂时都是ARC。
3 新旧系统消息处理区别
- 有一个坑IOS8以下系统对已被释放的对象发消息会导致APP崩溃,并且dealloc时移除通知.不然有可能奇奇怪怪的问题出现。
GCD ( )
1 异步处理同步化
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_async(globalQueue, ^{ //子线程异步执行下载任务,防止主线程卡顿 NSURL *url = [NSURL URLWithString:@"http://www.baidu.com"]; NSError *error; NSString *htmlData = [NSString stringWithContentsOfURL:url encoding:NSUTF8StringEncoding error:&error]; if (htmlData != nil) { dispatch_queue_t mainQueue = dispatch_get_main_queue(); //异步返回主线程,根据获取的数据,更新UI dispatch_async(mainQueue, ^{ NSLog(@"根据更新UI界面"); }); } else { NSLog(@"error when download:%@",error); } });
2 相互依赖
- (void)groupSync{ dispatch_queue_t disqueue = dispatch_queue_create("com.shidaiyinuo.NetWorkStudy", DISPATCH_QUEUE_CONCURRENT); dispatch_group_t disgroup = dispatch_group_create(); dispatch_group_async(disgroup, disqueue, ^{ NSLog(@"任务一完成"); }); dispatch_group_async(disgroup, disqueue, ^{ sleep(8); NSLog(@"任务二完成"); }); dispatch_group_notify(disgroup, disqueue, ^{ NSLog(@"dispatch_group_notify 执行"); }); dispatch_async(dispatch_get_global_queue(0, 0), ^{ dispatch_group_wait(disgroup, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)); NSLog(@"dispatch_group_wait 结束"); });}
3 强制打断
- 业务场景 滚动scrolview,监听最后一次滚动效果。或请求超时强制放弃超时结果。或多次调用统一函数只取最后一次结果。
@property (nonatomic, strong) dispatch_block_t dispatch_stopLoading_block;@weakify(self)if (self.dispatch_stopLoading_block) { dispatch_block_cancel(self.dispatch_stopLoading_block); self.dispatch_stopLoading_block = NULL;}self.dispatch_stopLoading_block = dispatch_block_create(0, ^{ @strongify(self) NSLog(@"任务一完成");});//任务延迟启动dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), self.dispatch_stopLoading_block);
4 信号量 ()
//由于设定的信号值为2,先执行两个线程,等执行完一个,才会继续执行下一个,保证同一时间执行的线程数不超过2。-(void)dispatchSignal{ //crate的value表示,最多几个资源可访问 dispatch_semaphore_t semaphore = dispatch_semaphore_create(2); dispatch_queue_t quene = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); //任务1 dispatch_async(quene, ^{ dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); NSLog(@"run task 1"); sleep(1); NSLog(@"complete task 1"); dispatch_semaphore_signal(semaphore); }); //任务2 dispatch_async(quene, ^{ dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); NSLog(@"run task 2"); sleep(1); NSLog(@"complete task 2"); dispatch_semaphore_signal(semaphore); }); //任务3 dispatch_async(quene, ^{ dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); NSLog(@"run task 3"); sleep(1); NSLog(@"complete task 3"); dispatch_semaphore_signal(semaphore); }); }
原文: