《iOS开发进阶》观后-中篇(OC底层原理篇)

简介

  • 本篇主要内容是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 系统底层实现

  • 《Block技巧与底层解析》这个说明说得比书本更详细。我简单总结一下,底层就是结构体存着函数地址起始,及其长度。在需要时调用该变量。但是中间过程会有变量的复制问题,和引用问题处理。

2 block中变量复制问题

  • __block修饰变量,在block中,使用能直接改变变量的值。

  • 没有__block修饰变量,在block使用,等于原变量copy了一个新变量,改变其值不影响原值。

3 循环引用 (ARC下)

  • 这个问题出现在block替换代理时,引用变量导致控制器不释放比较常见。因为copy导致原变量引用加一。

  • 所以block引用self,必须加上弱引声明。

__weak typeof(BaseViewController) *weakSelf = self;

4 《block基本用法》

内存管理 (参考《OC内存管理》

1 引用计数

  • 本质原因是因为对象和其他数据类型在系统中的存储空间不一样,其它局部变量主要存放于栈中,而对象存储于堆中,当代码块结束时这个代码块中涉及的所有局部变量会被回收,指向对象的指针也被回收,此时对象已经没有指针指向,但依然存在于内存中,造成内存泄露。

  • 每个OC对象都有自己的引用计数器,是一个整数表示对象被引用的次数,即现在有多少东西在使用这个对象。对象刚被创建时,默认计数器值为1,当计数器的值变为0时,则对象销毁。 在每个OC对象内部,都专门有4个字节的存储空间来存储引用计数器。判断对象要不要回收的唯一依据就是计数器是否为0,若不为0则存在。

  • block中,由于对象会copy,所有会做成原变量引用加入,所有会导致不释放。

2 ARC/MRC

  • MRC就是手工管理对象引用问题,ARC就是编译器处理引用问题,自动插入释放引用代码。实际衔接用MRC的可能就只有第三方库,真实开发暂时都是ARC。

3 新旧系统消息处理区别

  • 有一个坑IOS8以下系统对已被释放的对象发消息会导致APP崩溃,并且dealloc时移除通知.不然有可能奇奇怪怪的问题出现。

GCD (《iOS中GCD的使用小结》 《GCD死锁及取消》 《GCD 与 NSOperationQueue 区别》

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 信号量 (《浅谈GCD中的信号量》

//由于设定的信号值为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);       
    });<br>
    //任务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);       
    });<br>
    //任务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);       
    });   
}

原文:http://raychow.linkfun.top/2018/01/07/archives/1_ios/2017-section-2/index/

猜你喜欢

转载自my.oschina.net/u/3729367/blog/1606389