一、你在项目中用过 runtime 吗?举个例子。
a、
Method Swizzling
动态交换方法实现,实则交换方法的IMP
(IMP有点类似函数指针,指向具体的Method实现)指向,应用场景是在不知道方法源码的情况下偷天换日,抢占方法实现的先机:
主要调用的方法是 method_exchangeImplementations
来交换2个方法中的IMP
;
运行时还有以下几个常用的方法:
1、利用 class_replaceMethod
来修改方class_replaceMethod
本身会尝试调用class_addMethod和method_setImplementation);
2、利用 method_setImplementation 来直接设置某个方法的IMP,
3、利用class_addMethod为一个类动态添加一个方法;
b、路由动态创建类,遍历类的属性为类的属性实现set方法并动态push
动态生成类:
Class getClass = NSClassFromString(classStr);
id creatClass = [[getClass alloc] init];
动态获取类的所有属性:
objc_property_t *properties = class_copyPropertyList([instance class], &outCount);
objc_property_t property = properties[i];
手动释放
free(properties);
动态为属性赋值set方法:
属性fromvc的set方法
SEL setFromSel = [FunctionUtility creatSetterWithPropertyName:@"fromvc"];
NSString *str = @"1";
[class performSelectorOnMainThread:setFromSel withObject:str waitUntilDone:[NSThread isMainThread]];
c、关联对象的使用为分类添加属性
- (void)setCustomTabbar:(UIView *)customTabbar {
//这里使用方法的指针地址作为唯一的key
objc_setAssociatedObject(self, @selector(customTabbar), customTabbar, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (UIView *)customTabbar {
return objc_getAssociatedObject(self, @selector(customTabbar));
}
关联对象可以降低代码的耦合度:
UIAlertView * alertView = [[UIAlertView alloc] initWithTitle:@"Alert" message:@"This is deprecated?"
delegate:self
cancelButtonTitle:@"Cancel"
otherButtonTitles:@"Ok", nil];
void (^block)(NSInteger) = ^(NSInteger buttonIndex){
if (buttonIndex == 0) {
[self doCancel];
} else {
[self doOk];
}
};
objc_setAssociatedObject(self.alertView, MyAlertViewKey, block, OBJC_ASSOCIATION_COPY);
#pragma -mark UIAlertViewDelegate
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
void (^block)(NSInteger) = objc_getAssociatedObject(alertView, MyAlertViewKey);
block(buttonIndex);
}
d、为button添加多参数
//runtime 为button添加多参数
objc_setAssociatedObject(contractBt, "contractUrl", obj[@"contractUrl"], OBJC_ASSOCIATION_RETAIN_NONATOMIC); //实际上就是KVC
http://southpeak.github.io/2014/10/25/objective-c-runtime-1/
二、你在项目中用过 GCD 吗?举个例子。
1、dispatch_semaphore
GCD信号量,最多几个资源可访问,
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);创建一个semaphore
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);等待耗时进程的结束;
dispatch_semaphore_signal 发送一个信号,写在耗时操作里(如上传通讯录的事件里,时间执行完发送信号)
参数为“1”的话最多一个并发,A进程执行后信号量减一变为0,B进程执行时返现信号量为0所以等待执行,A执行完信号量加一,B进程可以执行,信号量必须大于等于一进程才不会阻塞;这就是信号量来控制互斥原理;
2、dispatch_barrier_(a)sync
共同点:
等待在它前面插入队列的任务先执行完
等待他们自己的任务执行完再执行后面的任务
不同点:
dispatch_barrier_sync将自己的任务插入到队列的时候,需要等待自己的任务结束之后才会继续插入被写在它后面的任务,然后执行它们
dispatch_barrier_async将自己的任务插入到队列之后,不会等待自己的任务结束,它会继续把后面的任务插入到队列,然后等待自己的任务结束后才执行后面任务。
3、dispatch_group
往往是执行多个任务之后,最后再执行某个任务往往会使用到
dispatch_group
dispatch_queue_t queue = dispatch_queue_create("name", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_async(group, queue, ^{
NSLog(@"任务1");
});
dispatch_group_async(group, queue, ^{
NSLog(@"任务2");
});
dispatch_group_notify(group, queue, ^{
NSLog(@"任务3");
});
任务1和任务2执行完之后才会执行任务3;
多线程概念延伸:
并行:就是队列里面的任务(代码块,block)不是一个个执行,而是并发执行,也就是可以同时执行的意思
串行:队列里面的任务一个接着一个执行,要等前一个任务结束,下一个任务才可以执行
异步:具有新开线程的能力
同步:不具有新开线程的能力,只能在当前线程执行任务
那么,如果他们相互串起来,会怎么样呢?
并行+异步:就是真正的并发,新开有有多个线程处理任务,任务并发执行(不按顺序执行)
串行+异步:新开一个线程,任务一个接一个执行,上一个任务处理完毕,下一个任务才可以被执行
并行+同步:不新开线程,任务一个接一个执行
串行+同步:不新开线程,任务一个接一个执行
1.NSThread
(1)使用NSThread对象建立一个线程非常方便
(2)但是!要使用NSThread管理多个线程非常困难,不推荐使用
(3)技巧!使用[NSThread currentThread]跟踪任务所在线程,适用于这三种技术
2.NSOperation/NSOperationQueue
(1)是使用GCD实现的一套Objective-C的API
(2)是面向对象的线程技术
(3)提供了一些在GCD中不容易实现的特性,如:限制最大并发数量、操作之间的依赖关系
3.GCD —— Grand Central Dispatch
(1)是基于C语言的底层API
(2)用Block定义任务,使用起来非常灵活便捷
(3)提供了更多的控制能力以及操作队列中所不能使用的底层函数
主队列、全局队列、自定义的队列有什么区别
Grand Central Dispatch(多线程的优化技术)GCD是一套底层API,基于C语言开发的多线程机制,提供了新的模式编写并发执行的程序。
特点:
1、允许将一个程序切分为多个单一任务,然后提交到工作队列中并发或者串行地执行
2、为多核的并行运算提出了解决方案,自动合理的利用CPU内核(比如双核,四核)
3、自动的管理线程的生命周期(创建线程、调度任务、销毁线程),完全不需要我们管理,只需要告诉它任务是什么就行
4、配合Block,使得使用起来更加方便灵活
什么是Queue队列?
GCD使用了队列的概念,解决了NSThread难于管理的问题,队列实际上就是数组的概念,通常我们把要执行的任务放到队列中管理
特点:
1.按顺序执行,先进先出
2.可以管理多线程,管理并发的任务,设置主线程
3.GCD的队列是任务的队列,而不是线程的队列
什么是任务?
任务即操作:你想要干什么,说白了就是一段代码,在GCD中,任务就是一个block
任务的两种执行方式:
同步执行:只要是同步任务,都会在当前的线程执行,不会另开线程
异步执行:只要是异步任务,都会开启新线程,在开启的线程中执行
什么是串行队列?
依次完成每一任务
什么是并行队列?
好像所有的任务都是在同一时间执行的
都有哪些队列?
Main Queue(主队列,串行);全局队列(Global Queue);自己创建的队列(Queue)
从上面的概念以及gcd所解决的问题来看,使用GCD的时候就要开始转变观念了。现在我们需要考虑的只是任务,队列,队列间同步或异步的关系了。而不是考虑怎么开辟线程,怎么管理线程,所有关于线程的东西,我们都不需要考虑。整个程序完全就是由队列来自动管理了。首先,整个程序是由全局队列来管理,然后UI的刷新是由mainqueue管理,我们可以将我们的任务放到我们创建的队列中去,也可以放在主队列中,也可以放在全局队列中。
举个例子:现在我们要从网络上下载一张图片,可以直接将任务放到主队列中去执行,但是这样会出项一个问题,因为主队列是处理和UI相关的任务的,所以在创建(或者刷新)UI的时候,界面就会卡住,直到这张图片现在完毕。所以一般的做法是首先获取全局队列,然后在全局队列中植入下载图片的代码,在下完成后,将图片刷回UI,也就是主队列。至于图片什么时候下载完毕,怎么开辟线程,我们都不需要管。需要做的就是处理代码逻辑就行。以下是使用GCD下载图片的代码
+ (void)SL_setImageView:(UIImageView *)imageView url:(NSString *)url
{
//对应全局队列开启一个异步任务
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//下载图片数据
NSURL *imageURL = [NSURL URLWithString:url];
NSData *data = [NSData dataWithContentsOfURL:imageURL];
UIImage *image = [UIImage imageWithData:data];
//刷新ImageView (回调主线程)
dispatch_async(dispatch_get_main_queue(), ^{
imageView.image = image;
});
});
}
关于容易混淆概念的区分: 同步,异步,串行,并发
同步和异步代表会不会开辟新的线程。串行和并发代表任务执行的方式。
同步串行和同步并发,任务执行的方式是一样的。没有区别,因为没有开辟新的线程,所有的任务都是在一条线程里面执行。
异步串行和异步并发,任务执行的方式是有区别的,异步串行会开辟一条新的线程,队列中所有任务按照添加的顺序一个一个执行,异步并发会开辟多条线程,至于具体开辟多少条线程,是由系统决定的,但是所有的任务好像就是同时执行的一样。
开辟队列的方法:
dispatch_queue_t myQueue = dispatch_queue_create("MyQueue", NULL);
/**
参数1:标签,用于区分队列
参数2:队列的类型,表示这个队列是串行队列还是并发队列NUll表示串行队列,
DISPATCH_QUEUE_CONCURRENT表示并发队列
*/
执行队列的方法
异步执行
dispatch_async(<#dispatch_queue_t queue#>, <#^(void)block#>)
同步执行
dispatch_sync(<#dispatch_queue_t queue#>, <#^(void)block#>)
主队列
主队列:专门负责调度主线程度的任务,没有办法开辟新的线程。所以,在主队列下的任务不管是异步任务还是同步任务都不会开辟线程,任务只会在主线程顺序执行。
主队列异步任务:现将任务放在主队列中,但是不是马上执行,等到主队列中的其它所有除我们使用代码添加到主队列的任务的任务都执行完毕之后才会执行我们使用代码添加的任务。
主队列同步任务:容易阻塞主线程,所以不要这样写。原因:我们自己代码任务需要马上执行,但是主线程正在执行代码任务的方法体,因此代码任务就必须等待,而主线程又在等待代码任务的完成好去完成下面的任务,因此就形成了相互等待。整个主线程就被阻塞了。
全局队列
全局队列:本质是一个并发队列,由系统提供,方便编程,可以不用创建就直接使用。
获取全局队列的方法:dispatch_get_global_queue(long indentifier.unsigned long flags)
/**
参数说明:
参数1:代表该任务的优先级,默认写0就行,不要使用系统提供的枚举类型,因为ios7和ios8的枚举数值不一样,使用数字可以通用。
参数2:苹果保留关键字,一般也写0
*/
全局队列和并发队列的区别:
1,全局队列没有名字,但是并发队列有名字。有名字可以便于查看系统日志
2,全局队列是所有应用程序共享的。
3,在mrc的时候,全局队列不用手动释放,但是并发队列需要。
三、Category 的实现原理,以及 Category 为什么只能加方法不能加属性。
基本原理:Category 的实例方法、属性都会被整合到主类中去;类方法会被整合到元类中去;协议会在主类中和元类中个整合一份;
实现方法:更新类的数据字段 data() 中 method_lists(或 method_list)、properties 和 properties 的值。
实现原理:就是将类中的旧有方法和 Category 中新添加的方法整合成一个新的方法列表,并赋值给 method_lists 或 method_list 。通过探究这个处理过程,我们也印证了一个结论,那就是主类中的方法和 Category 中的方法在 runtime 看来并没有区别,它们是被同等对待的,都保存在主类的方法列表中。
总结:其实就是把旧方法和旧属性和旧协议和新属性新方法新协议整合到一起,重新分别更新类的数据段data()的method_lists、properties、properties的值,就实现了为类添加方法和协议;
在类中使用@property,系统会自动生成带“_”的成员变量和该变量的setter和getter方法。也就是说,属性相当于一个成员变量加getter和setter方法;在分类里使用@property声明属性,只是将该属性添加到该类的属性列表,但是没有生成相应的成员变量,也没有实现setter和getter方法。
category 它是在运行期决议的。 因为在运行期即编译完成后,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的。
使用Runtime技术中的关联对象可以为类别添加属性。
扩展:无论我们有没有主动引入 Category 的头文件,Category 中的方法都会被添加进主类中。我们可以通过 - performSelector: 等方式对 Category 中的相应方法进行调用,之所以需要在调用的地方引入 Category 的头文件,只是为了“照顾”编译器。不同的Category如果有相同的方法名会全部执行 ,执行顺序按image里文件顺序,分类里面的方法会覆盖本类的方法,要想分类和元类都执行的话其实也是有办法的,其实要做到分类方法不覆盖原方法需要用的 OC 的 Runtime ,来测试一下
在 AppDelegate.m 中实现 applicationDidBecomeActive:
- (void)applicationDidBecomeActive:(UIApplication *)application {
NSLog(@"applicationDidBecomeActive");
}
再创建一个 AppDelegate 的分类,实现applicationDidBecomeActive:
- (void)applicationDidBecomeActive:(UIApplication *)application {
NSLog(@"applicationDidBecomeActive--分类");
Class currentClass = [AppDelegate class];
if (currentClass) {
unsigned int methodCount;
Method *methodList = class_copyMethodList(currentClass, &methodCount);
IMP lastImp = NULL;
SEL lastSel = NULL;
for (NSInteger i = 0; i < methodCount; i++) {
Method method = methodList[i];
NSString *methodName = [NSString stringWithCString:sel_getName(method_getName(method))
encoding:NSUTF8StringEncoding];
if ([@"applicationDidBecomeActive:" isEqualToString:methodName]) {
lastImp = method_getImplementation(method);
lastSel = method_getName(method);
}
}
typedef void (*fn)(id,SEL,id);
if (lastImp != NULL) {
fn f = (fn)lastImp;
f(self,lastSel,application);
}
free(methodList);
}
}
打印结果
2017-06-15 21:55:45.709 demo[807:25011] applicationDidBecomeActive--分类
2017-06-15 21:55:45.709 demo[807:25011] applicationDidBecomeActive
这样,就可以同时调用分类和原类的方法了,可能一些SDK是这样的思路。
实际用途
给现有的类添加方法;
将一个类的实现拆分成多个独立的源文件;
声明私有的方法。
extension看起来很像一个匿名的category,但是extension和有名字的category几乎完全是两个东西。 extension在编译期决议,它就是类的一部分,在编译期和头文件里的@interface以及实现文件里的@implement一起形成一个完整的类,它伴随类的产生而产生,亦随之一起消亡。extension一般用来隐藏类的私有信息,你必须有一个类的源码才能为一个类添加extension,所以你无法为系统的类比如NSString添加extension。
四、Category 中有 load 方法吗?load 方法是什么时候调用的?load 方法能继承吗
有load方法,编译期调用
load 方法不会被类自动继承, 每一个类中的 load 方法都不需要像 viewDidLoad 方法一样调用父类的方法。调用的顺序是父类优先于子类, 子类优先于分类。
如果分类和其所属的类都定义了load方法,则先调用类里的,再调用分类里的。
执行load方法时,运行期系统处于“脆弱状态”。在执行子类的load方法之前,必定会先执行所有超类的load方法,而如果代码还依赖了其他程序库,那么程序库里相关类的load方法也必定会先执行。
扩展
在整个应用程序执行load方法时都会阻塞
initialize方法
它是“惰性”调用的,也就是说,只有当程序用到了相关的类时,才会调用。因此,如果某个类一直都没有使用,那么其initialize方法就一直不会运行。这也就等于说,应用程序无须先把每个类的initialize都执行一遍。
http://blog.csdn.net/jasonjwl/article/details/53091435
五、block 的原理,block 的属性修饰词为什么用 copy,使用 block 时有哪些要注意的?
block:可以理解为一段带有自动变量的匿名函数,就是预先准备好一段代码,在使用的时候调用,Block也是Objective-C中的对象;
匿名函数:没有函数名的函数,一对{}包裹的内容是匿名函数的作用域。
自动变量:栈上声明的一个变量不是静态变量和全局变量,是不可以在这个栈内声明的匿名函数中使用的,但在Block中却可以。
虽然使用Block不用声明类,但是Block提供了类似Objective-C的类一样可以通过成员变量来保存作用域外变量值的方法,那些在Block的一对{}里使用到但却是在{}作用域以外声明的变量,就是Block截获的自动变量。
__block发挥作用的原理:将栈上用__block修饰的自动变量封装成一个结构体,让其在堆上创建,以方便从栈上或堆上访问和修改同一份数据。
copy的使用原理:配置在栈上的Block,如果其所属的栈作用域结束,该Block就会被废弃,对于超出Block作用域仍需使用Block的情况,Block提供了将Block从栈上复制到堆上的方法来解决这种问题,即便Block栈作用域已结束,但被拷贝到堆上的Block还可以继续存在。
复制到堆上的Block:在ARC有效时,大多数情况下编译器会进行判断,自动生成将Block从栈上复制到堆上的代码,以下几种情况栈上的Block会自动复制到堆上:
调用Block的copy方法
将Block作为函数返回值时
将Block赋值给__strong(默认修饰符)修改的变量时
向Cocoa框架含有usingBlock的方法或者GCD的API传递Block参数时
因为上面四条规则,在ARC下其实很少见NSConcreteStackBlock类的Block,大多数情况编译器都保证了Block是在堆上创建的,如下代码所示,仅最后一行代码直接使用一个不赋值给变量的Block,它的类才是NSStackBlock。
使用的时候防止循环引用,有一下几种方法:
方法一:对Block内要使用的对象A使用__weak进行修饰,Block对对象A弱引用打破循环。
有三种常用形式:
使用__weak ClassName
__block XXViewController* weakSelf = self;
self.blk = ^{
NSLog(@"In Block : %@",weakSelf);
};
使用__weak typeof(self)
__weak typeof(self) weakSelf = self;
self.blk = ^{
NSLog(@"In Block : %@",weakSelf);
};
Reactive Cocoa中的@weakify和@strongify
@weakify(self);
self.blk = ^{
@strongify(self);
NSLog(@"In Block : %@",self);
};
方法二:对Block内要使用的对象A使用__block进行修饰,并在代码块内,使用完__block变量后将其设为nil,并且该block必须至少执行一次。
__block XXController *blkSelf = self;
self.blk = ^{
NSLog(@"In Block : %@",blkSelf);
};
注意上述代码仍存在内存泄露,因为:
XXController对象持有Block对象blk
blk对象持有__block变量blkSelf
__block变量blkSelf持有XXController对象
__block XXController *blkSelf = self;
self.blk = ^{
NSLog(@"In Block : %@",blkSelf);
blkSelf = nil;//不能省略
};
self.blk();//该block必须执行一次,否则还是内存泄露
在block代码块内,使用完使用完__block变量后将其设为nil,并且该block必须至少执行一次后,不存在内存泄露,因为此时:
XXController对象持有Block对象blk
blk对象持有__block变量blkSelf(类型为编译器创建的结构体)
__block变量blkSelf在执行blk()之后被设置为nil(__block变量结构体的__forwarding指针指向了nil),不再持有XXController对象,打破循环
第二种使用__block打破循环的方法,优点是:
可通过__block变量动态控制持有XXController对象的时间,运行时决定是否将nil或其他变量赋值给__block变量
不能使用__weak的系统中,使用__unsafe_unretained来替代__weak打破循环可能有野指针问题,使用__block则可避免该问题
其缺点也明显:
必须手动保证__block变量最后设置为nil
block必须执行一次,否则__block不为nil循环应用仍存在
因此,还是避免使用第二种不常用方式,直接使用__weak打破Block循环引用。
方法三:将在Block内要使用到的对象(一般为self对象),以Block参数的形式传入,Block就不会捕获该对象,而将其作为参数使用,其生命周期系统的栈自动管理,不造成内存泄露。
即原来使用__weak的写法:
__weak typeof(self) weakSelf = self;
self.blk = ^{
__strong typeof(self) strongSelf = weakSelf;
NSLog(@"Use Property:%@", strongSelf.name);
//……
};
self.blk();
改为Block传参写法后:
self.blk = ^(UIViewController *vc) {
NSLog(@"Use Property:%@", vc.name);
};
self.blk(self);
优点:简化了两行代码,更优雅
更明确的API设计:告诉API使用者,该方法的Block直接使用传进来的参数对象,不会造成循环引用,不用调用者再使用weak避免循环
扩展:
https://www.jianshu.com/p/d28a5633b963
Block的声明:
返回值类型(^block变量名)(参数)
作为方法的参数:
(返回值类型 (^)(参数1,参数2))block名称
Block的定义:
返回值类型(^blcok变量名)(参数类型)=^(参数 ){
需要执行的操作
}
另外 __weak 是有代价的,需要检查对象是否已经消亡,而为了知道是否已经消亡,自然也需要一些信息去跟踪对象的使用情况。__unsafe_unretained 比 __weak 快。当明确知道对象的生命期时,选择 __unsafe_unretained 会有一些性能提升;
疑问:为什么所有的文章里说block是分配在栈里的?
五、iOS 的热更新方案有哪些?介绍一下实现原理。
六、class A 继承 class B,class B 继承 NSObject。画出完整的类图
每个Objective-C对象都有一个隐藏的数据结构,这个数据结构是Objective-C对象的第一个成员变量,它就是isa指针。这个指针指向哪 呢?它指向一个类对象(class object 记住它是个对象,是占用内存空间的一个变量,这个对象在编译的时候编译器就生成了,专门来描述某个类的定义),这个类对象包含了Objective-C 对象的一些信息(为了区分两个对象,我把前面提到的对象叫Objective-C对象),包括Objective-C对象的方法调度表,实现了什么协议等 等。这个包含信息就是Objective-C动态能力的根源了。那我们看看isa指针类型的数据结构是什么样的?
如果抛开NSObject对象的其他的成员数据和变量,NSObject可以看成这样:
@interface NSObject <NSObject> {
Class isa;
}
不考虑@interface关键字在编译时的作用,可以把NSObject更接近C语言结构表示为:
struct NSObject{
Class isa;
}
Class是用typedef 定义的: typedef struct objc_class *Class; ,那NSObject可以这么写了
struct NSObject{
objc_class *isa
}
那objc_class的结构大概是这样的:
struct objc_class {
Class isa;
Class super_class;
const char *name;
long version;
long info;
long instance_size;
struct objc_ivar_list *ivars;
struct objc_method_list **methodLists;
struct objc_cache *cache;
struct objc_protocol_list *protocols;
}
在这个定义中,下面几个字段是我们感兴趣的
isa:需要注意的是在Objective-C中,所有的类自身也是一个对象,这个对象的Class里面也有一个isa指针,它指向metaClass(元类),我们会在后面介绍它。
super_class:指向该类的父类,如果该类已经是最顶层的根类(如NSObject或NSProxy),则super_class为NULL。
cache:用于缓存最近使用的方法。一个接收者对象接收到一个消息时,它会根据isa指针去查找能够响应这个消息的对象。在实际使用中,这个对象只有一部分方法是常用的,很多方法其实很少用或者根本用不上。这种情况下,如果每次消息来时,我们都是methodLists中遍历一遍,性能势必很差。这时,cache就派上用场了。在我们每次调用过一个方法后,这个方法就会被缓存到cache列表中,下次调用的时候runtime就会优先去cache中查找,如果cache没有,才去methodLists中查找方法。这样,对于那些经常用到的方法的调用,但提高了调用的效率。
version:我们可以使用这个字段来提供类的版本信息。这对于对象的序列化非常有用,它可是让我们识别出不同类定义版本中实例变量布局的改变。
cash的过程如下:
NSArray *array = [[NSArray alloc] init];
其流程是:
-
[NSArray alloc]
先被执行。因为NSArray没有+alloc
方法,于是去父类NSObject去查找。 - 检测NSObject是否响应
+alloc
方法,发现响应,于是检测NSArray类,并根据其所需的内存空间大小开始分配内存空间,然后把isa
指针指向NSArray类。同时,+alloc
也被加进cache列表里面。 - 接着,执行
-init
方法,如果NSArray响应该方法,则直接将其加入cache
;如果不响应,则去父类查找。 - 在后期的操作中,如果再以
[[NSArray alloc] init]
这种方式来创建数组,则会直接从cache中取出相应的方法,直接调用。
objc_object与id
objc_object
是表示一个类的实例的结构体,它的定义如下(objc/objc.h
):
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
typedef struct objc_object *id;
这里会看到, 在这个结构体里还有一个isa指针,又是一重指向,是不是有种到了盗梦空间的感觉。不用紧张,take easy,不会有那么多层次的,这里的isa指针指向的是元类对象(metaclass object),带有元字,证明快到头了。那元对象有啥用呢?它用来存储的关于类的版本,名字,类方法等信息。所有的元类对象(metaclass object)都指向 NSObject的元类对象,到头还是NSObject。一共三次:类对象->元类对象->NSObject元类对象。
D3继承D2,D2继承D1,D1最终继承NSObject。下图从D3的一个对象开始,排列出D3 D2 D1 NSObject 类对象,元类对象等关系
八、Cocoa是什么?
Cocoa是OS X和 iOS操作系统的程序的运行环境;
是什么因素使一个程序成为Cocoa程序呢?不是编程语言,因为在Cocoa开发中你可以使用各种语言;也不是开发工具,你可以在命令行上就可以创建Cocoa程序。Cocoa程序可以这么说,它是由一些对象组成,而这些对象的类最后都是继承于它们的根类 :NSObject。而且它们都是基于Objective-C运行环境的;
Cocoa众多框架中最重要最基本的两个框架是:Foundation 和 UIKit。
Foundation 和界面无关,也可以说和界面无关的类基本是Foundation框架的,和界面相关的是UIKit框架。
除此之外还有CoreFoundation框架,和Foundation框架无缝连接,前者的C语言数据结构平滑的转接到Objective - C对象,也可以反向转换。除此之外还有别的系统库比如,CFNetworing、CoreAudio、AVFoundation、CoreData、CoreText;
七、细致地讲一下事件传递流程
事件的传递与响应:
1、当一个事件发生后,事件会从父控件传给子控件,也就是说由UIApplication -> UIWindow -> UIView -> initial view,以上就是事件的传递,也就是寻找最合适的view的过程。
2、接下来是事件的响应。首先看initial view能否处理这个事件,如果不能则会将事件传递给其上级视图(inital view的superView);如果上级视图仍然无法处理则会继续往上传递;一直传递到视图控制器view controller,首先判断视图控制器的根视图view是否能处理此事件;如果不能则接着判断该视图控制器能否处理此事件,如果还是不能则继续向上传递;(对于第二个图视图控制器本身还在另一个视图控制器中,则继续交给父视图控制器的根视图,如果根视图不能处理则交给父视图控制器处理);一直到 window,如果window还是不能处理此事件则继续交给application处理,如果最后application还是不能处理此事件则将其丢弃
3、在事件的响应中,如果某个控件实现了touches...方法,则这个事件将由该控件来接受,如果调用了[super touches….];就会将事件顺着响应者链条往上传递,传递给上一个响应者;接着就会调用上一个响应者的touches….方法
如何做到一个事件多个对象处理:
因为系统默认做法是把事件上抛给父控件,所以可以通过重写自己的touches方法和父控件的touches方法来达到一个事件多个对象处理的目的。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
// 1.自己先处理事件...
NSLog(@"do somthing...");
// 2.再调用系统的默认做法,再把事件交给上一个响应者处理
[super touchesBegan:touches withEvent:event];
}
事件的传递和响应的区别:
事件的传递是从上到下(父控件到子控件),事件的响应是从下到上(顺着响应者链条向上传递:子控件到父控件。
事件传递调用的方法是:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
事件响应调用的刚发是:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
默认是抛给父控件的;
八、main()之前的过程有哪些?
1)dyld 开始将程序二进制文件初始化
2)交由ImageLoader 读取 image,其中包含了我们的类,方法等各种符号(Class、Protocol 、Selector、 IMP)
3)由于runtime 向dyld 绑定了回调,当image加载到内存后,dyld会通知runtime进行处理
4)runtime 接手后调用map_images做解析和处理
5)接下来load_images 中调用call_load_methods方法,遍历所有加载进来的Class,按继承层次依次调用Class的+load和其他Category的+load方法
6)至此 所有的信息都被加载到内存中
7)最后dyld调用真正的main函数
注意:dyld会缓存上一次把信息加载内存的缓存,所以第二次比第一次启动快一点
九、讲一下 HTTPS 密钥传输流程
1、客户端发起连接请求
2、服务端配置数字证书(证书有颁发机构颁发或者是自己生成的,其实就是一对公钥和私钥)
3、服务端传送证书包含颁发机构过期时间其实这个就是公钥
4、客户端收到证书,TLS验证证书是否有效,如果有效就生成一个随机值其实就是私钥,接下来就只用这个私钥进行对称加密的,然后证书对这个随机值进行加密然后传给服务端
5、服务端的得到加密的随机数以后进行解密,然后把信息加密传给客户端,客户端收到以后用私钥进行解密
为了安全采用非对称加密,但是每次都进行非对称加密导致耗时太长(交互的时候用对方的公钥加密传给对方以后,对方用私钥解密),所以准备第一次用RSA非对称加密目的是把约定的秘钥传给对方,以后双方都使用这个秘钥对称加密。
CA证书的目的就是在第一次传秘钥的时候防止中间人冒充通信人的公钥,所以加以公正。办法是用CA的私钥和公钥对对方的公钥进行非对称加密。接受方收到证书后用hash算法对对方公钥解密然后和用CA的公钥对数字签名解密的公钥进行对比 ,如没有篡改就可以进行下面的对称加密进行通信了;
十、为什么是三次握手?为什么是四次挥手?三次挥手不行吗
通俗描述3次握手就是
A对B说:我的序号是x,我要向你请求连接;(第一次握手,发送SYN包,然后进入SYN-SEND状态)
B听到之后对A说:我的序号是y,期待你下一句序号是x+1的话(意思就是收到了序号为x的话,即ack=x+1),同意建立连接。(第二次握手,发送ACK-SYN包,然后进入SYN-RCVD状态)
A听到B说同意建立连接之后,对A说:与确认你同意与我连接(ack=y+1,ACK=1,seq=x+1)。(第三次握手,A已进入ESTABLISHED状态)
B听到A的确认之后,也进入ESTABLISHED状态。
描述四次挥手就是:
1.A与B交谈结束之后,A要结束此次会话,对B说:我要关闭连接了(seq=u,FIN=1)。(第一次挥手,A进入FIN-WAIT-1)
2.B收到A的消息后说:确认,你要关闭连接了。(seq=v,ack=u+1,ACK=1)(第二次挥手,B进入CLOSE-WAIT)
3.A收到B的确认后,等了一段时间,因为B可能还有话要对他说。(此时A进入FIN-WAIT-2)
4.B说完了他要说的话(只是可能还有话说)之后,对A说,我要关闭连接了。(seq=w, ack=u+1,FIN=1,ACK=1)(第三次挥手)
5.A收到B要结束连接的消息后说:已收到你要关闭连接的消息。(seq=u+1,ack=w+1,ACK=1)(第四次挥手,然后A进入CLOSED)
6.B收到A的确认后,也进入CLOSED。
十一、讲讲 RunLoop,项目中有用到吗?
线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。
十二、KVO使用案例和原理
监测按钮是否被选择
// kvo 添加观察者
[self addObserver:self forKeyPath:@"isSelected" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
/** 添加观察者必须要实现的方法 */
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context{
if ([keyPath isEqualToString:@"isSelected"]) {
/** 打印新老值 */
if ([[change objectForKey:@"old"] isEqual: [change objectForKey:@"new"]]) {
return;
}
if([[change objectForKey:@"new"] isEqual: @YES]){
[self setImage:[UIImage imageNamed:@"选中"] forState:UIControlStateNormal];
}else{
[self setImage:[UIImage imageNamed:@"未选"] forState:UIControlStateNormal];
}
}
}
1.KVO是基于runtime机制实现的
2.当某个类的属性对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的setter 方法。派生类在被重写的setter方法内实现真正的通知机制
3.如果原类为Person,那么生成的派生类名为NSKVONotifying_Person
4.每个类对象中都有一个isa指针指向当前类,当一个类对象的第一次被观察,那么系统会偷偷将isa指针指向动态生成的派生类,从而在给被监控属性赋值时执行的是派生类的setter方法
5.键值观察通知依赖于NSObject 的两个方法: willChangeValueForKey: 和 didChangevlueForKey:;在一个被观察属性发生改变之前, willChangeValueForKey:一定会被调用,这就 会记录旧的值。而当改变发生后,didChangeValueForKey:会被调用,继而 observeValueForKey:ofObject:change:context: 也会被调用。
KVO深入原理:
1.Apple 使用了 isa 混写(isa-swizzling)来实现 KVO 。当观察对象A时,KVO机制动态创建一个新的名为: NSKVONotifying_A的新类,该类继承自对象A的本类,且KVO为NSKVONotifying_A重写观察属性的setter 方法,setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象属性值的更改情况。
2.NSKVONotifying_A类剖析:在这个过程,被观察对象的 isa 指针从指向原来的A类,被KVO机制修改为指向系统新创建的子类 NSKVONotifying_A类,来实现当前类属性值改变的监听;
3.所以当我们从应用层面上看来,完全没有意识到有新的类出现,这是系统“隐瞒”了对KVO的底层实现过程,让我们误以为还是原来的类。但是此时如果我们创建一个新的名为“NSKVONotifying_A”的类(),就会发现系统运行到注册KVO的那段代码时程序就崩溃,因为系统在注册监听的时候动态创建了名为NSKVONotifying_A的中间类,并指向这个中间类了。
4.(isa 指针的作用:每个对象都有isa 指针,指向该对象的类,它告诉 Runtime 系统这个对象的类是什么。所以对象注册为观察者时,isa指针指向新子类,那么这个被观察的对象就神奇地变成新子类的对象(或实例)了。) 因而在该对象上对 setter 的调用就会调用已重写的 setter,从而激活键值通知机制。
5.子类setter方法剖析:KVO的键值观察通知依赖于 NSObject 的两个方法:willChangeValueForKey:和 didChangevlueForKey:,在存取数值的前后分别调用2个方法: 被观察属性发生改变之前,willChangeValueForKey:被调用,通知系统该keyPath 的属性值即将变更;当改变发生后, didChangeValueForKey: 被调用,通知系统该 keyPath 的属性值已经变更;之后, observeValueForKey:ofObject:change:context: 也会被调用。且重写观察属性的setter 方法这种继承方式的注入是在运行时而不是编译时实现的。
mage.jpeg](http://upload-images.jianshu.io/upload_images/2189030-3d8959ad90576e6c.jpeg?imageMogr2/auto-orient/strip%7CimageView2/2/w/400)
十三、通知不移除观察者会出现什么情况为什么?
因为观察者注册时,通知中心并不会对观察者对象做 retain 操作,而是对观察者对象进行unsafe_unretained (不安全引用,Cocoa 和 Cocoa Touch 中的一些类还没有支持弱引用,所以采用不安全的引用只是为了兼容旧的版本)引用。类似于weak不同之处是当对象被释放的时候不安全引用并不会被置为nil,所以可能产生野指针。
iOS 9 之后对观察者对象开始弱引用了,所以不会出现野指针;
但是,通过-[NSNotificationCenter addObserverForName:object:queue:usingBlock]方法注册的观察者依然需要手动的释放,因为通知中心对它们持有的是强引用。
十四、delegate 修饰符用strong 修饰为什么会循环引用?
因为互相强引用了,导致不能释放
eg:在B类中引入A的delegate,B就会持有A的对象,A的delegate又会强引用B,就造成了循环引用。
@property (nonatomic, strong) A *a; B类中
self.a.delegate=self又对B强引用,使B的retainCount + 1
@property (nonatomic, weak) id<ADelegate>delegate;
A类中造成循环引用,当B类对象被释放的时候,delegate仍然持有B对象,导致B和A都不能被释放;
用assign避免了这个问题,
self.a.delegate=self 在这一步中因为assign是单纯的拷贝所赋值变量的值,即进行简单的赋值操作,delegate不持有B,只是保留了B对象的指针的值,不会使引用计数加1。但是如果当B被销毁的时候,引用计数变成0但并不意味着被销毁,delegate不会跟着销毁,仍保存着之前对象的值,就成了野指针,造成内存泄露。当再向delegate发消息的时候,就会crash。
用week解决了这个问题,当B对象被销毁的时候,week修饰的delegate也会自动置为nil(销毁),这时再向nil发送消息,不会crash,OC可以向nil对象发送消息;
十五、为什么xib控件属性修饰符用weak
因为xib已经对控件默认强引用了,即父视图已经对控件强引用了,保证控件不会提前释放,所以使用者只需对xib所在的view强引用就行了;
十六、类的内省和比较
NSObjec有很多方法可以查询对象的运行时信息。这些内省方法有助于找出对象在类层次中的位置,确定对象是否实现特定的方法,以及测试对象是否遵循某种协议。下面是部分方法
superclass和class方法(实现为类和实例方法)分别以Class对象的形式返回接收者的父类和类。
您可以通过isKindOfClass:和isMemberOfClass:方法来确定对象属于哪个类。后者用于测试接收者是否为指定类的实例。isSubclassOfClass:类方法则用于测试类的继承性。
respondsToSelector:方法用于测试接收者是否实现由选择器参数标识的方法。instancesRespondToSelector:类方法则用于测试给定类的实例是否实现指定的方法。
conformsToProtocol:方法用于测试接收者(对象或类)是否遵循给定的协议。
isEqual:和hash方法用于对象的比较。
description方法允许对象返回一个内容描述字符串;这个方法的输出经常用于调试(“print object”命令),以及在格式化字符串中和“%@”指示符一起表示对象。
十七、讲一下oc的消息机制
十八、Swift是如何实现多态的
十八、Swift 中 struct 和 class 的区别
值类型和引用类型,栈上堆上
十九、讲一下你对 iOS 内存管理的理解
二十、你在项目中是怎么优化内存的?
二十一、列表卡顿的原因可能有哪些?你平时是怎么优化的?
二十二、项目有没有做过组件化?或者你是否调研过?
组件化核心技术是一套路由方案实现完全解耦,这样就可以根据自己的业务功能模块化并行开发。把不同的模块传到私有仓库里用cocopods
管理,这样就可以快速组装一个新的App了,多见于全家桶中使用。主要还是根据自己业务划分不同的组件,组件粒度大小的掌握是难点。组件之间的解耦和依赖是难点。路由方案可以自己实现也可以借鉴蘑菇街
和casetwy
的路由方案。casetwy
是Category
和Target_Action
的方式实现组件之间的解耦和组件之间的通信的。CTMediator
内部用runtime实现参数的传递,Target
实现事件的分发。CTMediator+ACategory
实现功能组件的解耦。
蘑菇街是以Url
+Pro
的方式(还没看完待续)。
二十三、ARC 都帮我们做了什么?
二十四、实现 isEqual 和 hash 方法时要注意什么?
二十五、property 的常用修饰词有哪些?weak 和 assign 的区别?weak 的实现原理是什么?
二十六、线程安全的处理手段有哪些?把你想到的都说一下
二十七、Swift 和 OC,各自的优缺点有哪些?
https://www.jianshu.com/p/433881d89c4a
二十八、如果让你实现 NSNotificationCenter,讲一下思路
二十九、如果让你实现 GCD 的线程池,讲一下思路
三十、讲讲 MVC、MVVM、MVP,以及你在项目里具体是怎么写的?
三十一、iOS 系统框架里使用了哪些设计模式?至少说6个。
三十二、你自己用过哪些设计模式?
三十三、哪一个项目技术点最能体现自己的技术实力?具体讲一下。
三十四、你在项目中遇到的最大的问题是什么?你是怎么解决的?
三十五、用 Alamofire 比直接使用 URLSession,优势是什么?
三十六、手写一下快排
三十七、遍历一个树,要求不能用递归
三十八、找出两个字符串的最大公共子字符串
三十九、weak的内部原理
四十、沙盒机制
沙盒也叫沙箱,英文standbox,其原理是通过重定向技术,把程序生成和修改的文件定向到自身文件夹中。在沙盒机制下,每个程序之间的文件夹不能互相访问。iOS系统为了保证系统安全,采用了这种机制
沙盒的的根目录有三个文件夹,分别是 Documents,Library,tmp
Documents中一般保存应用程序本身产生文件数据,例如游戏进度,绘图软件的绘图等, iTunes备份和恢复的时候,会包括此目录
注意:在此目录下不要保存从网络上下载的文件,否则app无法上架!
Library/Caches/
此目录用来保存应用程序运行时生成的需要持久化的数据,这些数据一般存储体积比较大,又不是十分重要,比如网络请求数据等。这些数据需要用户负责删除。iTunes同步设备时不会备份该目录。
Library/Preferences/
此目录保存应用程序的所有偏好设置,iOS的Settings(设置)应用会在该目录中查找应用的设置信息。iTunes同步设备时会备份该目录
在Preferences/下不能直接创建偏好设置文件,而是应该使用NSUserDefaults类来取得和设置应用程序的偏好.
tmp/
此目录保存应用程序运行时所需的临时数据,使用完毕后再将相应的文件从该目录删除。应用没有运行时,系统也可能会清除该目录下的文件。iTunes同步设备时不会备份该目录。
四十一、YYCaches实现思路
四十二、iOS中的 NSURLProtocol
四十三、消息转发防崩溃的三个方法实现原理
四十四、DNS防劫持
四十五、堆和栈的区别
https://www.jianshu.com/p/8588981a74de
四十六、动画
http://www.cocoachina.com/ios/20160712/17010.html
四十七、uiview和calayer的区别
1.首先UIView可以响应事件,Layer不可以.
2.View和CALayer的Frame映射及View如何创建CALayer.
一个 Layer 的 frame 是由它的 anchorPoint,position,bounds,和 transform 共同决定的,而一个 View 的 frame 只是简单的返回 Layer的 frame,同样 View 的 center和 bounds 也是返回 Layer 的一些属性。View中frame getter方法,bounds和center,UIView并没有做什么工作;它只是简单的各自调用它底层的CALayer的frame,bounds和position方法。
3.UIView主要是对显示内容的管理而 CALayer 主要侧重显示内容的绘制。
可以看到 UIView 是 CALayer 的CALayerDelegate,我猜测是在代理方法内部[UIView(CALayerDelegate) drawLayer:inContext]调用 UIView 的 DrawRect方法,从而绘制出了 UIView 的内容
4.在做 iOS 动画的时候,修改非 RootLayer的属性(譬如位置、背景色等)会默认产生隐式动画,而修改UIView则不会。
对于每一个 UIView 都有一个 layer,把这个 layer 且称作RootLayer,而不是 View 的根 Layer的叫做 非 RootLayer。我们对UIView的属性修改时时不会产生默认动画,而对单独 layer属性直接修改会,这个默认动画的时间缺省值是0.25s
UIView 默认情况下禁止了 layer 动画,但是在 animation block 中又重新启用了它们
是因为任何可动画的 layer 属性改变时,layer 都会寻找并运行合适的 'action' 来实行这个改变。在 Core Animation 的专业术语中就把这样的动画统称为动作 (action,或者 CAAction)。
layer 通过向它的 delegate 发送 actionForLayer:forKey: 消息来询问提供一个对应属性变化的 action。delegate 可以通过返回以下三者之一来进行响应:
四十八、calayer渲染图片
// 绘制图片显示方法1
// 图层形变
// 旋转(angle转换弧度:弧度=角度*M_PI/180;x上下对换、y左右对换、z先上下对换再左右对换;-1.0~1.0)
// layerContant.transform = CATransform3DMakeRotation(M_PI, 0.0, 0.0, 0.0);
// 缩放(0.0~1.0)
// layerContant.transform = CATransform3DMakeScale(0.8, 0.8, 0.8);
// 移动
// layerContant.transform = CATransform3DMakeTranslation(10.0, 1.0, 1.0);
// 显示内容
[layerContant setContents:[UIImage imageNamed:@"header"].CGImage];
绘制图片显示方法2
layerContant.delegate = self;
[layerContant setNeedsDisplay];
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
{
// 绘图
CGContextSaveGState(ctx);
// 图形上下文形变,避免图片倒立显示
CGContextScaleCTM(ctx, 1.0, -1.0);
CGContextTranslateCTM(ctx, 0.0, -150.0);
// 图片
UIImage *image = [UIImage imageNamed:@"header"];
CGContextDrawImage(ctx, CGRectMake(0.0, 0.0, 150.0, 150.0), image.CGImage);
CGContextRestoreGState(cox);
}
// 绘制实线、虚线
-
(void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
{
// 绘实线
// 线条宽
CGContextSetLineWidth(ctx, 1.0);
// 线条颜色
// CGContextSetRGBStrokeColor(ctx, 1.0, 0.0, 0.0, 1.0);
CGContextSetStrokeColorWithColor(ctx, [UIColor greenColor].CGColor);
// 方法1
// 坐标点数组
CGPoint aPoints[2];
aPoints[0] = CGPointMake(10.0, 50.0);
aPoints[1] = CGPointMake(140.0, 50.0);
// 添加线 points[]坐标数组,和count大小
CGContextAddLines(ctx, aPoints, 2);
// 根据坐标绘制路径
CGContextDrawPath(ctx, kCGPathStroke);
// 方法2
CGContextSetLineWidth(ctx, 5.0);
CGContextSetStrokeColorWithColor(ctx, [UIColor purpleColor].CGColor);
CGContextMoveToPoint(ctx, 10.0, 60.0); // 起点坐标
CGContextAddLineToPoint(ctx, 140.0, 60.0); // 终点坐标
CGContextStrokePath(ctx); // 绘制路径// 绘虚线
// 线条宽
CGContextSetLineWidth(ctx, 2.0);
// 线条颜色
CGContextSetStrokeColorWithColor(ctx, [UIColor blueColor].CGColor);
// 虚线
CGFloat dashArray[] = {1, 1, 1, 1};
CGContextSetLineDash(ctx, 1, dashArray, 1);
// 起点
CGContextMoveToPoint(ctx, 10.0, 100.0);
// 终点
CGContextAddLineToPoint(ctx, 140.0, 100.0);
// 绘制路径
CGContextStrokePath(ctx);
}
// 内存管理,避免异常crash
- (void)dealloc
{
for (CALayer *layer in self.view.layer.sublayers)
{
if ([layer.delegate isEqual:self])
{
layer.delegate = nil;
}
}
NSLog(@"%@ 被释放了~", self);
}
四十九、离平渲染
https://blog.csdn.net/qq_29846663/article/details/68960512
CAShapeLayer继承于CALayer,可以使用CALayer的所有属性值;
CAShapeLayer需要贝塞尔曲线配合使用才有意义(也就是说才有效果)
使用CAShapeLayer(属于CoreAnimation)与贝塞尔曲线可以实现不在view的drawRect(继承于CoreGraphics走的是CPU,消耗的性能较大)方法中画出一些想要的图形
CAShapeLayer动画渲染直接提交到手机的GPU当中,相较于view的drawRect方法使用CPU渲染而言,其效率极高,能大大优化内存使用情况。
总的来说就是用CAShapeLayer的内存消耗少,渲染速度快