1、CADisplayLink、NSTimer使用
CADisplayLink、NSTimer会对target产生强引用,如果target又对它们产生强引用,那么就会引发循环引用
解决方案
- 使用Block
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:2.0 repeats:YES block:^(NSTimer *timer) {
[weakSelf test];
}];
- (void)test {
}
- 使用代理对象(NSProxy)
@interface SRProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end
@implementation SRProxy
+ (instancetype)proxyWithTarget:(id)target {
// NSProxy对象不需要调用init,因为它本来就没有init方法
SRProxy *proxy = [SRProxy 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:[SRProxy proxyWithTarget:self]
selector:@selector(test)
userInfo:nil
repeats:YES];
- (void)test {
}
2、GCD定时器
-
NSTimer
依赖于RunLoop
,如果RunLoop
的任务过于繁重,可能会导致NSTimer
不准时 - 而GCD的定时器会更加准时
dispatch_queue_t queue = dispatch_queue_create("queue", NULL);
//创建定时器
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
//设置时间
uint64_t start = 2.0; // 2秒后开始执行
uint64_t interval = 1.0; // 每隔1秒执行
dispatch_source_set_timer(timer,
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(start * NSEC_PER_SEC)),
interval * NSEC_PER_SEC,
0 * NSEC_PER_SEC);
//设置回调
dispatch_source_set_event_handler(timer, ^{
});
//启动定时器
dispatch_resume(timer);
3、iOS程序的内存布局
- 代码段:编译之后的代码
- 数据段
- 字符串常量:比如
NSString *str = @"123"
- 已初始化数据:已初始化的全局变量、静态变量等
- 未初始化数据:未初始化的全局变量、静态变量等
- 栈:函数调用开销,比如局部变量。分配的内存空间地址越来越小
- 堆:通过
alloc
、malloc
、calloc
等动态分配的空间,分配的内存空间地址越来越大
4、Tagged Pointer
- 从64bit开始,iOS引入了
Tagged Pointer
技术,用于优化NSNumber
、NSDate
、NSString
等小对象的存储 - 在没有使用
Tagged Pointer
之前,NSNumber
等对象需要动态分配内存、维护引用计数等,NSNumber
指针存储的是堆中NSNumber
对象的地址值 - 使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中
- 当指针不够存储数据时,才会使用动态分配内存的方式来存储数据
-
objc_msgSend
能识别Tagged Pointer
,比如NSNumber
的intValue
方法,直接从指针提取数据,节省了以前的调用开销 - 如何判断一个指针是否为
Tagged Pointer
? - iOS平台,最高有效位是1(第64bit)
- Mac平台,最低有效位是1
//objc-internal.h
#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
# define _OBJC_TAG_MASK (1UL<<63)
#else
# define _OBJC_TAG_MASK 1UL
#endif
//objc-internal.h
static inline bool
_objc_isTaggedPointer(const void * _Nullable ptr)
{
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
例子
多线程访问设置NSString崩溃
@interface ViewController ()
@property (strong, nonatomic) NSString *name;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
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"];
});
}
}
@end
将字符串改为短点的abc不崩溃,因为abc直接存储到name的指针中。
@interface ViewController ()
@property (strong, nonatomic) NSString *name;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
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"];
});
}
}
@end
5、OC对象的内存管理
- 在iOS中,使用引用计数来管理OC对象的内存
- 一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间
- 调用
retain
会让OC对象的引用计数+1,调用release
会让OC对象的引用计数-1 - 内存管理的经验总结
- 当调用
alloc
、new
、copy
、mutableCopy
方法返回了一个对象,在不需要这个对象时,要调用release
或者autorelease
来释放它 - 想拥有某个对象,就让它的引用计数+1;不想再拥有某个对象,就让它的引用计数-1
- 可以通过以下私有函数来查看自动释放池的情况
extern void _objc_autoreleasePoolPrint(void);
6、copy和mutableCopy
Left Aligned | copy | mutableCopy |
---|---|---|
NSString | NSString(浅拷贝) | NSMutableString(深拷贝) |
NSMutableString | NSString(深拷贝) | NSMutableString(深拷贝) |
NSArray | NSArray(浅拷贝) | NSMutableArray(深拷贝) |
NSMutableArray | NSArray(深拷贝) | NSMutableArray(深拷贝) |
NSDictionary | NSDictionary(浅拷贝) | NSMutableDictionary(深拷贝) |
NSMutableDictionary | NSDictionary(深拷贝) | NSMutableDictionary(深拷贝) |
7、引用计数的存储
在64bit中,引用计数可以直接存储在优化过的isa
指针中,也可能存储在SideTable
类中
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
};
struct weak_table_t {
weak_entry_t *weak_entries;
size_t num_entries;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
refcnts是一个存放着对象引用计数的散列表
8、dealloc
当一个对象要释放时,会自动调用dealloc
,接下的调用轨迹是
dealloc
_objc_rootDealloc
rootDealloc
object_dispose
objc_destructInstance、free
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);
obj->clearDeallocating();//将指向当前对象的弱指针制为nil
}
return obj;
}
9、自动释放池
- 自动释放池的主要底层数据结构是:
__AtAutoreleasePool
、AutoreleasePoolPage
- 调用了
autorelease
的对象最终都是通过AutoreleasePoolPage
对象来管理的 - 源码分析
- clang重写
@autoreleasepool
- objc4源码:
NSObject.mm
class AutoreleasePoolPage
{
magic_t const magic;
id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
};
- 每个
AutoreleasePoolPage
对象占用4096字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放autorelease对象
的地址 - 所有的
AutoreleasePoolPage
对象通过双向链表的形式连接在一起 - 调用
push
方法会将一个POOL_BOUNDARY
入栈,并且返回其存放的内存地址 - 调用
pop
方法时传入一个POOL_BOUNDARY
的内存地址,会从最后一个入栈的对象开始发送release
消息,直到遇到这个POOL_BOUNDARY
- id
*next
指向了下一个能存放autorelease对象地址
的区域
10、Runloop和Autorelease关系
iOS在主线程的Runloop中注册了2个Observer
- 第1个Observer监听了
kCFRunLoopEntry
事件,会调用objc_autoreleasePoolPush()
- 第2个Observer
- 监听了
kCFRunLoopBeforeWaiting
事件,会调用objc_autoreleasePoolPop()
、objc_autoreleasePoolPush()
- 监听了
kCFRunLoopBeforeExit
事件,会调用objc_autoreleasePoolPop()