感谢大牛
iOS 面试 ChenYilong 推荐深入了解 runloop 推荐
iOS 多线程:『NSOperation、NSOperationQueue』详尽总结 推荐 文章底部还有 GCD 、NSThread、RunLoop 都不错
小知识集锦 推荐
1.dSYM你是如何分析的?
首先获取.crash 文件、.app文件、 dSYM文件,这个三个文件获取后放在一个文件夹下。通过 symblicatecrash工具进行解析
-
crash文件获取(连接真机)
- Xcode — Window — Devices And Simulators — View Device Logs — 选择 app — Export Log(这个就是 crash 文件)
- 通过 itunes获取 crash 文件
根据电脑操作系统的不同,崩溃日志将保存在以下位置:
Mac OS X:~/Library/Logs/CrashReporter/MobileDevice/
Windows XP:C:\Documents and Settings\Application Data\Apple computer\Logs\CrashReporter
Windows 7/Vista: C:\Users\电脑登陆的用户名\AppData\Roaming\Apple Computer\Logs\CrashReporter\MobileDevice
- itunes — 同步 — ~/Library/Logs/CrashReporter/MobileDevice/ — 目标手机文件目录 — 具体 app 名字的 crash 文件
-
Xcode — Organizer — Archives — 选择具体项目 — Show In Finder — XXXX 2019-2-27, 10.04 AM.xcarchive(XXX 项目文件名) — 显示包内容 — dSYMs目录下找到需要的符号表 — Products — Applications(文件夹下就是 .app 文件)
-
进入 文件夹下 运行 export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer
-
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/PrivateFrameworks/DTDeviceKitBase.framework/Versions/A/Resources/symbolicatecrash appName.crash appName.app > appName.log
2.多线程有哪几种?优缺点
线程创建 GCD 、 NSThread 、NSOperation(NSOperationQueue 和 NSOperation子类)。Operation是基于 GCD 更高一层的封装,完全面向对象。但是比 GCD 更简单易用、代码可读性也更高。
NSThread
优点
- NSThread 比其他两个轻量级。
缺点
- 需要自己管理线程生命周期,线程同步,线程同步加锁对性能有一定系统开销。
线程的生命周期 : 是指当前的 thread 完成后,需要调用 Exit方法释放线程。cancel只是标记线程的状态是cancel状态。exit会释放当前线程的资源
pthread_Detach (thread) 该线程运行结束后会自动释放所有线程持有的资源。
创建方式(两种方式)
[NSThread detachNewThreadSelector:@selector(doSomething:) toTarget:self withObject:nil];
NSThread *myThread = [[NSThread alloc] initWithTarget:self selector:@selector(doSomething:) object:nil]; [myThread start];
[self performSelectorInBackground:@selector(task3) withObject:nil];
Cocoa Operation
是基于 GCD 更高一层的封装,完全面向对象。但是比 GCD 更简单易用、代码可读性也更高。
优点
- 不需要关心线程管理,线程同步。
- 创建NSOperation对象(NSInvocationOperation 和 NSBlockOperation),把对象添加到NSOperationQueue队列里执行,我们会把我们的执行操作放在NSOperation中main函数中。
NSOperationQueue
- NSOperationQueue :自定义队列 通过 maxConcurrentOperationCount 为 1 是 串行队列,不等于1 是并行队列(默认是-1)。主队列 maxConcurrentOperationCount 不管是几,都是串行队列。
- 这里的队列指操作队列,即用来存放操作的队列(顺序优先级决定)。不同于 GCD 中的调度队列 FIFO(先进先出)的原则。
- NSOperationQueue 提供了两种类型队列,主队列([NSOperationQueue mainQueue] )和自定义队列([[NSOperationQueue alloc] init] )。主队列实在主线程执行,自定义队列实在后台自己执行。
- 使用 KVO 观察对操作执行状态的更改:isExecuteing、isFinished、isCancelled。GCD 就不行。
// 主队列
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
[mainQueue addOperationWithBlock:^{
//这个block语句块在主线程中执行
NSLog(@"mainQueue -- %@",[NSThread currentThread]);
}];
// 自定义队列
NSOperationQueue*oprationQueue= [[NSOperationQueue alloc] init];
[oprationQueue addOperationWithBlock:^{
//这个block语句块在子线程中执行
NSLog(@"oprationQueue --- %@",[NSThread currentThread]);
}];
NSOperation子类
- NSBlockOperation、NSInvocationOperation、自定义在主线程或者其他线程单独使用时会在当前线程执行,不会开启新的线程。
- NSBlockOperation 的 addExecutionBlock方法,NSBlockOperation 是否开启新线程,取决于操作的个数。如果添加的操作的个数多,就会自动开启新线程。当然开启的线程数是由系统来决定的。
- addExecutionBlock方法肯定会开辟线程执行,但是NSBlockOperation(blockOperationWithBlock)却不一定在当前线程执行,也可能回开辟线程。具体要看addExecutionBlock 任务多不多。
/**
* 设置 MaxConcurrentOperationCount(最大并发操作数)
*/
- (void)setMaxConcurrentOperationCount {
// 1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 2.设置最大并发操作数
queue.maxConcurrentOperationCount = 1; // 串行队列
// queue.maxConcurrentOperationCount = 2; // 并发队列
// queue.maxConcurrentOperationCount = 8; // 并发队列
// 3.添加操作
[queue addOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[queue addOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[queue addOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[queue addOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"4---%@", [NSThread currentThread]); // 打印当前线程
}
}];
}
NSOperation 操作依赖
- - (void)addDependency:(NSOperation *)op; 添加依赖,使当前操作依赖于操作 op 的完成。
- - (void)removeDependency:(NSOperation *)op; 移除依赖,取消当前操作对操作 op 的依赖。
- @property (readonly, copy) NSArray<NSOperation *> *dependencies; 在当前操作开始执行之前完成执行的所有操作对象数组。
/**
* 操作依赖
* 使用方法:addDependency:
*/
- (void)addDependency {
// 1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 2.创建操作
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
}
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
}
}];
// 3.添加依赖
[op2 addDependency:op1]; // 让op2 依赖于 op1,则先执行op1,在执行op2
// 4.添加操作到队列中
[queue addOperation:op1];
[queue addOperation:op2];
}
NSOperation 优先级
// 优先级的取值 typedef NS_ENUM(NSInteger, NSOperationQueuePriority) { NSOperationQueuePriorityVeryLow = -8L, NSOperationQueuePriorityLow = -4L, NSOperationQueuePriorityNormal = 0, NSOperationQueuePriorityHigh = 4, NSOperationQueuePriorityVeryHigh = 8 };
GCD
dispatch queue分为下面三种:
- private dispatch queues,同时只执行一个任务,通常用于同步访问特定的资源或数据。
- global dispatch queue,可以并发地执行多个任务,但是执行完成的顺序是随机的。
- Main dispatch queue 它是在应用程序主线程上执行任务的。
GCD概念:串行队列、并行队列、同步、异步、全局队列、主队列。
- 串行队列
- 任务依次执行
- 同一时间队列中只有一个任务在执行,每个任务只有在前一个任务执行完成后才能开始执行。
- 你不知道在一个Block(任务)执行结束到下一个Block(任务)开始执行之间的这段时间时间是多长。
- 并行队列
- 任务并发执行
- 你唯一能保证的是,这些任务会按照被添加的顺序开始执行。但是任务可以以任何顺序完成
- 你不知道在执行下一个任务是从什么时候开始,或者说任意时刻有多个Block(任务)运行,这个完全是取决于GCD
- 同步
- 完成需要做的任务后才会返回,进行下一任务。
- 不一定是多线程。
- 异步
- 不会等待任务完成才返回,会立即返回。
- 异步是多线程的代名词,因为必定会开启新的线程,线程的申请是由异步负责,起到开分支的作用。
- 全局队列
- 隶属于并行队列
- 不要与 barrier 栅栏方法搭配使用, barrier 只有与自定义的并行队列一起使用,才能让 barrier 达到我们所期望的栅栏功能。与 串行队列或者 global 队列 一起使用,barrier 的表现会和 dispatch_sync 方法一样。
- 主队列
- 隶属于串行队列
- 不能与 sync 同步方法搭配使用,会造成死循环
串行队列、并行队列、同步、异步四者的组合线程开启情况
同步 | 异步 | |
---|---|---|
串行队列 | 串行同步不会创建新的线程,是同步锁的替代方案。 | 会新建线程,只开一条线程。 每次使用 createDispatch 方法就会新建一条线程, 多次调用该方法,会创建多条线程,多条线程间会并行执行 |
并行队列 | 不会新建线程,依然在当前线程上 | 会创建新的线程,可以创建多条线程。 iOS7-SDK 时代一般是5、6条, iOS8-SDK 以后可以50、60条 |
串行队列中的同步与异步的区别
- 同步 dispatch_sync 会在等待 Block 执行完成后才会继续执行。
- 异步 dispatch_async 不会等待 Block 执行完成,同步执行 block 中的代码和其他任务。
// 串行队列
// dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", NULL); 也是串行的
dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(serialQueue, ^{
NSLog(@"1");
});
NSLog(@"2");
dispatch_sync(serialQueue, ^{
NSLog(@"3");
});
NSLog(@"4");
打印结果:1234
NOTE: 顺序执行
// 并行队列
dispatch_queue_t concurrentQueue =dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(serialQueue, ^{
NSLog(@"1");
});
NSLog(@"2");
dispatch_async(serialQueue, ^{
NSLog(@"3");
});
NSLog(@"4");
打印结果:2413 、 2143 、 1234 ,但有一点是可以确认的: 1 总是在 3 之前, 2 在 4 之前。
NOTE: 非顺序执行
3.单利的弊端
优点
- 单例对象实例变量只会创建一次,保证通过对象的单一性。
- 从节省资源的角度来说,只实例化一次后其他都是读取的缓存。
- 可以有可变数目的属性。
缺点
在实现了 - (instancetype)allocWithZone:(struct _NSZone *)zone 并返回单例对象的前提下
- 单例没有抽象层扩展性不好,主要针对继承来说 ,如果子类用自己的属性,但是初始化方法还是通过 ShareInstance 。在子类读取自己的属性时会导致闪退。对于类别来说只能单纯的扩展方法不能拥有原类的方法属性。
- 一个类只有一个对象可能造成单例功能太臃肿,内存也有可能占用过多。违背了"单一原则"
- 如果对象实例长时间不使用,系统会认为是垃圾因而会被回收。导致对象状态丢失。
4.介绍下App启动的完成过程
- 首先加载 Main 函数。
- 在 Main 函数中调用 UIApplicationMain 函数创建 application 的代理。
- 创建主循环(RunLoop),代理对象开始监听事件。
- 设置 UIWindow的根视图。
- 如果有 StoryBord 会加载 info.plsit 的
Main storyboard file base name
对应的对象控制器。 - 显示窗口
5.APP启动方法加载顺序
+ (void)load; // 这个方法是最先执行的
+ (void)initialize; // 启动的时先执行根视图的再返回来执行 Appdelegate 的
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions; // 这个方法创建 UIWindow ,也可以在这里进行授权操作、设置观察者、监听通知切换根视图。
父控件的 MianView - (instancetype)initWithFrame:(CGRect)frame; 方法,如果有 storyBord 就是 - (instancetype)initWithCoder:(NSCoder *)aDecoder;
子控件的 View - (instancetype)initWithFrame:(CGRect)frame; 方法,如果有 storyBord 就是 - (instancetype)initWithCoder:(NSCoder *)aDecoder;
.nib 文件先加载父类 MianView 的 - (void)awakeFromNib;
父类MianView加载完成,加载子控件 View的 - (void)awakeFromNib;
父类的VC - (void)loadView;
父类的VC - (void)viewDidLoad;
父类的VC - (void)viewWillAppear:(BOOL)animated;
父类的VC - (void)viewWillLayoutSubviews;
父类的 MainView - (void)layoutSubviews; // 不能把super layoutSubviews 忘记。这时的 frame 是最准确的。和这个方法会进行子控件的布局,布局基本完成。
父类的 VC - (void)viewDidLayoutSubviews;
子控件的 View 的 - (void)layoutSubviews;
父类的 MainView 的 - (void)drawRect:(CGRect)rect; // 绘制 UI、Quartz2D 绘图。通过 setNeedsDisplay 手动触发本方法。
父类的 VC - (void)viewDidAppear:(BOOL)animated;
Appdelegate 的 - (void)applicationDidBecomeActive:(UIApplication *)application;// app获取焦点。
6.静态库和动态库的区别
- 使用静态库时只需要拖到项目中,并在 library search path 设置静态库路径,但是动态库就多一个步骤 在 Build Setting — General — Embedded Binaries 中添加一下,否则在运行时会崩溃。
- 静态库不能有资源文件,只能把 bound 文件(资源文件)和.a 文件一起发出去。动态库被.framework文件包裹着,可以有资源文件。
- 静态库会被编译成二进制文件。动态只会拷贝到沙盒中,不会被编译进程序的二进制文件中。
- 静态库实际就是源码编译出的一些 .o 二进制文件的打了一个包。动态库(iOS系统库, OS X 没限制)可以在多个进程中共享,又叫共享库文件。程序在使用的时候会把 framework 映射都进程中的内存空间中(比如 iOS 沙盒),不会放在程序中。
- App启动的时候,系统会把所有链接的静态库全部放在分配好的内存空间中。如果一次性加载内容过多会导致启动过慢。自己编译的动态库,会通过 load 的方式加载进来。系统级别的动态库,只有用到的时候才加载到内存中。
7.APP启动慢可能是那些原因,那些因素影响启动
1.APP启动过程
- 解析 info.plist
- 加载相关信息。比如:APP 名字、icon、版本号、本地化语言、启动页(Launch screen)、程序入口(Main storyboard)。
- 沙盒建立、权限检查。
- Mach-O加载
Mach-O 是OS X 和 iOS 的可执行文件,和 window 的 PE 文件一样。
https://www.jianshu.com/p/71c75c287d26 介绍 Mach-O。
MachOView 区分是静态库、动态库,.a 结尾的不一定是静态库,.framework 结尾的也不一定是静态库。
如果是动态库,可以看到Shared Library标志;如果是静态库,可以看到Static Library标志。
MachOView下载地址:http://sourceforge.net/projects/machoview/
MachOView源码地址:<https://github.com/gdbinit/MachOView
- 如果是胖二进制文件,寻找合适当前 cpu类型的部分
- 加载所有依赖的 Mach-O 文件(递归调用 Mach-O加载方法)
- 定义内外部指针引用,字符串 、 函数。
- 执行命名为attribute 的C 语言函数。
- 加载类扩展(Category)中的方法。
- C++ 静态对象加载,调用 + Load 方法。
- 程序执行
- 调用 Main函数。
- 调用 UIAppliacationMain() 函数。
- 调用 ApplicationWillFinishLaunching。
2.影响启动性能的原因
- Main 函数之前导致 App 启动慢
- 静态库加载的过多,导致启动慢。减少非系统库的依赖,合并非系统库。
- Objc 类过多。
- C 的Constructor(构造函数)函数过多。Constructor修饰的函数会在 Main 函数之前调用。
- C++静态对象过多。
- Objc 的 Load 函数过多。
- Main 函数之后耗时影响因素
- RootViewController 加载 和 ChildViewController ,View 以及 subView 的加载。懒加载解决一次性加载问题。
解决方案:
1、用纯代码加载首页,而不是用 storybord。
2、didFinishLaunching 里面删除无用代码,用懒加载或者延迟加载。
3、对于 UI 无关的,比如授权 可以延迟加载。
4、对于实现了 + Load 的方法,如果可以延迟就延迟处理。
8. 0x8badf00d表示什么
看门狗超时。同步调用一个网络请求,造成主线程阻塞。
9.怎么防止反编译
- 数据加密。比如 需要存储在沙盒的数据、请求的数据(https 也可以抓包)、对程序中出现的 url 进行加密。
- 主要方法名字混淆。demo:https://github.com/IT-ZhongPeng/CodeObfuscation
- 文佳目录结构混排加密。 (不知道怎么做)
10. 什么情况使用 weak 关键字,和 assign 的区别
使用 weak 情况
- 防止循环应用、代理、一方已经强引用了。
不同点
- weak 表明是一种 "非拥有关系",对新值不保留,对旧值不释放。这个特性和 assign 类似。如果指向的值销毁,属性也会清空 (置为 nil)。
- assign 可以修饰 OC 对象,但是 weak 不能修饰非 OC 对象。
- assign 修饰的 OC 对象指向的值销毁后,属性值不清空。强制访问会闪退(野指针错误)。
- assign 修饰的基本数据类型 (int 、float)是存在栈区的,由系统释放。
11. 怎么用 copy 关键字
- NSArray、NSString、NSDictionary 因为他们有对应的可变类型,防止父类指针指向子类造成闪退。
- Block 关键字用 Copy,但是在 ARC 环境 copy 、strong都一样。
12. 这个写法会出什么问题: @property (copy) NSMutableArray *array;
- copy 是复制一个不可变的内容,这个属性用 NSMutableArray 赠、删、改、查会挂掉。
- atomic 影响性能。
13.如何让自己的类能用 copy 修饰,copy 属性的 setter 方法重写
类实现 copy 修饰
需要实现 NSCopying 协议,如果对象有可变版本也要把 NSMutableCopying 协议。
- 遵守 NSCopying 协议,并实现 - copyWithZone:(NSZone ) zone 方法。浅拷贝方式*。
- 如果遵守NSMutableCopying协议,并实现 - (id) mutableCopyWithZone:(NSZone )zone。深拷贝*。
copy setter方法重写
- (void)setName:(NSString *)name {
_name = [name copy];
}
14. @property 本质是什么, ivar 、 setter、getter是如何生成并添加到这个类的
本质
property 在运行时本质是 objc_property_t , objc_property_t 是结构体,其包含两个变量 name (const char *name;)也就是属性名字 、 attribute (const char *attributes;)。attribute 本质 是 objc_property_attribute_t ,objc_property_attribute_t 同样也是结构体,它包含 类型、原子性、关键字(内存语义)、实例变量
- property = ivar + setter + getter
属性的实现
完成属性定义后,编译器会自动编写访问这些程序所需要的方法,添加到method_list,这个过程叫 "自动合成"。
除了生成 setter、 getter 方法,编译器会自动像类中添加成员变量,以属性命前加下划线,添加到ivar_list,当做成员变量的名字。
都完成 把属性添加到 prop_list 。
属性自定义
- @synthesize 语法来指定实例变量的名字 。 name = _BigName;
15. @protocol 和 category 中如何使用 @property
- protocol 使用属性只会实现 setter 和 getter 方法声明,目的是让遵守我们协议的对象实现属性。
- category 使用属性也是只实现 setter 和 getter 方法声明 , 如果给类别添加属性的实现需要借助:
- objc_setAssociatedObject
- objc_getAssociatedObject
16. runtime 如何实现 weak
runtime 对注册的类,会进行布局。对 weak 修饰的对象放在一个 hash 表中,用 weak 属性指向的对象的内训地址 为 key , 属性的内存地址为 value 。当指向的对象引用计数为 0 时,就会 dealloc。通过 key 找到 value 并置为 nil
17. @property中有哪些属性关键字?/ @property 后面可以有哪些修饰符?
属性关键字
- nonatomic :原子性线程不安全、性能比较好、没有实现自旋锁。 atomic 实现了自旋锁,线程也不安全。
- readwrite 、readonly 读写权限
- assign、strong、weak、copy
- getter = <name> 、setter = <name> 指定特定方法
@property (nonatomic, getter=isOn) BOOL on;
18. weak属性需要在dealloc中置nil么?
- 不需要
19. @synthesize和@dynamic分别有什么作用?
synthesize
- 如果你没有手动实现 setter 方法和 getter 方法,那么编译器会自动为你加上这两个方法。
dynamic
- 属性的 setter 与 getter 方法由用户自己实现,不自动生成。
20.ARC下,不显式指定任何属性关键字时,默认的关键字都有哪些?
- 对应基本数据类型默认关键字是 atomic,readwrite,assign。
- 对于普通的 Objective-C 对象 atomic,readwrite,strong。
21. copy mutableCopy
对于非集合对象
对 immutable 对象进行 copy 操作,是指针复制,mutableCopy 操作时内容复制;对 mutable 对象进行 copy 和 mutableCopy 都是内容复制。
- [immutableObject copy] // 浅复制
- [immutableObject mutableCopy] //深复制
- [mutableObject copy] //深复制
- [mutableObject mutableCopy] //深复制
集合对象 NSArray、NSDictionary、NSSet
集合类对象中,对 immutable 对象进行 copy,是指针复制, mutableCopy 是内容复制;对 mutable 对象进行 copy 和 mutableCopy 都是内容复制。但是:集合对象的内容复制仅限于对象本身,对象元素仍然是指针复制。
- [immutableObject copy] // 浅复制
- [immutableObject mutableCopy] // 单层深复制
- [mutableObject copy] // 单层深复制
- [mutableObject mutableCopy] // 单层深复制
22. @synthesize合成实例变量的规则是什么?假如property名为foo,存在一个名为_foo
的实例变量,那么还会自动合成新变量么?
合成实例变量的规则
- 如果指定了成员变量的名称,会生成一个指定的名称的成员变量。@synthesize Name = _BigName;
- 如果这个成员已经存在了就不再生成了。
- 如果是
@synthesize foo;
还会生成一个名称为_foo的成员变量。如果没有指定成员变量的名称会自动生成一个属性同名的成员变量并加上下划线。 - 如果是
@synthesize foo = _foo;
就不会生成成员变量了。
假如property名为foo,存在一个名为_foo
的实例变量,那么还会自动合成新变量么?
- 不会
23. 什么情况下不会autosynthesis(自动合成)
- 同时重写了 setter 和 getter。
- 重写只读属性的 getter。
- 使用了 @dynamic。
- 在 @protocol 中定义的所有属性
- 在 category 中定义的所有属性
- 重载的属性
24. synthesize使用场景
- 重载了父类的属性。
- 手动设置属性的成员变量。
- 手动实现读写属性的setter 和 getter。
- 手动实现了只读属性的 getter
25. objc中向一个nil对象发送消息将会发生什么?
objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行,然后在发送消息的时候,objc_msgSend方法不会返回值,所谓的返回内容都是具体调用时执行的。 那么,回到本题,如果向一个nil对象发送消息,首先在寻找对象的isa指针时就是0地址返回了,所以不会出现任何错误。
- 在 Objective-C 中向 nil 发送消息是完全有效的,只是在运行时不会有任何作用。
26. objc中向一个对象发送消息[obj foo]和objc_msgSend()
函数之间有什么关系?
[obj foo];在objc编译时,会被转意为:
objc_msgSend(obj, @selector(foo));
。
27. 报unrecognized selector的异常如何补救?
- Method resolution
Objc运行时会调用 + (BOOL) resolveInstanceMethod 或者 + (BOOL) resolveClassMethod ,提供一个函数实现。如果没有继续往下走。
+ (BOOL)resolveInstanceMethod:(SEL)name
{
NSLog(@" >> Instance resolving %@", NSStringFromSelector(name));
if (name == @selector(MissMethod)) {
class_addMethod([self class], name, (IMP)dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:name];
}
- Fast forwarding
如果目标对象实现了 - (id)forwardingTargetForSelector:(SEL)aSelector ,可以把消息转发给其他类。 只要这个方法返回的不是nil和self,整个消息发送的过程就会被重启,当然发送的对象会变成你返回的那个对象。否则继续向下传。
- (id)forwardingTargetForSelector:(SEL)aSelector
{
Doctor *doctor = [[Doctor alloc]init];
if ([doctor respondsToSelector:aSelector]) {
return doctor;
}
return nil;
}
- Normal forwarding
这是最后一次补救机会,首先调用 -(NSMethodSignature *)methodSignatureForSelector:(SEL)selector 方法获取函数返回值类型 和 参数。如果没有就, Runtime则会发出
-doesNotRecognizeSelector:
消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime就会创建一个NSInvocation对象并发送-forwardInvocation:
消息给目标对象。http://blog.sina.com.cn/s/blog_8c87ba3b0102v006.html 解释下面的方法
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
{
NSString *sel = NSStringFromSelector(selector);
if ([sel rangeOfString:@"set"].location == 0){
return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
}else{
return [NSMethodSignature signatureWithObjCTypes:"@@:"];
}
}
- (void)forwardInvocation:(NSInvocation *)invocation
{
NSString *key = NSStringFromSelector([invocation selector]);
if ([key rangeOfString:@"set"].location == 0){
key= [[key substringWithRange:NSMakeRange(3, [key length]-4)] lowercaseString];
NSString *obj;
[invocation getArgument:&objatIndex:2];
[_propertiesDict setObject:obj forKey:key];
}else{
NSString *obj = [_propertiesDict objectForKey:key];
[invocation setReturnValue:&obj];
}
}
28. 一个objc对象如何进行内存布局?(考虑有父类的情况)
- 所有父类的成员变量和自己的成员变量都会存放在对象对应的存储空间内。
- 每个对象都有一个 isa 指针,指向它的类对象。类对象存放着本对象:
- 对象方法列表。
- 属性列表。
- 成员变量列表。
- 类对象也有 isa 指针,指向元对象。元对象存放着类方法列表。类对象还有一个superclass指针,指向它的父类对象。
- 根对象NSObject的 superclass指针指向 nil,isa指针指向自己。
29. 一个objc对象的isa的指针指向什么?有什么作用?
- 指向类对象,从而找到对象方法、属性列表、成员变量。
30. runtime如何通过selector找到对应的IMP地址?(分别考虑类方法和对象(实例)方法)
- 通过 isa 找到类对象,类中有对象方法列表。包含方法名称、参数、实现,通过 selector (方法名字)在方法列表中就能找到方法实现。
- 通过类对象的 isa 找到元对象,元对象存放着 类方法列表。具体寻找同上。
31. 使用runtime Associate方法关联的对象,需要在主对象dealloc的时候释放么?
- ARC 和 MRC 都不需要。因为 runloop每次循环会把 retaincount 为0和 Associate 的对象释放。
32.对象销毁的时间表
- 调用 - release ,引用计数变为零
- 对象正在被销毁,生命周期即将结束。
- 不能有新的 __weak 引用产生,否则直接指向 nil。
- 调用 [self dealloc];
- 子类调用 dealloc 函数
- 继承关系中的最底层子类,调用 dealloc 。
- 如果是 MRC 手动释放成员变量们。
- 继承关系的每一层父类,都掉用 dealloc。
- NSObject 调用 dealloc
- 只做一件事,调用 runtime 的object_dispose() 方法。
- 调用 object_dispose()
- 为C++ 的成员变量们(ivars) 调用destructors。
- 为 ARC 状态下的实例变量们(ivars), 调用 release。
- 解除所有使用 runtime associate 关联的对象。
- 解除所有__weak 引用。
- 调用 free()。
33. _objc_msgForward
函数是做什么的,直接调用它将会发生什么?
发送消息的流程是 通过对象的 isa 指针找到类,通过 objc_msgSend 首先在 class 的缓存表里面查找 IMP(没用缓存则初始化缓存),如果没找到则向父Class查找。如果一直没有找到则用_objc_msgForward 指针替代,最后调用。
- _objc_msgForward 是 IMP 指针类型,用于消息转发。当像一个对象发送消息时候,但是没有实现这个方法,_objc_msgForward会尝试消息转发。
34. 能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?
- 不能向编译后的对象添加对象。
因为编译后的类已经注册在 runtime中,类结构体中的 objc_ivar_list 实例变量链表和 instance_size 实例变量内存大小已经确定。同时runtime 会调用 objc_setIvarLayout 和 objc_setWeakIvarLayout 处理 Strong、weak 引用。所以不行。
- 可以向正在运行的类中添加实例变量。
运行时是可以的,调用 objc_addIvar 函数。但是点在objc_allocateClassPair 以后 objc_registerClassPair 之前。
网友评论