1. 定时器
1.1 CADisplayLink、NSTimer的问题
CADisplayLink
:保证调用频率表和屏幕的刷新频率一致- 常用属性 :
duration
两次屏幕刷新的时候间隔,通过此值可以拿到屏幕的刷新频率,苹果一般是60hz(一秒60次)是个估值,frameInterval
多少次屏幕刷新后才调用一次方法IOS10以后被废弃
,默认刷新一次调用一次timestamp
屏幕显示的上一帧的时间戳,是CoreAnimation使用的时间格式targetTimestamp
屏幕显示的下一帧时间戳paused
是否暂停计时器preferredFramesPerSecond
一秒内执行多次方法 默认60
//CADisplayLink
self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkTest)];
//
//需要添加到runloop中
[self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
- NSTimer用法我这里就不做介绍了,下面我们来分析下这两个定时器存在的问题。
- 定时器随控制器的销毁而销毁的问题, 很多人会写下面这段代码:
- (void)dealloc {
NSLog(@"%s",__func__);
[self.timer invalidate];
self.timer = nil;
}
但是实际上测试后我们可以发现,控制器和定时器都没有销毁,那是因为定时器内部对target
对象(也就是当前控制器)是有强引用的,而当前控制器又强引用定时器
对象,所以形成循环引用,所以根本不会走dealloc
方发。
有的人可能会想,在target对象处传入一个弱指针对象,不就解决循环引用的问题了吗?但是事实上并没有解决,因为CADisplayLink
、NSTimer
两个定时器对target
产生强引用,跟外面传入的target
对象是强指针和弱指针没有任何关系,他们内部有一个强指针指向这个target
对象,不像block跟外面对象的强弱指针有关
- 定时器有时候不准时的问题
- 因为这两个定时器都是依赖RunLoop来实现,我们都知道RunLoop是在循环处理事情,不是每时每刻都在关注定时器,而是到某一时间会去查看定时器是否到时间执行,如果当前RunLoop比较繁忙,那么就会导致延迟去执行定时器的任务
1.2 定时器循环引用解决方案
1.2.1 NSTimer使用blcok创建方式
- 针对
NSTimer
我们可以使用block
的创建方式, 这样就可以使用weakSelf
来解决循环引用
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
//执行任务
}];
1.2.2 利用中间对象和消息转发
- 我们可以创建一个中间对象
otherProxy
,使定时器对otherProxy
强引用,而otherProxy
对控制器是弱引用,这样就打破了循环引用,在使用消息转发机制来实现方法的调用
@interface GYOtherProxy : NSObject
@property (nonatomic, weak)id target;
+ (instancetype)proxyWithTarget:(id)target;
@end
@implementation GYOtherProxy
+ (instancetype)proxyWithTarget:(id)target {
GYOtherProxy *otherProxy = [[GYOtherProxy alloc] init];
otherProxy.target = target;
return otherProxy;
}
-(id)forwardingTargetForSelector:(SEL)aSelector {
//本质:objc_msgSend(self.target, aSelector)
return self.target;
}
//测试
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[GYOtherProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
- 经过测试可以发现,现在定时器不会和当前VC形成循环应用了,会随着VC的销毁而销毁
1.2.3 NSProxy
NSProxy
:基类,它存在的意义用来解决上面的代理行为,转发行为(消息转发),如果你调用NSProxy的某个方法,那么他会直接去调用methodSignatureForSelector
方法,直接进入消息转发机制- 下面我们使用NSProxy来解决定时器循环引用的问题
@interface GYProxy : NSProxy
@property (nonatomic, weak)id target;
+ (instancetype)proxyWithTarget:(id)target;
@end
@implementation GYProxy
+ (instancetype)proxyWithTarget:(id)target {
//继承自NSProxy 是没有init方法的,直接alloc方法即可直接分配内存
GYProxy *proxy = [GYProxy alloc];
proxy.target = target;
return proxy;
}
//消息转发
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation invokeWithTarget:self.target];
}
@end
//测试
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[GYProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
- 利用
中间对象
和直接使用NSProxy
有什么区别吗?- 直接使用
NSProxy
效率比较高,因为NSProxy
是专门用来做消息转发的,当没有找到方法,会直接报methodSignatureForSelector
错误,而继承自NSObject
的首先会进行消息查找、然后在是动态方法解析、最后才是消息转发,但是 继承是NSProxy的是直接进入消息转发阶段,所以继承自NSObject的要比继承自NSProxy的多了两个阶段 所以继承NSProxy的效率较高
- 直接使用
1.2.4 NSProxy和NSObject的区别
ViewController *vc = [[ViewController alloc] init];
GYOtherProxy *otherProxy = [GYOtherProxy proxyWithTarget:vc];
GYProxy *proxy = [GYProxy proxyWithTarget:vc];
NSLog(@"otherproxy ====%d",[otherProxy isKindOfClass:[UIViewController class]]); //0
NSLog(@"proxy ====%d",[proxy isKindOfClass:[UIViewController class]]);//1
- 由于
NSProxy
中方法几乎都会转发,所以NSProxy
调用isKindOfClass
方法也会进入消息转发阶段,所以[proxy isKindOfClass:[UIViewController class]]
本质上是vc
调用isKindOfClass
方法,所以返回1,这也NSProxy
和NSObject
的区别之处
1.3 GCD定时器
GCD定时器
是不依赖于RunLoop
的,是依赖内核的,所以GCD定时器
是非常精准的
1.3.1 GCD定时器的基本使用
//创建一个GCD定时器
dispatch_queue_t queue = dispatch_get_main_queue(); //创建队列
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
/**
void
dispatch_source_set_timer(dispatch_source_t source,
dispatch_time_t start, //开始时间 typedef uint64_t dispatch_time_t;
uint64_t interval, 间隔时间
uint64_t leeway); 延迟时间
NSEC_PER_SEC: 纳秒
*/
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{
NSLog(@"2223333----%@",[NSThread currentThread]);
});
//启动定时器
dispatch_resume(timer);
self.gcdTimer = timer;
GCD定时器
必须要强引用,保证生命周期,不然不会起作用- ARC环境下GCD创建的对象都不用我们管理内存,不需要我们去销毁
1.3.2 封装定时器
@interface GYTimer : NSObject
/// 使用该方法不需要时,需要手动停止定时器,不然定时器不会销毁,会一直在内存中执行
/// @param task <#task description#>
/// @param start <#start description#>
/// @param inteval <#inteval description#>
/// @param repeat <#repeat description#>
/// @param async <#async description#>
+ (NSString *)executeTask:(void(^)(void))task
start:(NSTimeInterval)start
interval:(NSTimeInterval)inteval
repeat:(BOOL)repeat
async:(BOOL)async;
/// 使用该方法不需要时,需要手动停止定时器,不然定时器不会销毁。 并且该定时器对target对象是强引用,如果不手动关闭定时器,那么会导致target对象不会销毁,但是不会和target对象形成循环引用
/// @param target <#target description#>
/// @param action <#action description#>
/// @param start <#start description#>
/// @param inteval <#inteval description#>
/// @param repeat <#repeat description#>
/// @param async <#async description#>
+ (NSString *)executeTaskWithTarget:(id)target
action:(SEL)action
start:(NSTimeInterval)start
interval:(NSTimeInterval)inteval
repeat:(BOOL)repeat
async:(BOOL)async;
//根据标记取消定时器
+ (void)cancelTiemr:(NSString *)identifier;
@end
@implementation GYTimer
/**
使用静态变量会造成以下问题,使用block执行任务时,即使退出,定时器依不会销毁,因为定时间cachesTiemr所拥有,除非手动停止定时器,不然会一直执行定时器
使用self selector等方式时,定时器会对target强引用,造成target对象不能释放,除非手动销毁定时器
*/
//缓存定时器
static NSMutableDictionary *cachesTiemr;
dispatch_semaphore_t semaphore;
+ (void)initialize {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
cachesTiemr = [NSMutableDictionary dictionary];
semaphore = dispatch_semaphore_create(1);
});
}
+ (NSString *)executeTask:(void (^)(void))task start:(NSTimeInterval)start interval:(NSTimeInterval)inteval repeat:(BOOL)repeat async:(BOOL)async {
if (!task || start < 0 || (inteval <= 0 && repeat)) return nil;
//创建一个GCD定时器
dispatch_queue_t queue = async ? dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
: dispatch_get_main_queue(); //创建队列
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC), inteval * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
//为了保证多线程安全 同时操作字典,我们需要加锁
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
//创建每个定时器对应的标记
NSString *identifier = [NSString stringWithFormat:@"%ld",cachesTiemr.count];
//定时器添加到字典,背字典引用, 字典被当前类强引用
cachesTiemr[identifier] = timer;
dispatch_semaphore_signal(semaphore);
dispatch_source_set_event_handler(timer, ^{
task();
if (!repeat) {
//如果不是重复,那么直接取消该定时器
[self cancelTiemr:identifier];
}
});
//启动定时器
dispatch_resume(timer);
return identifier;
}
+ (NSString *)executeTaskWithTarget:(id)target action:(SEL)action start:(NSTimeInterval)start interval:(NSTimeInterval)inteval repeat:(BOOL)repeat async:(BOOL)async {
if (!target || !action) return nil;
return [self executeTask:^{
//去除警告
#pragma clang diagnostic push
#pragma clang diagnostic ignored"-Warc-performSelector-leaks"
//写在这个中间的代码,都不会被编译器提示-Wdeprecated-declarations类型的警告
if ([target respondsToSelector:action]) {
[target performSelector:action];
}
#pragma clang diagnostic pop
} start:start interval:inteval repeat:repeat async:async];
}
+ (void)cancelTiemr:(NSString *)identifier {
if (!identifier || identifier.length == 0) {
return;
}
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_source_t timer = cachesTiemr[identifier];
if (timer) {
dispatch_source_cancel(timer);
[cachesTiemr removeObjectForKey:identifier];
}
dispatch_semaphore_signal(semaphore);
}
@end
2. 内存布局
int a = 10;
int b;
int main(int argc, char * argv[]) {
@autoreleasepool {
static int c = 20;
static int d;
int e;
int f = 20;
NSString *str = @"123";
NSObject *obj = [[NSObject alloc] init];
NSLog(@"\n&a=%p\n&b=%p\n&c=%p\n&d=%p\n&e=%p\n&f=%p\nstr=%p\nobj=%p\n",
&a, &b, &c, &d, &e, &f, str, obj);
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
/*
字符串常量
str=0x10dfa0068
已初始化的全局变量、静态变量
&a =0x10dfa0db8
&c =0x10dfa0dbc
未初始化的全局变量、静态变量
&d =0x10dfa0e80
&b =0x10dfa0e84
堆
obj=0x608000012210
栈
&f =0x7ffee1c60fe0
&e =0x7ffee1c60fe4
*/
3. Target Pointer技术
- 从64bit开始,IOS引入了
Target Pointer
技术,用于优化NSNumber、NSDate、NSString
等小对象的储存 - 在没有使用
Target Pointer
之前,NSNumber
等对象需要动态分配内存 、维护引用计数器等,NSNumber
指针存储的是堆中的NSNumber
对象的地址值 - 使用
Target Pointer
之后,NSNumber
指针里面就存储的数据变成了:Tag + Data
,也就是将数据直接存储了指针中 - 当对象指针最低有效位是1,则该指针为
Target Pointer
- 当指针不够存储数据时,才会使用动态分配内存的方式来储存数据
objc_msgSend()
能识别Target Pointer
,比如NSNumber
的intValue
方法,直接从指针提取数据,节省了以前的调用开销- 如何判断一个指针是否为
Target Pointer
- IOS平台,最高有效位为1(第64bit)
- Mac OS 平台,最低有效位为1
NSNumber *number1 = @2;
NSNumber *number2 = @3;
NSNumber *number3 = @4;
NSNumber *number4 = @5;
NSNumber *number5 = @(0xFFFFFFFFFFFFFFF);
//NSLog(@"%d %d %d", isTaggedPointer(number1), isTaggedPointer(number2), isTaggedPointer(number3));
NSLog(@"%p %p %p %p %p", number1, number2, number3,number4,number5);
//0xc9aa33901e86fdcb 0xc9aa33901e86fddb 0xc9aa33901e86fdab 0xc9aa33901e86fdbb 0x600003fbdc60
- 观察地址,number5的地址值明显要大很多,属于堆空间的地址
- 查看源码是怎么判断是不是属于Target Pointer对象,查看objc源码中
objc-internal.h
static inline bool
_objc_isTaggedPointer(const void * _Nullable ptr)
{
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
#if TARGET_OS_OSX && __x86_64__
// 64-bit Mac - tag bit is LSB
# define OBJC_MSB_TAGGED_POINTERS 0
#else
// Everything else - tag bit is MSB
# define OBJC_MSB_TAGGED_POINTERS 1
#endif
#if OBJC_MSB_TAGGED_POINTERS
//如果是IOS平台 (指针最高有效位是1 ,就是Target Pointer)
# define _OBJC_TAG_MASK (1UL<<63)
#else
//如果是MAC平台 (指针最低有效位是1 ,就是Target Pointer)
# define _OBJC_TAG_MASK 1UL
#endif
//所以我们完全可以根据源码写一个判断是否是Target Pointer 对象
BOOL isTaggedPointer(id pointer)
{
return (long)(__bridge void *)pointer & 1;
}
//在根据源码改造
BOOL isTargetPointer(id pointer) {
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;;
}
- 思考下面两段代码能发生什么事? 有什么区别?
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 1000; i++) {
dispatch_async(queue, ^{
// 加锁
self.name = [NSString stringWithFormat:@"abcdefghijk"];
// 解锁
});
}
- 这段代码会发生崩溃,报错,坏内存访问
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 1000; i++) {
dispatch_async(queue, ^{
self.name = [NSString stringWithFormat:@"abc"];
});
}
- 正常运行
- 那么这两段代码有什么区别,为什么前者会崩溃,后者征程运行?
- 首先我们要知道属性的
setter
方法底层实现, 底层实质是转成MRC执行,首先是把原先储存的变量释放,在赋值新的值
- 首先我们要知道属性的
- (void)setName:(NSString *)name
{
if (_name != name) {
//崩溃的原因就是多条线程可能同时访问到[_name release];这一行代码,同样的一个变量多次释放,所以发生坏内存访问(释放一个已经不存在的对象)
[_name release];
_name = [name retain];
}
}
- 第二段代码为什么不会崩溃,因为第二代码,给name赋值是直接缓存在地址中(也就是Target Pointer)的而不是动态分配内存的储存模式,下面我们可以看看这两个字符串的类型、地址
NSString *str1 = [NSString stringWithFormat:@"abcdefghijk"];
NSString *str2 = [NSString stringWithFormat:@"123abc"];
NSLog(@"%@ %@", [str1 class], [str2 class]); //
NSLog(@"%p %p", str1,str2);
//输出结果
// __NSCFString NSTaggedPointerString
// 0x600003b88de0 0xb413d58ae9046c7c
我们可以看出str1是__NSCFString
类型,地址是比较大的0x600003b88de0
, 而str2的类型NSTaggedPointerString
,地址是0xb413d58ae9046c7c
,由此我们可以看出,地址可以储存的对象,是直接使用Target Pointer
方式 来储存,而地址无发储存的对象采用动态分配内存的方式来储存的,而Target Pointer
方式储存的对象不会像OC对象一样去调用set
方法 就不会release
而是直接把值赋值到指针中(相当于不是一个oc对象,直接给指针赋值) 所以不会崩溃
4. MRC内存管理
4.1 概述
- 在IOS中,使用计时器来管理OC对象的内存
- 一个新创建的OC对象计数器默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间
- 调用
retain
会让OC对象的引用计数+1,调用release
会让OC对象的引用计数-1 - 所谓的内存泄漏:该释放的东西没有释放,所以我们需要保证引用计数的平衡
- 内存管理总结经验:
- 当调用alloc、new、copy、mutableCopy方法返回一个对象,在不需要这个对象时,要调用
release
或则autorelease
来释放它 - 想拥有某个对象,就让它的引用计数+1;不想在拥有某个对象,就让它引用计数-1
- 当调用alloc、new、copy、mutableCopy方法返回一个对象,在不需要这个对象时,要调用
4.2 成员变量式内存管理
- 简单对象的内存管理
//alloc 创建对象;引用计数默认为1
GYPerson *person = [[GYPerson alloc] init];
//GYPerson *person = [GYPerson new]; 这两句代码等价
//如果你不释放,就会形成内存泄漏,
[person release]; // 引用计数器减1 -> 销毁
- 对象中包含对象的内存管理,如果成员变量是OC对象那么必须进行内存管理;如果成员变量是基本数据类型,那就不需要内存管理
@interface GYPerson : NSObject
{
GYDog *_dog;
//如果是基本数据类型,我们就不必进行内存管理了 setter方法内部直接赋值就可以了
int _age;
}
- (void)setDog:(GYDog *)dog;
- (GYDog *)dog;
- (void)setAge:(int)age;
- (int)age;
@end
@implementation GYPerson
//ARC中对象setter方法转换成MRC模式下底层就是这样实现的
- (void)setDog:(GYDog *)dog {
//每次赋值的时候,需要判断赋值对象是否相同,如果没有这个判断,那么每次赋值同样的对象时候执行[_dog release];,就可能把当前赋值的对象给释放掉,然后在访问_dog时,_dog对象早就被释放了,会发生坏内存访问
if (_dog != dog) {
//每次赋值的时候,我们需要先释放上一个对象,在赋值新的对象,如果不释放掉以前的对象,那么就会造成内存泄漏
[_dog release];
//首先我们往_dog在当前对象存在的时候,就存在不会死,所以赋值让属性引用计数器+1
_dog = [dog retain];
}
}
- (GYDog *)dog {
return _dog;
}
- (void)setAge:(int)age {
_age = age;
}
- (int)age {
return _age;
}
- (void)dealloc {
//最后,当我们的set方法中把成员变量对象引用计数+1操作,我们这里需要做建议-1操作,达到引用计数器平衡
[_dog release];
_dog = nil; //最好是吧指针也星空
//上面两句代码相当于下面这句代码
//self.dog = nil; self.dog相当于调用set方法,会把nil传递进去[nil retain] = nil
NSLog(@"%s",__func__);
//一般父类的dealloc放到最好,也就是父类的东西放到最后进行销毁
[super dealloc];
}
@end
GYPerson *person = [[GYPerson alloc] init];
GYDog *dog = [[GYDog alloc] init];
[person setDog:dog];
//重复调用也不会发生问题
[person setDog:dog];
[person setDog:dog];
GYDog *dog1 = [[GYDog alloc] init];
//中途更换对象也不会报错
[person setDog:dog1]
[dog release];
[[person dog] run];
[person release];
4.3 属性式内存管理
- 首先我们不必在声明一个个成员变量,setter、getter方法了,直接声明一个属性即可
- 声明属性如果使用
assign
生成的setter
方法就是直接赋值,如果是retain
修饰生成的setter
方法就会把原来的值执行release
操作,然后新传进来的值进行一次retain
操作在赋值 - 属性: 声明一个属性编译器帮我们自动生成属性了
setter
和getter
方法的声明,无需我们自己写声明 @synthesize
:帮我们生成了一个和属性关联的成员变量,还自动帮我们生成了setter和getter方法的实现(例子:@synthesize age = _age 帮age属性生成一个对应的成员变量_age和setter、getter方法)retain
:使用retain
修饰的OC对象,系统会自动帮我们生成setter和getter方法实现,就和上面我们自己写的setter和getter方法的实现是一样的,还会帮我们自动生成对应的成员变量
- 声明属性如果使用
@interface GYPerson : NSObject
//声明一个OC对象属性时,需要使用retain来修饰,现在使用strong也可以
@property (nonatomic, retain) GYCar *car;
@end
@implementation GYPerson
//关联属性和成员变量,现在声明属性之后,就不需要这一步了
//@synthesize car = _car;
- (void)dealloc {
//虽然是系统自动帮我们生成setter方法,但是这里最后我们还是需要自己释放属性对象
[_car release];
_car = nil;
NSLog(@"%s",__func__);
//一般父类的dealloc放到最好,也就是父类的东西放到最后进行销毁
[super dealloc];
}
@end
- 只要不是alloc、new、copy开头创建对象的,一般我是不需要进行release操作的,底层内部可能调用了
autorelease
方法
4.4 MRC开发
@interface ViewController ()
@property (retain, nonatomic) NSMutableArray *data;
@property (retain, nonatomic) UITabBarController *tabBarController;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.tabBarController = [[[UITabBarController alloc] init] autorelease];
self.data = [NSMutableArray array];
// self.data = [[[NSMutableArray alloc] init] autorelease];
// self.data = [[NSMutableArray alloc] init];
// [self.data release];
// NSMutableArray *data = [[NSMutableArray alloc] init];
// self.data = data;
// [data release];
}
- (void)dealloc {
self.data = nil;
self.tabBarController = nil;
[super dealloc];
}
@end
- 如果你不调用
autorelease
方法,那么你就需要自己在不需要这个对象的时候调用release
方法,dealloc
方法中对应是set
方法中的引用计数器
5. Copy
- 拷贝: 产生一个副本对象,并切两者互不影响,这也是拷贝的本质所在
copy
:不可变拷贝,产生不可变副本mutableCopy
: 可变拷贝,产生可变副本- 只要调用的是
copy
方法返回就是不可变的对象,调用mutableCopy
返回的对象就是可变的,不管拷贝的对象是否是可变
5.1 copy、mutableCopy
NSString *str1 = [[NSString alloc] initWithFormat:@"NSString-copy"];
NSString *str2 = [str1 copy];
NSMutableString *str3 = [str1 mutableCopy];
[str3 appendString:@"1213123"];
NSLog(@"%@ %@ %@", str1, str2, str3);
NSLog(@"%p %p %p",str1,str2,str3);
[str3 release];
[str2 release];
[str1 release];
//打印结果 :0x10056fe70 0x10056fe70 0x10056fe90
- 由打印结果,str1和str2地址指向都是同一个对象,而str3明显是一个新的对象
NSMutableString *str1 = [[NSMutableString alloc] initWithFormat:@"NSString-copy"];
NSString *str2 = [str1 copy];
NSMutableString *str3 = [str1 mutableCopy];
[str1 appendString:@"11111"];
[str3 appendString:@"1213123"];
NSLog(@"%@ %@ %@", str1, str2, str3);
NSLog(@"%p %p %p",str1,str2,str3);
[str3 release];
[str2 release];
[str1 release];
//打印结果 :NSString-copy11111 NSString-copy NSString-copy1213123
//打印结果 :0x100404120 0x100404150 0x1004068f0
-
上述打印结果可知,str2、str3都是全新对象,并且修改其中任何一个,不会影响其他的对象
-
总结:
- 不管拷贝的对象是否是可变对象,调用
copy
方法拷贝出来的对象都是不可变的,而调用mutableCpoy
方法拷贝出来的对象都是一个可变对象 - 不可变对象调用
copy
方法,拷贝出来的对象对象本质上就是它本身,因为他们本身是不可变的,所以也满足拷贝出来的对象和原对象互不影响,因为它们是无法改变的,而两个对象指向同一个对象,也节省了内存 copy
方法返回都是不可变对象,mutableCopy
方法返回的都是可变对象
- 不管拷贝的对象是否是可变对象,调用
-
由此上述结论,我们引申出了,
浅拷贝
和深拷贝
的概念浅拷贝
:指针拷贝,没有产生新的对象深拷贝
:内容拷贝,没有产生新的对象- 注意: 不要认为
copy
方法就是浅拷贝,上述案例中如果可变对象
调用copy
方法,拷贝出来也是一个全新的对象,也是深拷贝
-
数组和字典和字符串的情况类似,这几类拷贝情况如下图:
5.2 使用copy修饰可变类型属性问题
@interface GYPerson : NSObject
/// MRC下使用copy来修饰可变类型的变量
@property (copy, nonatomic) NSMutableArray *data;
@end
@implementation GYPerson
- (void)dealloc {
self.data = nil;
NSLog(@"%s",__func__);
[super dealloc];
}
@end
//测试代码:
GYPerson *person = [[GYPerson alloc] init];
NSMutableArray *data = [[NSMutableArray alloc] initWithObjects:@"tom",@"jack", nil];
person.data = data;
[person.data addObject:@"andrew"];
[data release];
[person release];
执行结果报了一个非常经典错误:-[__NSArrayI addObject:]: unrecognized selector sent to instance 0x10283bec0
,NSAarry
对象找不到addObject
方法,有得人可能很奇怪,明明的我声明的是一个可变数组,为什么报错NSAarry
对象找不到addObject
方法错误了?
首先我们需要知道在MRC的环境下,使用copy
来修饰属性,编译器帮我们自动生成的setter
是什么样的
//在MRC环境下,我们使用copy来修饰属性,系统自动生成的set方法的实现如下
- (void)setData:(NSMutableArray *)data {
if (_data != data) {
[_data release];
_data = [data copy];
}
}
由上述代码可知,copy
修饰属性,底层是传递进来的对象调用一次copy
,返回一个不可变的对象,然后再复制给属性,所以不管你外面出传递进来的参数是否可变,经过copy
方法之后,这个属性最后一定是不可变类型,所以你在调用上面的data
,看似是一个可变数组,但是本质是已经是NSArray
类型的不可变数组了,所以你调用addObject
时会找不到该方法,因为NSArray
对象中确实没有该方法(把一个不可变的对象当成一个可变的对象来用 会出现问题)
有的人可能想,能不能用mutableCopy
来修饰属性,这个是没有的,属性中只存在copy
操作,不存在mutableCopy
操作
- 其实我们发现
mutableCopy
这种方案是只给Foundation
框架自带的一些类做事情的,如:NSArray、NSMuatbleArray、NSDictonary、NSMutableDictionary、NSString、NSMutableString、NSData、NSMutableData、NSSet、NSMutableSet
等等
5.3 自定义对象使用copy
@interface GYPerson : NSObject
@property (assign, nonatomic) int age;
@property (assign, nonatomic) int weight;
@end
@implementation GYPerson
- (NSString *)description {
return [NSString stringWithFormat:@"age====%d weight====%d",self.age, self.weight];
}
@end
//测试代码:
GYPerson *person = [[GYPerson alloc] init];
person.age = 20;
person.weight = 50;
GYPerson *person2 = [person copy];
person2.age = 30;
person2.weight = 60;
NSLog(@"person====%@",person);
NSLog(@"person2====%@",person2);
[person2 release];
[person release];
运行之后发现报错-[GYPerson copyWithZone:]: unrecognized selector sent to instance 0x100524ed0
,调用copy
方法,底层实质上调用copyWithZone
方法,所以如果要OC自定义对象,调用copy
方法,首先需要遵守NSCopying
协议,然后实现copyWithZone
方法,调用mutableCopy
需要遵守NSMutableCopying
协议,然后实现mutableCopyWithZone
方法
所以GYPerson需要遵守NSCopying
协议,实现copyWithZone
方法
- (id)copyWithZone:(NSZone *)zone {
//首先需要生成一个新的对象,然后把拷贝对象的所有属性都赋值给这个全新的对象
GYPerson *person = [GYPerson allocWithZone:zone];
person.age = self.age;
person.weight = self.weight;
return person;
}
实现上述方法之后,在此运行程序,打印结果如下:
6. 引用计数器、weak指针
6.1 引用计数器
- 在64bit中,引用计数器可以直接储存在优化过的isa指针中,也可能储存在
SideTable
类中(关于isa结构,有在Runtime中讲解过) SideTable
的结果源码(删除部分源码):
struct SideTable {
spinlock_t slock;
RefcountMap refcnts; //refcnts是一个储存着对象引用计数的散列表
weak_table_t weak_table;//储存着弱指针的表(也是采用散列表储存的)
};
- 下面我们查看
retainCount
方法的源码
- (NSUInteger)retainCount {
return ((id)self)->rootRetainCount();
}
inline uintptr_t
objc_object::rootRetainCount()
{
if (isTaggedPointer()) return (uintptr_t)this;
sidetable_lock();
isa_t bits = LoadExclusive(&isa.bits);
ClearExclusive(&isa.bits);
if (bits.nonpointer) {//判断是不会优化过的isa指针
uintptr_t rc = 1 + bits.extra_rc;
if (bits.has_sidetable_rc) {//判断引用计数不是存在isa中,而是存在sidetable中
rc += sidetable_getExtraRC_nolock();
}
sidetable_unlock();
return rc;
}
sidetable_unlock();
return sidetable_retainCount();
}
size_t
objc_object::sidetable_getExtraRC_nolock()
{
assert(isa.nonpointer);
//根据当前类作为key去除表中对应的引用计数,然后进行一些操作后返回
SideTable& table = SideTables()[this];
RefcountMap::iterator it = table.refcnts.find(this);
if (it == table.refcnts.end()) return 0;
else return it->second >> SIDE_TABLE_RC_SHIFT;
}
6.2 weak指针的实现原理
- (void)viewDidLoad {
[super viewDidLoad];
__strong GYPerson *person1;
__weak GYPerson *person2;
__unsafe_unretained GYPerson *person3;
NSLog(@"111111");
{
//正常情况下 person对象 出了{} 就会销毁
GYPerson *person = [[GYPerson alloc] init];
//使用一个强指针来延长person的声明周期,等到强指针销毁时,person对象才销毁
//person1 = person;
//不会产生一个强引用,当指向对象销毁时,会自动设置值为null
//person2 = person; //打印perosn2 为null,
//也不会产生强引用,但是如果对象销毁之后,还继续访问该指针,会发生野指针错误,对象销毁时,不会自动清空指针的值,还保存着原来的地址值,这个时候再去访问,会发生坏内存访问
person3 = person;
}
NSLog(@"22222====%@", person3);
}
- 下面我们可以通过查看
dealloc
方法的源码,连接weak指针的原理dealloc
方法_objc_rootDealloc
objc_object::rootDealloc
object_dispose
objc_destructInstance
- (void)dealloc {
//把当前对象传递进去
_objc_rootDealloc(self);
}
void
_objc_rootDealloc(id obj)
{
assert(obj);
obj->rootDealloc();
}
inline void
objc_object::rootDealloc()
{
//判断是不是一个Target Pointer对象,如果是则直接返回
if (isTaggedPointer()) return; // fixme necessary?
if (fastpath(isa.nonpointer && //判断是否是优化之后的isa指针
!isa.weakly_referenced && //判断是否存在弱指针指向
!isa.has_assoc && //判断是否有关联对象
!isa.has_cxx_dtor && //判断是否有C++析构函数
!isa.has_sidetable_rc))//判断引用计数是否存在sidetable中
{
assert(!sidetable_present());
//如果上述条件都不存在 那么直接释放对象
free(this);
}
else {
//否则条用该方法释放对象
object_dispose((id)this);
}
}
id
object_dispose(id obj)
{
if (!obj) return nil;
//调用该方法释放对象
objc_destructInstance(obj);
free(obj);
return nil;
}
void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();
// This order is important.
//清除成员变量
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj);
//将指向当前对象的弱指针设置为nil
obj->clearDeallocating();
}
return obj;
}
7. autorelease
7.1 概述
首先我们观察自动释放池转成的c++代码
int main(int argc, const char * argv[]) {
@autoreleasepool {
GYPerson *person = [[[GYPerson alloc] init] autorelease];
}
return 0;
}
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
GYPerson *person = ((GYPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((GYPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("GYPerson"), sel_registerName("alloc")), sel_registerName("init"));
}
return 0;
}
//结构体自动释放池代码
struct __AtAutoreleasePool {
// 构造函数,在创建结构体的时候调用
__AtAutoreleasePool() {
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
//析构函数,在结构体销毁的时候调用
~__AtAutoreleasePool() {
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
void * atautoreleasepoolobj;
};
所以上述代码的实质是,自动释放池的开始时调用objc_autoreleasePoolPush()
方法,结束时调用objc_autoreleasePoolPop()
方法
@autoreleasepool {
//自动释放池开始时会调用构造方法创建
//atautoreleasepoolobj = objc_autoreleasePoolPush();
GYPerson *person = [[GYPerson alloc] init];
//objc_autoreleasePoolPop(atautoreleasepoolobj);
}
7.2 autoreleasePool的结构
- 自动释放池的主要底层主要结构是:
__AtAutoreleasePool
、AutoreleasePoolPage
- 调用了
autorelease
的对象最终都会通过AutoreleasePoolPage
对象来管理的 - 源码分析:
- clang重写
@autoreleasepool
- objc4源码:
NSObject.mm
- clang重写
struct AutoreleasePoolPageData
{
magic_t const magic;
__unsafe_unretained id *next;//永远都指向下一个能存放autorelease对象的地方
pthread_t const thread;
AutoreleasePoolPage * const parent;//指向上一个AutoreleasePoolPage对象
AutoreleasePoolPage *child;//指向下一个AutoreleasePoolPage对象
uint32_t const depth;
uint32_t hiwat;
};
class AutoreleasePoolPage : private AutoreleasePoolPageData
{}
- 每个
AutoreleasePoolPage
对象占用4096字节内容,除了用来存放它内不的成员变量,剩下的空间用来存放autorelease
对象的地址 objc_autoreleasePoolPush()
方法会把调用autorelease
方法的对象的地址存放在AutoreleasePoolPage
这个对象内存中- 所有的
AutoreleasePoolPage
对象是通过双向链表的形式连接在一起的
- 调用
push
方法会传入一个POOL_BOUNDARY
入栈,并且返回其存放的内存地址,然后如果有对象调用autorelease
方法,就把这个对象的地址入栈 - 调用
pop
方法传入一个POOL_BOUNDARY
的内存地址,然后从最后一个入栈对象开始发送release
消息(出栈),直到遇到这个POOL_BOUNDARY
结束 - 我们可以利用extern关键字来声明系统内部一个函数,来查看
@autoreleasePool
的情况–extern void _objc_autoreleasePoolPrint(void)
7.3 Runtime与auotreleasePool
- IOS在主线程的RunLoop中注册了2个Observer
- 第1个Observer监听了
KCFRunLoopEntry
事件,会调用objc_autoreleasePoolPush()
- 第二个Observer:
- 监听了
KCFRunLoopBeforeWating
事件,会调用objc_autoreleasePoolPop()
、objc_autoreleasePoolPush()
- 监听了
KCFRunLoopBeforeExit
事件,会调用objc_autoreleasePoolPop()
- 监听了
- 第1个Observer监听了
- 我们可以通过打印
RunLoop
对象来查看Observer
对象 - 调用autorelease方法的对象什么时候释放,是由RunLoop来控制,比如可能实在当前所在Runloop循环中,RunLoop进入休眠之前调用release,或是在RunLoop退出之前调用release
8. 面试题
8. 1 使用CADisplayLink、NSTimer有什么注意点?
- 可能产生循环引用的问题
- 可能不准时的问题
8.2 介绍下内存的几大区域?
- 参考 本文内存布局
8.3 讲一下你对IOS的内存管理的理解
- TaggedPointer(针对类似于 NSNumber 的小对象类型),指针可以储存的就直接使用指针储存数据,不能储存的就使用分配内存的方式来储存
- isa指针在64bit下采用
共用体(union)和位域优
化过,没有优化之前储存的是一个普通的地址值,但是优化之后,isa指针中除了储存地址值,还储存了许多其他的信息:- 第一位的 0 或 1 代表是纯地址型 isa 指针,还是 NONPOINTER_ISA 指针。
- 第二位,代表是否有关联对象
- 第三位代表是否有 C++ 代码。
- 接下来33位代表指向的内存地址
- 接下来有 弱引用 的标记
- 接下来有是否 delloc 的标记…等等
8.4 autorelease对象在什么时候会调用release
- 首先调用
autorelease
方法的对象什么时候释放,是由RunLoop
来控制 - 比如可能实在当前所在
Runloop
循环中,RunLoop
进入休眠之前调用release
,或者是RunLoop
退出的时候调用release
8.5 方法里有局部对象,出了方法后会立即释放吗
- 立刻释放, ARC相当于在方法的结尾插入了一句代码(
[obj release]
) 直接调用对象的release代码 ,所以出了方法之后立即释放 ,如果是给对象加上autorelease方法的话,释放就是由Runloop来控制
8.6 ARC都帮我们做了什么?
- ARC是由LLVM+Runtime相互协作的结果
- 利用LLVM这个编译自动帮我们成retain,release、autorelease代码
- 在程序运行中利用Runtime帮我们处理弱引用这些操作
8.7 weak指针的实现原理
- 将弱引用存到一个hash表中,到时候这个对象要销毁时,取出当前对象对应的弱引用,把弱应用表中存储的弱引用清空