文章目录
1:strong weak copy assign retain区别
- strong和copy的区别
#import <Foundation/Foundation.h>
@interface Model : NSObject
@property(strong,nonatomic) NSString * str1;
@property(copy,nonatomic) NSString * str2;
@end
@implementation Model
@end
int main() {
Model *model = [[Model alloc] init];
NSMutableString *str = [[NSMutableString alloc] initWithString:@"123"];
model.str1 = str;
model.str2 = str;
[str appendString:@"123"];
NSLog(@"%@",model.str1);
NSLog(@"%@",model.str2);
return 0;
}
输出
声明为copy的对象被复制了
- assgin和weak的区别:
通常来说,assgin用于声明基础类型,weak用于声明对象,两者都不会使引用计数器+1,但是用于声明代理的时候,两者是有区别的:
一旦指向的对象被销毁
weak会将delegate置为nil
assign还指向被释放的对象,这样就变成了野指针
- retain和strong的区别:
一般来说,两者没有区别,都是让引用计数器+1,但是用于声明block的时候,两者是有区别的,据说strong在声明block的时候,等同于copy,没有验证,但还是使用copy吧。
2:block的使用
block的本质是一个匿名函数,iOS引入block,使iOS拥有面向函数编程的能力。在面向函数编程模型中,函数就像普通变量一样,可以作为参数、成员变量、返回值,也可以像js那样实现链式编程。
//这样就定义了一个输入值为int类型,返回值也为int类型的block
int (^block)(int val);
//将block定义为一种变量名
typedf int (^Block)(int val);
@interface Model:NSObject
//这两种定义block为成员变量的方法都可以
@property(copy,nonatomic) Block b;
@property(copy,nonatomic) void(^block)(void);
//也可以将block作为函数的参数和返回值
-(void(^)(int val))method:(int(^)(void))block;
@end
@implementation Model
- (instancetype)init{
if (self = [super init]) {
//block作为局部变量
int (^localBlock)(int val) = ^int(int val){
return val;
}
localBlock(1);
}
return self;
}
-(void(^)(int val))method:(int(^)(void))block{
int val = block();
return ^(int val){
NSlog(@"这是返回的block");
}
}
@end
3:block对变量进行截取
函数中的局部变量,会存入栈中,当block作为局部变量时,可以对栈中的变量进行 截取,也就是将栈中的局部变量复制一份。
但是,block不能修改截取的局部变量,因为捕获的是const类型。
但是可以修改全局变量和局部变量。
int main() {
int a = 2;
void(^block)(void) = ^{
NSLog(@"%d",a);
};
a = 10;
block();
return 0;
}
这里输出的还是2,因为block对变量进行了复制。
当然,如果是对象就不一样了,block复制了指向对象的指针,但如果存于堆中的对象发生了改变,block的访问还是会变的。
int main() {
NSMutableString * str = [NSMutableString stringWithFormat:@"123"];
void(^block)(void) = ^{
NSLog(@"%@",str);
};
[str appendString:@"456"];
block();
return 0;
}
block由于复制了指向对象的指针,按理来说,是会让对象的引用计数器+1,所以这个时候就需要用 __block , 直接使用原指针,避免对象引用计数器的增加。
__block 的意思是不复制,直接使用截取的变量,因此使用 __block修饰的变量,可以在block内进行更改。
@interface MyClass : NSObject {
NSObject* _instanceObj;
}
@end
@implementation MyClass
NSObject* __globalObj = nil;
- (id) init {
if (self = [super init]) {
_instanceObj = [[NSObject alloc] init];
}
return self;
}
- (void) test {
static NSObject* __staticObj = nil;
__globalObj = [[NSObject alloc] init];
__staticObj = [[NSObject alloc] init];
NSObject* localObj = [[NSObject alloc] init];
__block NSObject* blockObj = [[NSObject alloc] init];
typedef void (^MyBlock)(void) ;
MyBlock aBlock = ^{
NSLog(@"%@", __globalObj);
NSLog(@"%@", __staticObj);
NSLog(@"%@", _instanceObj);
NSLog(@"%@", localObj);
NSLog(@"%@", blockObj);
};
aBlock = [[aBlock copy] autorelease];
aBlock();
NSLog(@"%d", [__globalObj retainCount]);
NSLog(@"%d", [__staticObj retainCount]);
NSLog(@"%d", [_instanceObj retainCount]);
NSLog(@"%d", [localObj retainCount]);
NSLog(@"%d", [blockObj retainCount]);
}
@end
int main(int argc, charchar *argv[]) {
@autoreleasepool {
MyClass* obj = [[[MyClass alloc] init] autorelease];
[obj test];
return 0;
}
}
执行结果为1 1 1 2 1。
- static 和 global类型的变量由于在静态区,和进程的生命周期一致,所以引用计数不会增加。
- 成员变量,self的引用计数+1,而变量本身不会增加。
- 局部变量,引用计数器+1,为2
- __block类型的变量,block不会复制__block类型的变量,而是使用了和外面同一个,自然也不会使引用计数器+1.
4:关于block的循环引用
这样就引起了循环引用,self持有block,block持有self,谁都释放不掉。
self.block = ^{
NSLog(@"%lu",self.val);
};
这样解决了循环引用,block中使用self不会使引用计数器+1,但是不够完美,万一self被释放掉了,block中的用到weakSelf的代码又没有执行到,就会有异常。
__weak ViewController * weakSelf = self;
self.block = ^{
NSLog(@"%lu",weakSelf.val);
};
完美的写法
block中声明的strongSelf和原先的self不同,当block中的代码执行完毕,strongSelf会被弹出栈中,不会引起循环引用。如果block中的代码还没有执行,self也不会被提前释放。
__weak ViewController * weakSelf = self;
self.block = ^{
__strong typeof(self) strongSelf = weakSelf;
NSLog(@"%lu",strongSelf.val);
};
5:GCD
1:使用group同步进程
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_queue_create("group.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_async(group, queue, ^{
[NSThread sleepForTimeInterval:1];
NSLog(@"任务1完成");
});
dispatch_group_async(group, queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"任务2完成");
});
dispatch_group_notify(group, queue, ^{
NSLog(@"请求完成");
});
输出
网上有教程在queue中发起网络异步请求,发现不能够成功同步,这是因为NSUrlSession在发起网络请求的时候,又另外开启了一个异步线程,所以queue中,不会等待session的网络请求完成
然后,坑爹的是!好像现在iOS不支持同步网络请求了,所有请求都是异步的,只能使用信号量了。
2:使用信号量和PV操作同步网络请求
关于信号量的基础,可以看看北大的操作系统视频(陈向群老师讲的),讲的很棒。
信号量主要有如下几个操作
//创建一个信号量,参数为信号量的值,互斥情况,一般为1
dispatch_semaphore_t mutex = dispatch_semaphore_create(1);
//p操作(信号量减1),第一个参数是信号量,第二个参数是等待时间
dispatch_semaphore_wait(mutex, 1000);
//v操作(信号量加1),参数为信号量
dispatch_semaphore_signal(mutex);
解决多异步请求同步的问题:
__block int n = 0;
dispatch_semaphore_t mutex = dispatch_semaphore_create(1);
NSURLSessionTask *task1 = [[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:@"https://www.baidu.com"] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSLog(@"任务1完成");
dispatch_semaphore_wait(mutex, 1000);
n++;
dispatch_semaphore_signal(mutex);
}];
[task1 resume];
NSURLSessionTask * task2 = [[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:@"https://www.baidu.com"] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSLog(@"任务2完成");
dispatch_semaphore_wait(mutex, 1000);
n++;
dispatch_semaphore_signal(mutex);
}];
[task2 resume];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (TRUE) {
if (n == 2) {
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"任务完成,刷新界面");
});
break;
}
}
});
但是总觉得使用while循环等待有点浪费资源。。。不知道有没有更好的办法。
6:iOS 内存管理基础
虽然现在都是ARC机制,但是面试还是喜欢问MRC相关的东西,引用计数就不说了,默认都会。这里主要介绍 autorelesepool
autorelesepool代码块结束后,会对其内部的对象统一release一次,从而达到延迟释放的目的。
比如,但循环生产大内存对象时,如果不适
…未完
7:iOS单例模式
懒汉式(第一次使用的时候创建)
@implementation Single
static id instance;
+ (instancetype)sharedInstance{
dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone{
dispatch_once_t token;
dispatch_once(&token, ^{
instance = [super allocWithZone:zone];
});
return instance;
}
- (id)copyWithZone:(NSZone *)zone{
return [Single sharedInstance];
}
- (id)mutableCopyWithZone:(nullable NSZone *)zone{
return [Single sharedInstance];
}
@end
饿汉式(类初始化的时候创建,由于这个时候只有一个线程,所以不需要考虑临界资源访问的问题)
#import "Single.h"
@implementation Single
static id instance;
+ (void)load{
instance = [[Single alloc] init];
}
+ (instancetype)sharedInstance{
return instance;
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone{
if (instance == nil) {
instance = [super allocWithZone:zone];
}
return instance;
}
- (id)copyWithZone:(NSZone *)zone{
return instance;
}
- (id)mutableCopyWithZone:(nullable NSZone *)zone{
return instance;
}
@end
这里注意instancetype和id的区别
- id 可以作为参数类型和返回值类型,但 instancetype 只能作为返回值类型
- instancetype 作为返回值时,编译器会自动检测返回值类型,因此做返回值的时候,一律使用 instancetype
8:iOS copy和mutableCopy
网络上,关于这两个的区别,很多都是错的,比如copy是浅拷贝,mutableCopy是深拷贝。。。扯淡。
iOS中,对象要具备拷贝能力,需要继承两个协议,NSCopy 和NSMutableCopy,当我们调用
[object copy];
[object mutableCopy]
的时候,具体结果如何,关键看我们是如何实现这两个协议的 。
对于内置对象来说,以NSString为例子,对NSString调用copy方法
NSString *str = @"123";
NSString *str2 = [str copy];
NSLog(@"%p %p",str,str2);
输出
0x100001030 0x100001030
内存地址是一样的,这说明cocoa的作者在实现NSString的时候,copyWithZone中并没有真的对对象进行了拷贝,因此str2只是复制了str1的地址,因为NSString作为不可变字符串,是不能够修改的,既然不能够修改,为什么要拷贝呢?所以copyWithZone中没有对对象进行拷贝。
对NSString调用mutableCopy就不一样了,由于要复制一个可变对象,自然要进行深拷贝:
NSString *str = @"123";
NSMutableString *str2 = [str mutableCopy];
NSLog(@"%p %p",str,str2);
输出:
0x100001030 0x100582510
str2为新的可变字符串,可以进行字符串的增加
对于作为成员变量声明的对象,如:
@property(copy,nonatomic) NSString *str;
str的set方法中,会调用copy方法,根据上面的分析,当使用的使用NSString对象的时候,copy和strong不会有什么区别,因为并不会真正的拷贝对象。
但是如果将NSMutableString赋值给str就不一样了。
NSMutableString由于是可变数组,copy方法会对对象进行深拷贝,拷贝出来的是不可变对象,如果要拷贝可变对象,就要用mutableCopy1
9:当我们点击一个app图标后,发生了什么
mach-0文件:包括
- 动态库(dylib)
- bundle:info.plist 图片等 [NSBundle mainBundle] 调用
- 可执行文件
- Framework(动态库对应的头文件和资源文件的集合)
main函数调用之前
- 加载dyld(dynamic loader)到内存中,
- dyle加载app启动所需的资源文件(bundle)和动态链接库(dylib)
- Rebase && Bind:当应用程序执行时,对应的进程会被加载入虚拟地址空间,ARSL技术,使得这个进程对应的虚拟地址空间的起始是随机的,为了让应用程序能够找到指令的正确位置,dyld使用
Rebase: 修正内部(指向当前mach-o文件)的指针指向
Bind: 修正外部指针指向 - objective-c/Initializers: oc作为动态语言,在执行之前,会把类的信息注册到一个全局的table中,之前的iOS版本中,接下来,就会对类进行加载,调用 +load 方法,单例的饿汉式和method-swizzing就是在里面实现的。但是目前+load方法已经弃用了,官方建议使用initialize,该方法会在类第一次收到消息前调用
更新的dyld3有所不同
- 分析Mach-o Headers
- 分析依赖的动态库
- 查找需要Rebase & Bind之类的符号
- 把上述结果写入缓存
所以,新的iOS版本中,上面的步骤会加快,可以直接从缓存中读入数据。
main函数调用之后
didFinishLaunchingWithOptions
加载UIWindow
加载rootViewController
有博客算上了 applicationDidBecomeActive,这其实是有问题的,因为这一步在根视图控制器之后。
优化启动时间
main之前:
dylibs(动态库)
系统的动态库由于有缓存,其实加载速度非常快,内嵌的动态库加载速度往往较慢,这个时候可以减少动态库的数量或者将多个动态库合并为一个。
Rebase和bind
- 删除无用的方法和类
- 将功能类似的category合并
initialize
用initialize替代load
main之后
优化这些初始化的核心思想就是:
能延迟初始化的尽量延迟初始化,不能延迟初始化的尽量放到后台初始化。
这些工作主要可以分为几类:
- 三方SDK初始化,比如Crash统计; 像分享之类的,可以等到第一次调用再出初始化。
- 初始化某些基础服务,比如WatchDog,远程参数。
- 启动相关日志,日志往往涉及到DB操作,一定要放到后台去做
- 业务方初始化,这个交由每个业务自己去控制初始化时间。
比如根视图控制器通常是 UITabarController,里面的视图控制器可以只初始化一个,其他的使用占位符,等用户点击的时候,再初始化。
10:button的事件响应流程是什么
凡是继承于 UIResponder 或者 UIResponder 的子类的对象都可以作为响应对象,比如UIApplication、UIViewController和UIView。
在响应用户触摸等事件中,APP具体会通过下面三步来完成操作:
- 生成事件 。当用户点击屏幕时,会产生一个触摸事件,并放入由Application管理的事件队列中,然后在队列中取出最前面的事件交给Window处理。
- 查找第一响应对象 。Window收到事件后会在视图层次结构中找到最适合的一个视图来处理事件,通常一个窗口中最适合处理当前事件的对象称为第一响应对象。
- 处理事件 。通常最后是第一响应对象处理事件,如果第一响应对象无法处理事件,就会把事件传递给下一个响应对象,直到Application。如果Application也无法处理,那就丢弃掉此事件。
在上述系列操作中,所参与到的UIApplication、UIViewController和UIView就作为响应对象构成这次事件的响应链。
举个例子:
这里View A是window的根视图
1:调用A的 hit-test:withEvent,返回nil,调用 pointInside:withEvent: ,查看是否还有子视图,返回true。
2:对子视图 (View B,View C)进行 hit-Test
3:View B调用 pointInside:withEvent:,返回false,对应 hitTest:withEvent: 返回nil。
4:View C调用 pointInside:withEvent:,返回true,对View C所有子视图进行 hit-test
5:以此类推,一直到View E,调用 pointInside:withEvent: 方法,返回true,对应的hitTest:withEvent: 返回View E,回溯,View C返回View E,View A返回View E,这样Application就知道View E是第一响应对象。
如果第一响应对象能处理时间,则响应结束,否则就会沿着响应链一直回溯
11:weak的使用原理
weak是通过runtime初始化并维护的,本质是一个 weak表结构
struct weak_table_t {
weak_entry_t *weak_entries; //保存了所有指向指定对象的weak指针,weak_entries的对象
size_t num_entries; // weak对象的存储空间
uintptr_t mask; //参与判断引用计数辅助量
uintptr_t max_hash_displacement; //hash key 最大偏移值
};
typedef objc_object ** weak_referrer_t;
struct weak_entry_t {
DisguisedPtr referent; //范型
union {
struct {
weak_referrer_t *referrers;
uintptr_t out_of_line : 1;
uintptr_t num_refs : PTR_MINUS_1;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
struct {
// out_of_line=0 is LSB of one of these (don't care which)
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
};
}
}
- weak_table_t(weak 全局表):采用hash(哈希表)的方式把所有weak引用的对象,存储所有引用weak对象。
- weak_entry_t(weak_table_t表中hash表的value值,weak对象体):用于记录hash表中weak对象。
- objc_object(weak_entry_t对象中的范型对象,用于标记对象weak对象):用于标示weak引用的对象。
weak的初始化方法:
id objc_initWeak(id *addr, id val)
{
*addr = 0;
if (!val) return nil;
return objc_storeWeak(addr, val); // 存储weak对象
}
储存weak对象的方法
id
objc_storeWeak(id *location, id newObj)
{
id oldObj;
SideTable *oldTable;
SideTable *newTable;
spinlock_t *lock1;
#if SIDE_TABLE_STRIPE > 1
spinlock_t *lock2;
#endif
retry:
oldObj = *location;
oldTable = SideTable::tableForPointer(oldObj);
newTable = SideTable::tableForPointer(newObj);
lock1 = &newTable->slock;
#if SIDE_TABLE_STRIPE > 1
lock2 = &oldTable->slock;
if (lock1 > lock2) {
spinlock_t *temp = lock1;
lock1 = lock2;
lock2 = temp;
}
if (lock1 != lock2) spinlock_lock(lock2);
#endif
spinlock_lock(lock1);
if (*location != oldObj) {
spinlock_unlock(lock1);
#if SIDE_TABLE_STRIPE > 1
if (lock1 != lock2) spinlock_unlock(lock2);
#endif
goto retry;
}
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
newObj = weak_register_no_lock(&newTable->weak_table, newObj, location);
// weak_register_no_lock returns nil if weak store should be rejected
// Set is-weakly-referenced bit in refcount table.
if (newObj && !newObj->isTaggedPointer()) {
newObj->setWeaklyReferenced_nolock();
}
// Do not set *location anywhere else. That would introduce a race.
*location = newObj;
spinlock_unlock(lock1);
#if SIDE_TABLE_STRIPE > 1
if (lock1 != lock2) spinlock_unlock(lock2);
#endif
return newObj;
}
- 旧对象解除注册操作 weak_unregister_no_lock
该方法主要作用是将旧对象在 weak_table 中解除 weak 指针的对应绑定。根据函数名,称之为解除注册操作。从源码中,可以知道其功能就是从 weak_table 中接触 weak 指针的绑定。而其中的遍历查询,就是针对于 weak_entry 中的多张弱引用散列表。
- 新对象添加注册操作 weak_register_no_lock
这一步与上一步相反,通过 weak_register_no_lock 函数把心的对象进行注册操作,完成与对应的弱引用表进行绑定操作。
- 初始化弱引用对象流程一览
弱引用的初始化,从上文的分析中可以看出,主要的操作部分就在弱引用表的取键、查询散列、创建弱引用表等操作,可以总结出如下的流程图:
3.weak释放为nil过程
weak被释放为nil,需要对对象整个释放过程了解,如下是对象释放的整体流程:
- 调用objc_release
- 因为对象的引用计数为0,所以执行dealloc
- 在dealloc中,调用了_objc_rootDealloc函数
- 在_objc_rootDealloc中,调用了object_dispose函数
- 调用objc_destructInstance
- 最后调用objc_clear_deallocating。
对象准备释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。
详细见:http://www.cocoachina.com/ios/20181210/25761.html
12:NSTimer循环引用问题(等mac修好后试试)
timer作为VC的属性,被VC强引用,创建timer对象时VC作为target被timer强引用,即循环引用。
用于解决block循环引用的 weakself,无法用于NSTimer中,因为无论是weak还是strong,NSTimer中都会重新生成一个新的强引用指针指向self,导致循环引用。
解决办法:
创建一个继承NSObject的子类TempTarget,并创建开启计时器的方法。
.h文件
@property (nonatomic, assign) SEL selector; //VC中要调用的方法
@property (nonatomic, weak) NSTimer *tempTimer;
@property (nonatomic, weak) id tempTarget; //指向VC
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval target:(id)tempTarget selector:(SEL)selector userInfo:(id)userInfo repeats:(BOOL)repeats;
.m文件
@implementation TempTarget
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval target:(id)tempTarget selector:(SEL)selector userInfo:(id)userInfo repeats:(BOOL)repeats {
TempTarget *target = [[TempTarget alloc] init];
target.tempTarget = tempTarget;
target.selector = selector;
target.tempTimer = [NSTimer scheduledTimerWithTimeInterval:interval target:target selector:@selector(timerSelector:) userInfo:userInfo repeats:repeats];
return target.tempTimer;
}
- (void)timerSelector:(NSTimer *)tempTimer {
if (self.tempTarget && [self.tempTarget respondsToSelector:self.selector]) {
[self.tempTarget performSelector:self.selector withObject:tempTimer.userInfo];
}else {
[self.tempTimer invalidate];
}
}
@end
13:arc下什么时候release?
arc是编译器特性:
- 对于方法内的对象,在方法结束时添加release
- 对于成员变量,在dealloc中添加release
14: category和extentsion原理
category原理:
(1)在编译期间生成category对象
(2)在运行时动态的将普通方法添加到类对象中,将类方法添加到元类中
(3)catogory结构体中有属性,因而可以使用runtime添加set和get方法,但成员变量时在编译的时候确定的,因此category无法添加成员变量。
(4)如果category和原类中有同名方法,那么会优先调用category中的方法。
extension:
extension就是类的一部分,只有知道类的源码才可以为它添加extension,因此它在编译期决定。在ViewController中看到的这个其实就是一个extension。
@interface ViewControlelr()
@end