1 面试:全局变量和局部变量在内存中是否有区别?如果有,是什么区别?
局部变量定义在局部的空间,全局变量定义在全局的区域,存储的区域不一样
2 block 是否可以直接修改全局变量?
可以,全局变量作用空间特别大,是公共的
3静态区安全测试
全局静态变量可以修改 只对文件有效,不是对类有效
新建一个LGPerson的类
//.h文件
#import <Foundation/Foundation.h>
static int personNum = 100;
NS_ASSUME_NONNULL_BEGIN
@interface LGPerson : NSObject
- (void)run;
+ (void)eat;
@end
//.m文件
@implementation LGPerson
- (void)run{
personNum ++;
NSLog(@"LGPerson内部:%@-%p--%d",self,&personNum,personNum);
}
+ (void)eat{
personNum ++;
NSLog(@"LGPerson内部:%@-%p--%d",self,&personNum,personNum);
}
- (NSString *)description{
return @"";
}
@end
再新建一个LGPerson的一个分类
#import "LGPerson.h"
NS_ASSUME_NONNULL_BEGIN
@interface LGPerson (LG)
- (void)cate_method;
@end
//.m文件
@implementation LGPerson (LG)
- (void)cate_method{
NSLog(@"LGPerson内部:%@-%p--%d",self,&personNum,personNum);
}
@end
调用
// 100 可以修改
// 只针对文件有效 -
NSLog(@"vc:%p--%d",&personNum,personNum); // 100
personNum = 10000;
NSLog(@"vc:%p--%d",&personNum,personNum); // 10000
[[LGPerson new] run]; // 100 + 1 = 101
NSLog(@"vc:%p--%d",&personNum,personNum); // 10000
[LGPerson eat]; // 102
NSLog(@"vc:%p--%d",&personNum,personNum); // 10000
[[LGPerson alloc] cate_method];
打印结果为
2020-11-07 12:10:39.026774+0800 001---五大区Demo[5285:436872] vc:0x101bc23cc--100
2020-11-07 12:10:39.027201+0800 001---五大区Demo[5285:436872] vc:0x101bc23cc--10000
2020-11-07 12:10:39.027532+0800 001---五大区Demo[5285:436872] LGPerson内部:-0x101bc23b0--101
2020-11-07 12:10:39.027790+0800 001---五大区Demo[5285:436872] vc:0x101bc23cc--10000
2020-11-07 12:10:39.028165+0800 001---五大区Demo[5285:436872] LGPerson内部:LGPerson-0x101bc23b0--102
2020-11-07 12:10:39.028627+0800 001---五大区Demo[5285:436872] vc:0x101bc23cc--10000
2020-11-07 12:10:39.029702+0800 001---五大区Demo[5285:436872] LGPerson内部:-0x101bc23d0--100
Tagged Pointer
先看一个面试题
- (void)taggedPointerDemo {
self.queue = dispatch_queue_create("com.cooci.cn", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i<10000; i++) {
dispatch_async(self.queue, ^{
self.nameStr = [NSString stringWithFormat:@"cooci"];
NSLog(@"%@",self.nameStr);
});
}
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// 多线程
// setter getter
/**
retian newvalue
realase oldvalue
taggedpointer 影响
*/
NSLog(@"来了");
for (int i = 0; i<10000; i++) {
dispatch_async(self.queue, ^{
self.nameStr = [NSString stringWithFormat:@"cooci_和谐学习不急不躁"];
NSLog(@"%@",self.nameStr);
});
}
}
运行taggedPointerDemo,代码运行没问题,点击运行另一块代码,项目崩溃,之前多线程部分讲到新值替换旧值的一瞬间可能会为空,导致程序崩溃,那么为什么上面的没有问题,下面的崩溃呢?添加断点,打印得知:
当self.nameStr为cooci时,类型为NSTaggedPointerString
当self.nameStr为cooci_和谐学习不急不躁时,类型为NSCFString,
打开源码,搜索release方法
__attribute__((aligned(16), flatten, noinline))
id
objc_retain(id obj)
{
if (!obj) return obj;
if (obj->isTaggedPointer()) return obj;
return obj->retain();
}
__attribute__((aligned(16), flatten, noinline))
void
objc_release(id obj)
{
if (!obj) return;
if (obj->isTaggedPointer()) return;
return obj->release();
}
可以发现不会对TaggedPointer类型进行release,retain操作,一般是在8到10位左右长度的小对象,编译读取的时候更加直接,释放是由系统直接回收
总结:
1:Tagged Pointer专⻔用来存储小的对象,例如NSNumber和NSDate
2:Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再 是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储 在堆中,也不需要malloc和free
3.在内存读取上有着3倍的效率,创建时比以前快106倍。
NONPOINTER_ISA
NONPOINTER_ISA同样是苹果公司对于内存优化的一种方案。用 64 bit 存储一个内存地址显然是种浪费,毕竟很少有那么大内存的设备。于是可以优化存储方案,用一部分额外空间存储其他内容。isa 指针第一位为 1 即表示使用优化的 isa 指针,这里列出在x86_64架构下的 64 位环境中 isa 指针结构,arm64的架构会有所差别。
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 8
};
#endif
};
nonpointer:示是否对isa开启指针优化。0代表是纯isa指针,1代表除了地址外,还包含了类的一些信息、对象的引用计数等
has_assoc:关联对象标志位,0没有,1存在。
has_cxx_dtor:该对象是否有C++或Objc的析构器,如果有析构函数,则需要做一些析构的逻辑处理,如果没有,则可以更快的释放对象。
shiftcls:存在类指针的值,开启指针优化的情况下,arm64位中有33位来存储类的指针
magic:判断当前对象是真的对象还是一段没有初始化的空间
weakly_referenced:是否被指向或者曾经指向一个ARC的弱变量,没有弱引用的对象释放的更快。
deallocating:标志是否正在释放内存。
has_sidetable_rc:是否有辅助的引用计数散列表。当对象引⽤技术⼤于 10 时,则需要借⽤该变量存储进位。
extra_rc:表示该对象的引⽤计数值,实际上是引⽤计数值减 1,例如,如果对象的引⽤计数为 10,那么 extra_rc 为 9。如果引⽤计数⼤于 10,则需要使⽤到下⾯的 has_sidetable_rc。
内存管理
retain是如何处理的
查看源码
retain方法会调用方法rootRetain,
objc_object::retain()
{
ASSERT(!isTaggedPointer());
if (fastpath(!ISA()->hasCustomRR())) {
return rootRetain();
}
return ((id(*)(objc_object *, SEL))objc_msgSend)(this, @selector(retain));
}
继续查看方法rootRetain
ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
if (isTaggedPointer()) return (id)this;
bool sideTableLocked = false;
bool transcribeToSideTable = false;
isa_t oldisa;
isa_t newisa;
// retain 引用计数处理
//
do {
transcribeToSideTable = false;
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
// 散列表的引用计数表 进行处理 ++
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa.bits);
if (rawISA()->isMetaClass()) return (id)this;
if (!tryRetain && sideTableLocked) sidetable_unlock();
if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
else return sidetable_retain();
}
// don't check newisa.fast_rr; we already called any RR overrides
if (slowpath(tryRetain && newisa.deallocating)) {
ClearExclusive(&isa.bits);
if (!tryRetain && sideTableLocked) sidetable_unlock();
return nil;
}
uintptr_t carry;
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++
if (slowpath(carry)) {
// newisa.extra_rc++ overflowed
if (!handleOverflow) {
ClearExclusive(&isa.bits);
return rootRetain_overflow(tryRetain);
}
// Leave half of the retain counts inline and
// prepare to copy the other half to the side table.
if (!tryRetain && !sideTableLocked) sidetable_lock();
sideTableLocked = true;
transcribeToSideTable = true;
newisa.extra_rc = RC_HALF;
newisa.has_sidetable_rc = true;
}
} while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));
if (slowpath(transcribeToSideTable)) {
// Copy the other half of the retain counts to the side table.
sidetable_addExtraRC_nolock(RC_HALF);
}
if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
return (id)this;
}
首先判断是不是nonpointer isa ,如果不是,引用计数存储在散列表里面
这个散列表包含哪些东西呢?我们进源码看一下:
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
SideTable() {
memset(&weak_table, 0, sizeof(weak_table));
}
~SideTable() {
_objc_fatal("Do not delete SideTable.");
}
void lock() { slock.lock(); }
void unlock() { slock.unlock(); }
void forceReset() { slock.forceReset(); }
// Address-ordered lock discipline for a pair of side tables.
template<HaveOld, HaveNew>
static void lockTwo(SideTable *lock1, SideTable *lock2);
template<HaveOld, HaveNew>
static void unlockTwo(SideTable *lock1, SideTable *lock2);
};
可以看到散列表包含一把锁 lock,引用计数表,弱引用表。
在代码中我们看到有SideTables,可以知道里面有多张散列表,出于安全和性能问题,里面有多张表。数量不得知,大概64张。
通过哈希结构存储多张散列表。
retain是对引用计数进行处理,也是对散列表的引用计数表进行处理 ++ --。
如果是nonpointer isa,调用addc,对bits里面的位数进行操作
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);
这个地方如果超负荷的话,就会对散列表进行操作,会把原来的一半存储在extra_rc里面,另一半存储在散列表里面
if (!tryRetain && !sideTableLocked) sidetable_lock();
sideTableLocked = true;
transcribeToSideTable = true;
newisa.extra_rc = RC_HALF;
newisa.has_sidetable_rc = true;
}
这里优先isa,是因为extra_rc比散列表的操作要快
release操作
我们查看源码,发现release方法会调用rootRelease方法。
objc_object::release()
{
ASSERT(!isTaggedPointer());
if (fastpath(!ISA()->hasCustomRR())) {
rootRelease();
return;
}
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(release));
}
继续查看rootRelease方法,其原理与retain一致,这里需要注意一点,就是dealloc方法是在哪里调用的呢?
是在release调用的时候,当引用计数为0的时候,会通过objc_msgSend方式调用
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
if (isTaggedPointer()) return false;
bool sideTableLocked = false;
isa_t oldisa;
isa_t newisa;
retry:
do {
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa.bits);
if (rawISA()->isMetaClass()) return false;
if (sideTableLocked) sidetable_unlock();
return sidetable_release(performDealloc);
}
// don't check newisa.fast_rr; we already called any RR overrides
uintptr_t carry;
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); // extra_rc--
if (slowpath(carry)) {
// don't ClearExclusive()
goto underflow;
}
} while (slowpath(!StoreReleaseExclusive(&isa.bits,
oldisa.bits, newisa.bits)));
if (slowpath(sideTableLocked)) sidetable_unlock();
return false;
underflow:
// newisa.extra_rc-- underflowed: borrow from side table or deallocate
// abandon newisa to undo the decrement
newisa = oldisa;
if (slowpath(newisa.has_sidetable_rc)) {
if (!handleUnderflow) {
ClearExclusive(&isa.bits);
return rootRelease_underflow(performDealloc);
}
// Transfer retain count from side table to inline storage.
if (!sideTableLocked) {
ClearExclusive(&isa.bits);
sidetable_lock();
sideTableLocked = true;
// Need to start over to avoid a race against
// the nonpointer -> raw pointer transition.
goto retry;
}
// Try to remove some retain counts from the side table.
size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);
// To avoid races, has_sidetable_rc must remain set
// even if the side table count is now zero.
if (borrowed > 0) {
// Side table retain count decreased.
// Try to add them to the inline count.
newisa.extra_rc = borrowed - 1; // redo the original decrement too
bool stored = StoreReleaseExclusive(&isa.bits,
oldisa.bits, newisa.bits);
if (!stored) {
// Inline update failed.
// Try it again right now. This prevents livelock on LL/SC
// architectures where the side table access itself may have
// dropped the reservation.
isa_t oldisa2 = LoadExclusive(&isa.bits);
isa_t newisa2 = oldisa2;
if (newisa2.nonpointer) {
uintptr_t overflow;
newisa2.bits =
addc(newisa2.bits, RC_ONE * (borrowed-1), 0, &overflow);
if (!overflow) {
stored = StoreReleaseExclusive(&isa.bits, oldisa2.bits,
newisa2.bits);
}
}
}
if (!stored) {
// Inline update failed.
// Put the retains back in the side table.
sidetable_addExtraRC_nolock(borrowed);
goto retry;
}
// Decrement successful after borrowing from side table.
// This decrement cannot be the deallocating decrement - the side
// table lock and has_sidetable_rc bit ensure that if everyone
// else tried to -release while we worked, the last one would block.
sidetable_unlock();
return false;
}
else {
// Side table is empty after all. Fall-through to the dealloc path.
}
}
// Really deallocate.
if (slowpath(newisa.deallocating)) {
ClearExclusive(&isa.bits);
if (sideTableLocked) sidetable_unlock();
return overrelease_error();
// does not actually return
}
newisa.deallocating = true;
if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;
if (slowpath(sideTableLocked)) sidetable_unlock();
__c11_atomic_thread_fence(__ATOMIC_ACQUIRE);
if (performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
}
return true;
}
retainCount
先看一个面试题:
NSObject *objc = [NSObject alloc]; // 0
NSLog(@"%ld",objc.retainCount); // 1
NSLog(@"%ld",objc.retainCount); // 1
两次打印结果都为1,那么这里请问alloc出来的对象引用计数为1 是否正确?答案错误,alloc出来的对象引用计数为0.
因为alloc 源码里没有对引用计数进行操作,那我们看下retainCount的源码,bits.extra_rc为0,可以发现alloc出来的对象引用计数为0,打印出来为1是因为这里会默认给他一个1。再次打印一次retainCount还是为1,是因为这里的bits.extra_rc并没有存值,还是为0,所以打印出来的还是1。
inline uintptr_t
objc_object::rootRetainCount() // 1
{
if (isTaggedPointer()) return (uintptr_t)this;
sidetable_lock();
isa_t bits = LoadExclusive(&isa.bits);
ClearExclusive(&isa.bits);
if (bits.nonpointer) {
// bits.extra_rc = 0;
//
uintptr_t rc = 1 + bits.extra_rc; // isa
if (bits.has_sidetable_rc) {
rc += sidetable_getExtraRC_nolock(); // 散列表
}
sidetable_unlock();
return rc; // 1
}
sidetable_unlock();
return sidetable_retainCount();
}
dealloc
下面我们分析dealloc操作源码,这里对调用方法rootDealloc,然后是object_dispose方法
inline void
objc_object::rootDealloc()
{
if (isTaggedPointer()) return; // fixme necessary?
if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
!isa.has_cxx_dtor &&
!isa.has_sidetable_rc))
{
assert(!sidetable_present());
free(this);
}
else {
object_dispose((id)this);
}
}
object_dispose方法
id
object_dispose(id obj)
{
if (!obj) return nil;
// weak
// cxx
// 关联对象
// ISA 64
objc_destructInstance(obj);
free(obj);
return nil;
}
在free方法之前走方法objc_destructInstance
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();
}
return obj;
}
然后走方法clearDeallocating,在这里进行散列表清空,如果含有弱引用计数,调用方法clearDeallocating_slow,继续清理引用计数表
inline void
objc_object::clearDeallocating()
{
if (slowpath(!isa.nonpointer)) {
// Slow path for raw pointer isa.
sidetable_clearDeallocating();
}
else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) {
// Slow path for non-pointer isa with weak refs and/or side table data.
clearDeallocating_slow();
}
assert(!sidetable_present());
}
objc_object::clearDeallocating_slow()
{
ASSERT(isa.nonpointer && (isa.weakly_referenced || isa.has_sidetable_rc));
SideTable& table = SideTables()[this];
table.lock();
if (isa.weakly_referenced) {
weak_clear_no_lock(&table.weak_table, (id)this);
}
if (isa.has_sidetable_rc) { // 清理引用计数表
table.refcnts.erase(this);
}
table.unlock();
}
总结一下上面的流程可以如下所示:
强引用
我们知道打破block强引用的话,可以用weakSelf解决,self指向block,block指向weakSelf,来打破循环引用,注意一点,这个block捕获的是weakSelf这个临时变量的指针地址,不是这个对象,weakSelf和self是两个不同的指针地址,指向了同一个对象,所以打破了这里的循环引用。
self -> block -> weakSelf(临时变量的指针地址)
下面我们再看一个例子,这里会形成一个强引用,self持有timer,timer持有self
self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
如果我们也像上面那样操作用weakSelf是否能解决呢?改成下面这样
__weak typeof(self) weakSelf = self; // weak
self.timer = [NSTimer timerWithTimeInterval:1 target:weakSelf selector:@selector(fireHome) userInfo:nil repeats:YES];
答案是不能,因为这里timer强持有的事weakSelf指针指向的对象,也就是self指针指向的对象。
自动释放池
自动释放池始于MRC时代,主要是用于 自动 对 释放池内 对象 进行引用计数-1的操作,即自动执行release方法,在MRC中使用autoreleasepool必须在代码块内部手动为对象调用autorelease把对象加入到的自动释放池,系统会自动在代码块结束后,对加入自动释放池中的对象发送一个release消息.无需手动调用release.
通过clang生成cpp文件,查看
int main(int argc, const char * argv[]) {
@autoreleasepool {
}
}
在cpp文件显示为这个结构体的调用,也就是用走方法__AtAutoreleasePool和~__AtAutoreleasePool()
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
先看方法objc_autoreleasePoolPush,搜索源码
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
继续往下点击
这里看到继承自AutoreleasePoolPageData,我们看下AutoreleasePoolPageData是什么
class AutoreleasePoolPage;
struct AutoreleasePoolPageData
{
magic_t const magic; // 16
__unsafe_unretained id *next; //8
pthread_t const thread; // 8
AutoreleasePoolPage * const parent; //8
AutoreleasePoolPage *child; //8
uint32_t const depth; // 4
uint32_t hiwat; // 4
AutoreleasePoolPageData(__unsafe_unretained id* _next, pthread_t _thread, AutoreleasePoolPage* _parent, uint32_t _depth, uint32_t _hiwat)
: magic(), next(_next), thread(_thread),
parent(_parent), child(nil),
depth(_depth), hiwat(_hiwat)
{
}
};
struct magic_t {
static const uint32_t M0 = 0xA1A1A1A1; // 静态变量不在这里存储
# define M1 "AUTORELEASE!"
static const size_t M1_len = 12;
uint32_t m[4]; //4*4 = 16
magic_t() {
ASSERT(M1_len == strlen(M1));
ASSERT(M1_len == 3 * sizeof(m[1]));
m[0] = M0;
strncpy((char *)&m[1], M1, M1_len);
}
~magic_t() {
// Clear magic before deallocation.
// This prevents some false positives in memory debugging tools.
// fixme semantically this should be memset_s(), but the
// compiler doesn't optimize that at all (rdar://44856676).
volatile uint64_t *p = (volatile uint64_t *)m;
p[0] = 0; p[1] = 0;
}
AutoreleasePoolPageData是个结构体,里面有自己的一些成员变量,里面的字段的意义如下
magic 用来校验 AutoreleasePoolPage 的结构是否完整;
• next 指向最新添加的 autoreleased 对象的下一个位置,初始化时指向
begin() ;
• thread 指向当前线程;
• parent 指向父结点,第一个结点的 parent 值为 nil ;
• child 指向子结点,最后一个结点的 child 值为 nil ;
• depth 代表深度,从 0 开始,往后递增 1;
• hiwat 代表 high water mark 最大入栈数量标记
总结一下:
AutoreleasePoolPage其实就是一个双向链表结构,AutoreleasePoolPage(自动释放池页) 用来存放 autorelease 的对象,但是每一页的大小是有限制的,假如某个AutoreleasePoolPage页中需要存放的autorelease 的对象过多,一页存放不完,所以它就需要指向父结点点,在指向父结点里的AutoreleasePoolPage页中继续存放.
那么每一页大小是多少呢?
class AutoreleasePoolPage : private AutoreleasePoolPageData
{
friend struct thread_data_t;
public:
static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
PAGE_MAX_SIZE; // must be multiple of vm page size
#else
PAGE_MIN_SIZE; // size and alignment, power of 2
.
.
.
#endif
}
有个PAGE_MAX_SIZE,点击进去是4096.原来每一页AutoreleasePoolPage可以存放4096个字节.一共4096个字节, (4096 - AutoreleasePoolPage 中自己成员变量所占的字节)/每个对象中所占的字节. (4096 - 56)/8 = 505. 好的,每一AutoreleasePoolPage可以存放505个对象。
一个autoreleasePoolPage最多能添加504个8字节对象是否正确?
第一页是504+1个对象,这一个不是你添加进去的,是一个标记,是边界,也就是第一页最多能添加504个对象,其他页最多能添加505个对象。
总结
在APP中,整个主线程是运行在一个自动释放池中的。
main函数中的自动释放池的作用:这个池块给出了一个pop点来显式的告诉我们这里有一个释放点,如果你的main在初始化的过程中有别的内容可以放在这里。
使用@autoreleasepool标记,调用push()方法。
没有hotpage,调用(),设置EMPTY_POOL_PLACEHOLDER。
因为设置了EMPTY_POOL_PLACEHOLDER,所以会设置本页为hotpage,添加边界标记POOL_BOUNDARY,最后添加obj。
继续有对象调用autorelease,此时已经有了page,调用page->add(obj)。
如果page满了,调用autoreleaseFullPage()创建新page,重复第6点。
到达autoreleasePool边界,调用pop方法,通常情况下会释放掉POOL_BOUNDARY之后的所有对象