总结一些iOS的底层面试题。巩固一下iOS的相关基础知识。
如有出入,还望各位大神指出。
OC对象
1. NSObject对象的本质是什么?
- NSObject对象的本质就是结构体
2. 一个NSObject对象占用多少内存?
-
NSObject对象创建实例对象的时候系统分配了16个内存(通过malloc_size函数可获得)
-
但是 NSObject只使用了8个字节 使用(class_getinstanceSize可获得)
3. 对象的isa指针指向哪里?
-
instance对象的isa指针指向class对象
-
class对象的isa指针指向 meta-class对象
-
meta-class对象的isa指针基类的meta-class对象
-
isa的优化
-
isa在arm64构架之前 isa的值就类对象的地址值。
-
isa在arm64构架开始的时候 采用了 isa优化的策略, 使用了共用体的技术。将64位的内存地址存储了很多东西,其中33位存储的是isa具体的地址值的。因为共用体中 前三位有存储的东西(),所以在&isa_mask出来的类对象地址值的二进制后面三位永远都是000, 十六进制就是8 或者0结尾的地址值
union isa_t { isa_t() { } isa_t(uintptr_t value) : bits(value) { } Class cls; uintptr_t bits; ///> typedef unsigned long struct { ///> 0 代表普通指针,存储着Class、MetaClass对象的内存地址 ///> 1 代表优化过,使用位域存储更多信息 uintptr_t nonpointer : 1; ///> 是否有设置过关联对象,如果没有,释放时会更快 uintptr_t has_assoc : 1; ///> 是否有C++的析构函数(.cxx_destruct),如果没有,释放会更快 uintptr_t has_cxx_dtor : 1; ///> 存储着Class、MetaClass的内存地址 uintptr_t shiftcls : 33; // MACH_VM_MAX_ADDRESS 0x1000000000 ///> 用于在调试时分辨率是否未完成初始化 uintptr_t magic : 6; ///> 是否被弱指针指向过? 如果没有,释放会更快 uintptr_t weakly_referenced : 1; ///> 对象是否正在释放 uintptr_t deallocating : 1; ///> 引用计数器是否过大?无法存储在isa中 ///> 如果为1,那么引用计数会存储在一个叫 side table的类属性中 uintptr_t has_sidetable_rc : 1; ///> 里面存储的值是引用计数器减1 uintptr_t extra_rc : 19; }; ... }
-
4. OC类的信息存储在哪里?
- meta-class存储:类方法
- class对象存储: 对象方法,属性,成员变量,协议信息
- instance存储: 成员变量具体的值
5. 说说你对函数调用的理解吧。
- 函数调用 实际实际上就是 给对象发送一条消息
- objc_msgSend(对象, @selectir(对象方法))
- 寻找顺序(对象方法) instance的isa指针找到类对象 在类对象中寻找方法,若没有向superClass中查找。
- 寻找顺序(类方法) instance的isa指针找到类对象 --> 类对象的isa找到meta-calss --> 在meta-class对象中寻找类方法,若没有向superClass中查找。
KVO
1. iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)
-
利用Runtime API 动态生成了一个新的类, 并且instance对象的isa指针指向这个生成的新子类。
-
当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueForKey函数
- willChangeValueForKey
- 父类原来的set方法
- didChangeValueForKey
- didChangeValueForKey 内部会触发observerValueForKeyPath方法 实现监听。
-
轻量级KVO框架:GitHub - facebook/KVOController
KVC
1. 使用KVC会不会调用KVO?
- 会调用KVO, 因为他的内部使用了:
- willChangeValueForKey
- 直接去_方式去更改
- didChangeValueForKey
- didChangeValueForKey 内部会触发
2. KVC的赋值和取值过程是怎么样的?原理是什么?
- 赋值
- setKey、_setKey 顺序查找方法
- 有: 传递参数调用防范
- 没有: 查看accessInstanceVariablesDirectly 方法返回值 yes 继续查找
- _key、_iskey、key、iskey 顺序查找
- setKey、_setKey 顺序查找方法
Category:
1. Category的使用场合是什么?
2. Category中的属性是否也存在类对象中?如果存在是怎么生成和存在的?如果不存在,它存在的位置在哪里?
- 一个类永远只有一个类对象
- 在运行起来之后 最重都会合并在 类对象中去。
3. Category的使用原理是什么?实现过程
-
原理:底层结构是结构体 categoty_t 创建好分类之后分两个阶段:
-
编译阶段:
将每一个分类都生成所对应的 category_t结构体, 结构体中存放 分类的所属类name、class、对象方法列表、类方法列表、协议列表、属性列表。
-
Runtime运行时阶段:
将生成的分类数据合并到原始的类中去,某个类的分类数据会在合并到一个大的数组当中(后参与编译的分类会在数组的前面),分类的方法列表,属性列表,协议列表等都放在二维数组当中,然后重新组织类中的方法,将每一个分类对应的列表的合并到原始类的列表中。(合并前会根据二维数组的数量扩充原始类的列表,然后将分类的列表放入前面)
-
-
调用顺序
-
为什么Category的中的方法会优先调用?
如上所述, 在扩充数组的时候 会将原始类中拥有的方法列表移动到后面, 将分类的方法列表数据放在前面,所以分类的数据会优先调用
-
延伸问题 - 如果多个分类中都实现了同一个方法,那么在调用该方法的时候会优先调用哪一个方法?
在多个分类中拥有相同的方法的时候, 会根据编译的先后顺序 来添加分类方法列表, 后编译的分类方法在最前面,所以要看 Build Phases --> compile Sources中的顺序。 后参加编译的在前面。
-
4. Category和Extension的区别是什么?
- Category 在运行的时候才将数据合并到类信息中
- Extension 在编译的时候就将数据包含在类信息中了 @interface Xxxx() 也叫做匿名分类
5. Category中有load方法吗?load是什么时候调用的?
- 有load方法
- load什么时候调用
- load方法在runtime加载类、分类的时候调用
6. load、initialize方法的区别是什么?它们在Category中的调用顺序?以及出现继承时他们之间的调用过程?当一个类有分类的时候为什么+load能多次调用儿initialize值调用了一次?
-
调用方式:
- load 根据函数地址直接调用
- initialize 是通过 objc_msgSend调用
-
调用时刻:
- load是runtime加载类、分类的时候调用(只会调用1次)
- initialize 是类第一次接收到消息的时候调用objc_msgsend()方法 如alloc、每一个类只会调用1次(但是父类的initialize方法可能会调用多次) 有些子类没有initialize方法所以调用父类的。
-
调用顺序:
- load:
- 先调用类的+load方法
- 按照编译的先后顺序调用(先编译、先调用)
- 调用子类的+load方法之前会先调用父类的+load
- 再调用分类的+load方法
- 按照编译的先后顺序调用(先编译、先调用)
- 先调用类的+load方法
- initialize
- 初始化父类
- 在初始化子类(可能最终调用的是父类的initialize方法)
- load:
-
load方法可以继承 我们在子类没有实现的时候可以调用,但是一般都是类自动去调用,我们不会主动调用,当子类没有实现+load方法的时候不会不会自动调用了就
<img src="/Users/yuangonmg/Library/Application Support/typora-user-images/image-20191112191237086.png" alt="image-20191112191237086" style="zoom:25%;" />
-
当一个类有分类的时候为什么+load能多次调用儿initialize值调用了一次?
- 根据源码看出来,+load 直接通过函数指针指向函数,拿到函数地址,找到函数代码,直接调用 分开来直接调用的 不是通过objc_msgsend调用的
- 而 initialize是通过消息发送机制,isa找到类对象找到方法调用的 所以只调用一次
7. Category能否添加成员变量?如果可以,如何给Category添加成员变量?
-
不能直接给Category添加成员变量,但是可以间接添加。
- 使用一个全局的字典 (缺点: 每一个属相都需要一套相同的代码)
///> DLPerson+Test.h @interface DLPerson (Test) ///> 如果直接使用 @property 只会生成方法的声名 不会生成成员变量和set、get方法的实现。 @property (nonatomic, assign) int weigjt; @end ///> DLPerson+Test.m #import "DLPerson+Test.h" @implemention DLPerson (Test) NSMutableDictionary weights_; + (void)load{ weights_ = [NSMutableDictionary alloc]init]; } - (void)setWeight:(int)weight{ NSString *key = [NSString stringWithFormat:@"%p",self]; weights_[key] = @(weight); } - (int)weight{ NSString *key = [NSString stringWithFormat:@"%p",self]; return [weights_[key] intValue] } @end
-
使用runtime机制给分类添加属性
```objc
#import<objc/runtime.h>
const void *DLNameKey = &DLNameKey
///> 添加关联对象
void objc_setAssociatedObject(
id object, ///> 给哪一个对象添加关联对象
const void * key, ///> 指针(赋值取值的key) &DLNameKey
id value, ///> 关联的值
objc_AssociationPolicy policy ///> 关联策略 下方表格
)
eg : objc_setAssociatedObject(self,@selector(name),name,OBJC_ASSOCIATION_COPY_NONATOMIC);
///> 获得关联对象
id objc_getAssociatedObject(
id object, ///> 哪一个对象的关联对象
const void * key ///> 指针(赋值取值的key)
)
eg:
objc_getAssociatedObject(self,@selector(name));
/// _cmd == @selector(name);
objc_getAssociatedObject(self,_cmd);
///> 移除所有的关联对象
void objc_removeAssociatedObjects(
id object ///>
)
- objc_AssociationPolicy(关联策略)
|objc_AssociationPolicy(关联策略) |对应的修饰符|
|:---|:---|:---|:---|
|OBJC_ASSOCIATION_ASSIGN | assign |
|OBJC_ASSOCIATION_RETAIN_NONATOMIC | strong, nonatomic |
|OBJC_ASSOCIATION_COPY_NONATOMIC | copy, nonatomic |
|OBJC_ASSOCIATION_RETAIN | strong, atomic |
|OBJC_ASSOCIATION_COPY | copy, atomic |
Block
1. block的原理是怎样的?本质是什么
- block的本质就是一个oc对象 内部也有isa指针, 封装了函数及调用环境的OC对象,
2. 看代码解释原因
int main(int argc, const char *argv[]){
@autoreleasepool{
int age = 10;
void (^block)(void) = ^{
NSLog(@" age is %d ",age);
};
age = 20;
block();
}
}
/*
输出结果为? 为什么?
输出结果是: 10
如果没有修饰符 默认是auto
为了能访问外部的变量
block有一个变量捕获的机制
因为他是局部变量 并且没有用static修饰
所以它被捕获到block中是 一个值,外部再次改变时 block中的age不会改变。
*/
变量类型 | 捕获到Block内部 | 访问方式 | |
---|---|---|---|
局部变量 | auto | ✅ | 值传递 |
局部变量 | static | ✅ | 指针传递 |
全局变量 | ❌ | 直接访问 |
int main(int argc, const char *argv[]){
@autoreleasepool{
int age = 10;
static int height = 10;
void (^block)(void) = ^{
NSLog(@" age is %d, height is %d",age, height);
};
age = 20;
height = 20;
block();
}
}
/*
输出结果为? 为什么?
age is 10, height is 20
局部变量用static 修饰之后 捕获到block中的是 height的指针,
因此修改通过指针修改变量之后 外部的变量也被修改了
*/
int age = 10;
static int height = 10;
int main(int argc, const char *argv[]){
@autoreleasepool{
void (^block)(void) = ^{
NSLog(@" age is %d, height is %d",age, height);
};
age = 20;
height = 20;
block();
}
}
/*
输出结果为? 为什么?
age is 20, height is 20
因为 age 和 height是全局变量不需要捕获直接就可以修改
全局变量 对应该就可以访问,
局部变量 需要跨函数访问,所以需要捕获
因此修改通过指针修改变量之后 外部的变量也被修改了
*/
int main(int argc, const char *argv[]){
@autoreleasepool{
void (^block)(void) = ^{
NSLog(@" self %p",self);
};
block();
}
}
/*
self 会不会被捕获?
因为函数默认会有两个参数 void test(DLPerson *self, SEL _cmd)
所以self 也是一个局部变量
访问@property(nonatmic, copy) NSString *name;
因为他是成员变量, 访问的时候 用self.name 或者 self->_name 访问 所以 block在内部会捕获self。
*/
3. 既然block是一个OC对象,那么block的对象类型是什么?
- ios内存分为5发区域
- 堆: 动态分配内存,需要程序员申请,也需要程序员自己管理(alloc、malloc等...)
- 栈: 放一些局部变量,临时变量 系统自己管理
- 静态区(全局区): 存放全局的静态对象。(编译时分配,APP结束由系统释放)
- 常量区: 常量。(编译时分配,APP结束由系统释放)
- 代码区: 程序区
-
block有三种类型,最终都继承自NSBlock类型
- superClass
NSGlobalBlock : __NSGlobalBlock : NSBlock : NSObject
block类型 环境 存放位置 NSGlobalBlock 没有访问auto变量 静态区 NSStackBlock 访问了auto变量 栈 NSMallocBlock NSStackBlock调用了copy 堆 - superClass
-
NSStackBlock调用了copy 代码实例
void (^block)(void); void (^block1)(void); void test2(){ int age = 10; block = ^{ NSLog("age is %d",age); /* 因为 block访问了 auto变量 所以目前block的类型为NSStackBlock类型, 存放的位置在 栈 上 在 main访问是 这个block已经被释放了。 */ }; [block1 = ^{ NSLog("age is %d",age); /* 因为 block访问了 auto变量 并且 进行了copy操作 所以目前block的类型为 NSMallocBlock 类型 存放的位置在 堆 上 在 main访问是 这个block是可以被访问的。 */ } copy]; } int main(int argc, const char *argv[]){ @autoreleasepool{ test2(); block(); /// 输出的值为 很大不是想要的值 block1();/// 输出的值是10 } }
-
每一种类型的block调用了copy之后结果如下所示
block的类型 副本源的配置存储域 复制后的区域 NSGlobalBlock 程序的数据区域 什么都不做 NSStackBlock 栈 从栈复制到堆 NSMallocBlock 堆 引用计数器+1
4. 在什么情况下 ARC环境下,编译器会根据情况自动将栈上的block复制到堆上?
-
block作为函数的返回值的时候
-
将block赋值给__strong指针时
-
block作为 Cocoa API中方法名含有usingBlock的方法参数时
NSArray *arr = @[]; /// 遍历数组中包含 usingBlock方法的参数 [arr enumerateObjectUsingBlock:^(id _Nonnullobj, NSUInteger idx, Bool _Nonnull stop){ }] ;
-
block作为GCD属性的建议写法
static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ }); disPatch_after(disPatch_time(IDSPATCH_TIME_NOW, (int64_t)(delayInSecounds *NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ });
-
MRC下block属性建议写法
- @property (copy, nonatomic) void (^block)(void);
-
ARC下block属性建议写法
- @property (strong, nonatomic) void (^block)(void);
- @property (copy, nonatomic) void (^block)(void);
5. __weak的作用是什么?有什么使用注意点?
- __weak 是一个修饰符
- 当block内部访问的对象类型的auto变量
- 如果block是在栈上,将不会对auto变量产生强引用
- 如果Block被拷贝到堆上
- 会调用block内部的copy函数
- copy函数会调用源码中的_Block_object_assign函数
- _Block_object_assign函数会根据修饰 auto 变量的修饰符(__strong、__weak 、__unsafe_unretained)来决定作出相应的操作,形成强引用或者弱引用
- block从对上移除
- 会调用block内部的dispose函数
- dispose函数会调用源码中的 _Block_object_dispose函数
- _Block_object_dispose函数会自动释放auto变量(release)
6. __block的作用是什么?有什么使用注意点?
-
block为什么不能修改外部变量的值?
- 因为block也是一个函数调用(可以说他们是两个函数, block调用另一个函数的变量是不能调的)
- 在C++内部 block是调用的另一个函数 实现。
-
__block修饰之后会将变量包装成一个对象 可以解决block内部无法修改auto变量的问题
-
包装秤对象之后就可以通过指针修改 外部的变量了
-
使用注意点: 在MRC环境下不会对指向的对象产生强引用的
7. __block的属性修饰词是什么?为什么?使用block有哪些注意点?
- 修饰词是copy
- block 如果没有进行copy操作就不会再堆上, 在堆上才能控制它的生命周期
- 注意循环引用的问题
- 在ARC环境下 使用呢strong和copy都可以没有区别 在MRC环境下有区别
- block是一个对象, 所以block理论上是可以retain/release的. 但是block在创建的时候它的内存是默认是分配在栈(stack)上, 而不是堆(heap)上的. 所以它的作用域仅限创建时候的当前上下文(函数, 方法...), 当你在该作用域外调用该block时, 程序就会崩溃.
8. block在修饰NSMutableArray,需不需要添加__block?
-
不需要
NSMutableArray *array = [[NSMutableView alloc]init] void (^block)(void) = ^{ array = nil; ///> 这样操作是需要__block的。 ///> ///> 下面这个是不需要 __block修饰的,因为这个只是使用它的指针而不是修改它的值 [array addObject:@"aa"]; [array addObject:@"aa"]; }
- 在修改NSMutableArray的数组的时候 并不是在修改这个数据 而是在使用这个指针去
Runtime
22. 讲一下OC的消息机制
- OC中的方法调用最后都是 objc_msgSend函数的调用,给receiver(方法调用者)发送了一条消息(selector方法名)
- objc_msgSend底层有三大模块
- 消息发送(当前类、父类中查找)、动态方法解析、消息转发
23. 消息转发机制流程
在objc_msgSend有三大阶段
- 消息发送阶段
- 给当前类发送一条消息,
- 会先从当前的类中的缓存查找
- 如果没有去遍历 class_rw_t 方法列表查找
- 如果没有再去父类的缓存查找
- 如果没有在去父类的class_rw_t方法列表中查找
- 循环父类 如果找到调用方法, 并且将方法缓存到 方法调用者的方法缓存中
- 如果一直没有到下一个阶段 动态解析阶段
- 动态解析阶段
- 动态解析会调用-resolveInstanceMethod \ +resolveClassMethod 方法 在方法中手动添加class_addMethod方法的调用。
- 只会解析一次 会将是否解析过的参数置位YES
- 然后在次 调用消息发送的阶段
- 如果我们实现了 方法的添加 则在消息发送阶段可以找到这个方法
- 调用方法并 将方法缓存到 方法调用者的缓存中
- 如果没有实现, 在第二次走到动态解析阶段,不会进入动态解析,因为上一次已经解析过了
- 我们将动态解析过的参数设置为YES,所以会走到下一个阶段 消息转发阶段
- 消息转发阶段
- 第一种: 实现了forwardingTargetForSelector方法
- 调用forwardingTargetForSelector 方法(返回一个类对象), 直接使用我们设置的类去发送消息。
- 第二种: 没有实现forwardingTargetForSelector
- 回去调用 methodSignatureForSelector 方法,在这个方法添加方法签名
- 之后会调用forwardInvocation 方法, 在这个方法中我们 [anInvocation invokeWithTarget:类对象];
- 或者其他操作都可以 这里没有什么限制。
- 第一种: 实现了forwardingTargetForSelector方法
24. 什么是runtime? 平时项目中有用过吗?
- OC是一门动态性比较强的语言,允许很多操作推迟到程序运行时才进行
- OC的动态性是由runtime来支撑实现的,runtime是一套C语言的API,封装了许多动态性相关的函数
- 平时写的代码 底层都是转换成了 runtime的API进行调用的
- 具体应用
- 关联对象,给分类添加属性,set和get的实现
- 遍历类的成员变量 归档解档、字典转模型
- 交换方法(系统的交换方法)
26. iskindOfClass 和 isMemberOfClass的区别?
-
isMemberOfClass源码:
/// 返回的直接是 是否是当前的类, 当 象 - (BOOL)isMemberOfClass:(Class)cls { return [self class] == cls; } /// 返回的直接是 是否是当前的类, /// 当前元类对象 + (BOOL)isMemberOfClass:(Class)cls { return object_getClass((id)self) == cls; }
-
iskindOfClass源码:
/// for循环查找 , 会根据当前类和 当前类的父类去逐级查找 , - (BOOL)isKindOfClass:(Class)cls { for (Class tcls = [self class]; tcls; tcls = tcls->superclass) { if (tcls == cls) return YES; } return NO; } /// for循环查找 , 会根据当前类和 当前类的额父类去逐级查找 , /// 当前元类对象 + (BOOL)isKindOfClass:(Class)cls { for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) { if (tcls == cls) return YES; } return NO; }
-
相关面试题:
// NSLog(@"%d", [[NSObject class] isKindOfClass:[NSObject class]]); // NSLog(@"%d", [[NSObject class] isMemberOfClass:[NSObject class]]); // NSLog(@"%d", [[MJPerson class] isKindOfClass:[MJPerson class]]); // NSLog(@"%d", [[MJPerson class] isMemberOfClass:[MJPerson class]]); /// 上面的写法 与 下面的写法 相同 // 这句代码的方法调用者不管是哪个类(只要是NSObject体系下的、继承于NSObject),都返回YES NSLog(@"%d", [NSObject isKindOfClass:[NSObject class]]); // 1 NSLog(@"%d", [NSObject isMemberOfClass:[NSObject class]]); // 0 NSLog(@"%d", [MJPerson isKindOfClass:[MJPerson class]]); // 0 NSLog(@"%d", [MJPerson isMemberOfClass:[MJPerson class]]); // 0 // 输出的结果是什么?
Runloop
1. 讲讲Runloop片在项目中的应用
Runloop
多线程
1. iOS中常见的多线程方案
技术方案 | 简介 | 语言 | 线程生命周期 | 使用频率 |
---|---|---|---|---|
pthread | 1. 一套通用的多线程API 2. 适用于Unix/Linux/Windows等系统 3. 跨平台、可移植 4. 使用难度大 |
C | 程序员管理 | 几乎不用 |
NSThread | 1. 使用更加面向对象 2. 简单易用,可直接操作线程对象 |
OC | 程序员管理 | 偶尔使用 |
GCD | 1. 旨在代替NSThread等线程技术 2. 充分利用设备的多核 |
C | 自动管理 | 经常使用 |
NSOperation | 1. 基于GCD(底层是GCD) 2. 比GCD多了一些简单使用的功能 3. 使用更加面向对象 |
OC | 自动管理 | 经常使用 |
2. GCD的常用函数
- GCD中有2个用来执行任务的函数
- 用同步的方式执行任务
- dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
- queue: 队列
- block: 任务
- 用异步的方式执行任务
- dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
- 用同步的方式执行任务
3. GCD的队列
- GCD的队列可以分为2大类型
- 并发队列
- 可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)
- 并发功能只有在异步(dispatch_async)函数下才有效
- 串行队列
- 让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务)
- 并发队列
4. 组合队列执行表
并发队列 | 手动创建串行队列 | 主队列 | |
---|---|---|---|
同步 | 没有开启新线程 串行执行任务 |
没有开启新线程 串行执行任务 |
没有开启新线程 串行执行任务 |
异步 | 开启新线程 并行执行任务 |
开启新线程 串行执行任务 |
没有开启新线程 串行执行任务 |
5. GCD的线程锁
- (void)interview01{
///> 会发生死锁,
NSLog(@"任务1");
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
NSLog(@"任务2");
});
NSLog(@"任务3");
/// dispatch_sync 需要立马在当前线程 同步执行任务 当前在主线程中
/// 而主队列需要等 主线程的东西执行完之后才会执行。 所以造成了死锁
}
- (void)interview02{
///> 不会发生死锁
NSLog(@"任务1");
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue, ^{
NSLog(@"任务2");
});
NSLog(@"任务3");
// dispatch_sync 不需要需要立马在当前线程 同步执行任务 所以等待主线程执行结束之后才执行的
}
- (void)interview03{
///> 会产生死锁
NSLog(@"任务1");
dispatch_queue_t queue = dispatch_queue_create("muqueue", DISPATCH_QUEUE_SERIAL); /// 同步;
dispatch_async(queue, ^{
NSLog(@"任务2");
dispatch_sync(queue, ^{ //死锁
NSLog(@"任务3");
});
NSLog(@"任务4");
});
NSLog(@"任务5");
}
- (void)interview04{
///> 不会产生死锁
NSLog(@"任务1");
dispatch_queue_t queue = dispatch_queue_create("muqueue", DISPATCH_QUEUE_SERIAL); /// 串行;
// dispatch_queue_t queue2 = dispatch_queue_create("muqueue2", DISPATCH_QUEUE_CONCURRENT); /// 并发;
dispatch_queue_t queue2 = dispatch_queue_create("muqueue2", DISPATCH_QUEUE_SERIAL); /// 串行; 也不会
dispatch_async(queue, ^{
NSLog(@"任务2");
dispatch_sync(queue2, ^{ //死锁
NSLog(@"任务3");
});
NSLog(@"任务4");
});
NSLog(@"任务5");
//不会产生死锁 因为两个任务不在同一个队列之中, 所以不存在互相等待的问题。
}
- (void)interview05{
///> 不会产生死锁
NSLog(@"任务1 thread:%@",[NSThread currentThread]);
dispatch_queue_t queue = dispatch_queue_create("muqueue2", DISPATCH_QUEUE_CONCURRENT); /// 并发;
dispatch_async(queue, ^{
NSLog(@"任务2 thread:%@",[NSThread currentThread]);
dispatch_sync(queue, ^{
NSLog(@"任务3 thread:%@",[NSThread currentThread]);
});
NSLog(@"任务4 thread:%@",[NSThread currentThread]);
});
NSLog(@"任务5 thread:%@",[NSThread currentThread]);
//不会产生死锁 因为两个任务不在同一个队列之中, 所以不存在互相等待的问题。
}
6. GCD的线程锁-- runloop有关的锁
- (void)test{
NSLog(@"2");
}
- (void)touchesBegan03{
// NSThread *thread = [[NSThread alloc]initWithBlock:^{
// NSLog(@"1");
//
// }];
// [thread start];
// [self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
// 运行后会崩溃 因为子线程 performSelector方法 没有开启runloop, 当执行test的时候这个线程已经没有了。
NSThread *thread = [[NSThread alloc]initWithBlock:^{
NSLog(@"1");
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc]init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}];
[thread start];
[self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
/// r添加开启runloop后 在线程中有runloop存在线程就不会死掉, 之后调用performSelect就没有问题了
}
- (void)touchesBegan02{
/// 创建全局并发队列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
NSLog(@"1");
//[self performSelector:@selector(test) withObject:nil];/// 打印结果 1 2 3 等价于[self test]
/// 这句代码点进去发现是在Runloop中的方法
/// 本质就是向Runloop中添加了一个定时器。 子线程默认是没有启动 Runloop的
[self performSelector:@selector(test) withObject:nil afterDelay:.0]; /// 打印结果 1 3
NSLog(@"3");
/// 启动runloop
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc]init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
});
}
- (void)touchesBegan01{
/// 创建全局并发队列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
NSLog(@"1");
// [self performSelector:@selector(test) withObject:nil];/// 打印结果 1 2 3 等价于[self test]
/// 这句代码点进去发现是在Runloop中的方法
// 本质就是向Runloop中添加了一个定时器。 子线程默认是没有启动 Runloop的
[self performSelector:@selector(test) withObject:nil afterDelay:.0]; /// 打印结果 1 3
NSLog(@"3");
});
}
7. GCD组队列的使用
- 异步并发执行任务1、任务2
- 等任务1、任务2都执行完毕后,再回到主线程执行任务3
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_queue_create("muqueue", DISPATCH_QUEUE_CONCURRENT);// 并发队列
dispatch_group_async(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务1 thread --- %@",[NSThread currentThread]);
}
});
dispatch_group_async(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务2 thread --- %@",[NSThread currentThread]);
}
});
///> 回到主线程执行 任务3
// dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// for (int i = 0; i < 5; i++) {
// NSLog(@"任务3 thread --- %@",[NSThread currentThread]);
// }
// });
///> 执行完任务1、2之后再执行任务3、4
dispatch_group_notify(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务3 thread --- %@",[NSThread currentThread]);
}
});
dispatch_group_notify(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务4 thread --- %@",[NSThread currentThread]);
}
});
}
8. 多线程安全隐患的解决方案
- 隐患造成, 多个线程同时访问一个数据然后对数据进行操作
- 解决方案:使用线程同步技术,
- 常见线程同步技术: 加锁
- iOS线程同步方案:
- OSSpinLock
- os_unfair_lock
- pthread_mutex
- dispatch_semaphore
- dispatch_queue(DISPATCH_QUEUE_SERIAL)
- NSLock
- NSRecursiveLock
- NSCondition
- NSConditionLock
- @synchronized
内存管理
性能优化
1. 什么是CPU和GPU
- CPU (Central Processing Unit,中央处理器 )
- 对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制 (Core Graphics)
- GPU (Graphics Processing Unit,图形处理器 )
-
纹理的渲染
-
2. 卡顿原因
-
cpu处理后GPU处理 若垂直同步信号早于GPU处理的速度那么会形成掉帧问题
- 在 iOS中有双缓存机制,有前帧缓存、后帧缓存
2. 卡顿优化 - CPU
- 尽量使用轻量级的对象,比如用不到事件处理的地方,可以考虑使用CALayer 替代 UIView
- UIView和CALayer的区别?
- CALayer是UIView的一个成员
- CALayer是专门用来显示东西的
- UIView是用来负责监听点击事件等
- UIView和CALayer的区别?
- 不要频繁的调用UIView的相关属性,比如frame、bounds、transform等属性,尽量减少不必要的修改。
- 尽量提前计算好布局,在有需要时一次性调整好对应的属性,不要多次修改属性。
- Autolayout会比直接设置frame消耗更多的CPU资源
- 图片的size最好跟UIImageView的size保持一致
- 因为如果超出或者UIImageView会对图片进行伸缩的处理
- 控制线程的最大并发数量
- 尽量把一些耗时的操作放到子线程
- 文本处理(储存计算和绘制)
- 图片处理(解码、绘制)
3. 卡顿优化 - GPU
- 尽量减少视图数量和层次
- 尽量避免短时间内大量图片的显示,尽可能将多张图片合成一张进行显示
- GPU能处理的最大纹理尺寸是4096x4096,一旦超过这个尺寸,机会占用CPU资源进行处理,所以纹理尽量不要超过这个尺寸。
- 减少透明视图,不透明的就设置opaque为YES
- 尽量避免离屏渲染
4. 离屏渲染
-
在OpenGL中,GPU有2中渲染方式
- On-Screen Rendering: 当前屏幕渲染,在当前用于显示的屏幕缓冲区进行渲染操作。
- Off-Screen Rendering: 离屏渲染,在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作
-
离屏渲染消耗性能原因:
- 需要创建新的缓冲区
- 离屏渲染的整个过程,需要多次切换上下文环境,先是从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束后,将离屏缓冲区的渲染结果显示到屏幕上,有需要将上下文环境从离屏切换到当前屏幕。
-
那些操作会出发离屏渲染?
-
光栅化 layer.shouldRasterize = YES;
-
遮罩,layer.mask =
-
圆角,同事设置layer.maskToBounds = YES、layer.cornerRadius大于0
- 考虑通过CoreGraphics绘制圆角,或者美工直接提供。
- 第一种方法:通过设置layer的属性
imageView.layer.masksToBounds = YES; [self.view addSubview:imageView];
- 第二种方法:使用贝塞尔曲线UIBezierPath和Core Graphics框架画出一个圆角
UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)]; imageView.image = [UIImage imageNamed:@"1"]; //开始对imageView进行画图 UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, NO, 1.0); //使用贝塞尔曲线画出一个圆形图 [[UIBezierPath bezierPathWithRoundedRect:imageView.bounds cornerRadius:imageView.frame.size.width] addClip]; [imageView drawRect:imageView.bounds]; imageView.image = UIGraphicsGetImageFromCurrentImageContext(); //结束画图 UIGraphicsEndImageContext(); [self.view addSubview:imageView];
- 第三种方法:使用CAShapeLayer和UIBezierPath设置圆角
#import "ViewController.h" @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)]; imageView.image = [UIImage imageNamed:@"1"]; UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:imageView.bounds byRoundingCorners:UIRectCornerAllCorners cornerRadii:imageView.bounds.size]; CAShapeLayer *maskLayer = [[CAShapeLayer alloc]init]; //设置大小 maskLayer.frame = imageView.bounds; //设置图形样子 maskLayer.path = maskPath.CGPath; imageView.layer.mask = maskLayer; [self.view addSubview:imageView]; }
- 推荐三种方式,对内存的消耗最少啊,渲染速度快
-
阴影, layer.shadowXXX
- 如果设置了layer.shadowPath就不会产生
-
4. 卡顿检测
-
平时所说的“卡顿”主要是因为在主线程执行了比较耗时的操作
-
可以添加Observer到主线程RunLoop中,通过监听RunLoop状态切换的耗时,以达到监控卡顿的目的
5. 耗电优化
- 耗电来源
- CPU处理 Processing
- 网络 Networking
- 定位 Location
- 图像 Graphice
- 尽可能降低CPU、GPU的功耗
- 少用定时器
- 优化I/O操作 文件读写
- 尽量不要频繁的写入小数据,最好批量一次性写入
- 读写大量重要的数据的时候,考虑使用dispatch_io,其提供了基于GCD的异步操作文件I/O的API。用dispatch_io系统会优化磁盘访问
- 数据量比较大的,建议使用数据库(比如SQList、CoreData)
- 网络优化
- 减少,压缩网络数据
- 多次请求结果相同,尽量使用缓存
- 使用断点续传,否则网络不稳定时可能多次传输相同的内容
- 网络不可用时尽量不要尝试执行网络请求
- 让用户可以取消长时间运行或者网络速度很慢的网络操作,设置合理的超时时间
- 批量传输,比如:下载视频流时,不要传输很小的数据包,直接下载整个文件或者一大块一大块的下载,如果下载广告,一次性多下载一些,然后慢慢展示。如果下载电子邮件,一次下载多封不要,一封一封的下载。
- 定位优化
- 如果只需要快速确定用户位置,最好用CLLocationManager的requestLocation方法。定位完成之后,会自动让定位硬件断电。
- 如果不是导航应用,尽量不要实时更新位置,定位完毕之后就关掉定位服务
- 尽量降低定位的精准度,如果没有需求的话尽量使用低精准度的定位。随软件自身要求
- 如果需要后台定位,尽量设置pausesLocationUpdatasAutomatically为YES,如果用户不太可能移动的时候系统会自动暂停位置更新
- App启动
- App启动分为两种
- 冷启动(Cold Launch):从零开始启动App
- 热启动(Warm Launch):App已经在内存中,在后台存活,再次点击图标启动App
- App启动时间的优化,主要针对于冷启动
- 通过添加环境变量可以打印app启动的时间分析(Edit scheme -> Run -> Arguments)
- DYLD_PRINT_STATISTICS设置为1
- 如果想要看更详细的内容,那就将DYLD_PRINTP_STATISTICS_DETAILS设置为1
- 通过添加环境变量可以打印app启动的时间分析(Edit scheme -> Run -> Arguments)
- 冷起订分为三大阶段
- dyld(dynamic link editor),Apple的动态连接器,可用来装在Mach-O文件(可执行文件,动态库等)
- 启动时做的事情
- 装载App的可执行文件,同时会递归加载所有依赖的动态库
- 当dyld把所有的可执行文件和动态库都装载完毕之后,会通知runtime进行下一步处理
- 启动时做的事情
- runtime
- 启动时runtime所做的事情
- 调用map_images进行可执行文件内容的解析和处理
- 在load_images中调用call_load_methods,调用所有Class和Catrgory的+load方法
- 进行各种Objc结构的初始化,(注册Objc类、初始化类对象等等)
- 调用C++静态初始化器和attribute((constructor))修饰的函数
- 到此为止可执行文件的动态库和所有的符号(Class、protocols、Selector、IMP...)都已经按格式成功加载到内存中,被runtime所管理
- 启动时runtime所做的事情
- main
- 总结一下
- APP的启动有dyld主导,将可执行文件加载到内存、顺便加载所有依赖的动态库
- 并有Runtime负责加载成objc定义的结构
- 所有初始化工作结束后,dyld就会调用main函数
-
接下来就是UIApplicationMain函数,AppDelegate的Application:didFinishLaunchingWithOptions:方法
image
- 总结一下
- dyld(dynamic link editor),Apple的动态连接器,可用来装在Mach-O文件(可执行文件,动态库等)
- App启动分为两种
- 如何优化启动时间
- dyld
- 减少动态库、合并一些动态库(定期清理不必要的动态库)
- 减少Objc类、分类的数量、减少Selector数量(定期清理不必要的类、分类)
- 减少C++虚函数数量
- swift尽量使用struct
- runtime
- 用+initialize方法和dispatch_once取代所有的attribute((constructor))、C++静态构造器、Objc的+load
- main
- 在不影响用户体验的前提下,尽可能将一些操作延迟,不要全部都放在finishLaunching方法中
- 按需求加载
- dyld
6. 安装包瘦身
-
安装包(IPA)主要由可执行文件、资源组成
-
资源(图片、音频、视频等)
- 采用无损压缩
- 去除没用到的资源文件(GitHub -查询多余的资源文件_下载链接)
-
可执行文件瘦身
-
编译器优化
- Strip Linked Product、Make Strings Read-Only、Symbols Hidden by Default设置为YES
- 去掉异常支持,Enable C++ Exceptions、Enable Objective-C Exceptions设置为NO,Other C Flags添加-fno-exceptions
-
利用AppCode(AppCode_下载链接)检测未使用的代码:
- 菜单栏 -> Code -> inspect Code
-
编写LLVM插件检测出重复代码、未被调用的代码
-
生成LinkMap文件,可以查看可执行文件的具体组成
image- 可借助第三方工具解析LinkMap文件 GitHub -检查每个类占用空间大小工具_下载链接
-
构架设计
1. 设计模式
-
设计模式(Design Pattern)
- 是一套被反复使用、代码设计经验的总结
- 使用设计模式的好处是:可重用代码、让代码更容易被他人理解、保证代码可靠性
- 一般与编程语言无关,是一套比较成熟的编程思想
-
设计模式可以分为三大类
- 创建型模式:对象实例化的模式,用于解耦对象的实例化过程
- 单例模式、工厂方法模式,等等
- 结构型模式:把类或对象结合在一起形成一个更大的结构
- 代理模式、适配器模式、组合模式、装饰模式,等等
- 行为型模式:类或对象之间如何交互,及划分责任和算法
- 观察者模式、命令模式、责任链模式,等等
- 创建型模式:对象实例化的模式,用于解耦对象的实例化过程
网友评论