全局静态变量考察
.h文件
static int personNum = 100;
@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);
}
@end
然后在ViewController中执行下面代码的打印结果是什么?
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
结果已经在上面代码的注释中,全局静态变量只对文件内有效。一个文件内是同一个变量。如果在其他文件访问这个全局静态变量,那么访问到的是另外copy的变量,地址和源文件的全局静态变量不一样。
taggedPointer考察
已知ViewController中有以下的nameStr属性。
@interface ViewController ()
@property (nonatomic, strong) dispatch_queue_t queue;
@property (nonatomic, strong) NSString *nameStr;
@end
下面的代码可以正常执行吗?
- (void)taggedPointerDemo {
self.queue = dispatch_queue_create("com.zf.cn", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i<10000; i++) {
dispatch_async(self.queue, ^{
self.nameStr = [NSString stringWithFormat:@"zf我是谁的谁是我的我是谁的谁"];
NSLog(@"%@",self.nameStr);
});
}
}
答案是不能正常执行,会出现野指针崩溃。原因nameStr属性是通过nonatomic
修饰的,是线程不安全的。
setter方法的实现中存在旧值得释放和新值的retain或者copy。如果不加锁进行保护的话,多线程可能会对旧值release多次,从而出现野指针。我们只需要将
nonatomic
换成atomic
就可以解决。
如果将以上代码中的self.name的赋值改为下面这样
self.nameStr = [NSString stringWithFormat:@"zf"];
然后再次运行就不会崩溃了。为什么换了一个值就会不一样呢?我们通过断点来看下具体的区别
可以看出区别就是,如果字符比较短,那么nameStr就是
NSTaggedPointerString
,否则就是_NSCFString
。我们查看objc的源码找到
objc_release
函数实现。
void
objc_release(id obj)
{
if (!obj) return;
if (obj->isTaggedPointer()) return;
return obj->release();
}
可以看到release方法会判断是否为taggedPointer,如果为true直接就返回了,不会对taggedPointer对象进行发送release消息。所以上面代码中 nameStr为taggedPointer时不会崩溃。
TaggedPointer是系统对地址的一个优化,此时TaggedPointer地址已经不再是纯粹的地址,而是表示了变量的值以及类型。下面我们来打印一下TaggedPointer变量的地址。
NSString *str1 = [NSString stringWithFormat:@"a"];
NSString *str2 = [NSString stringWithFormat:@"b"];
NSLog(@"%p-%@",str1,str1);
NSLog(@"%p-%@",str2,str2);
NSNumber *number1 = @1;
NSNumber *number2 = @1;
NSNumber *number3 = @2.0;
NSNumber *number4 = @3.2;
NSLog(@"%@-%p-%@", object_getClass(number1), number1, number1);
NSLog(@"%@-%p-%@", object_getClass(number2), number2, number2);
NSLog(@"%@-%p-%@", object_getClass(number3), number3, number3);
NSLog(@"%@-%p-%@", object_getClass(number4), number4, number4);
打印的结果如下:
taggedPointer[62681:1674928] 0xef3a6d66db722165-a
taggedPointer[62681:1674928] 0xef3a6d66db722155-b
taggedPointer[62681:1674928] __NSCFNumber-0xff3a6d66db722766-1
taggedPointer[62681:1674928] __NSCFNumber-0xff3a6d66db722766-1
taggedPointer[62681:1674928] __NSCFNumber-0xff3a6d66db722751-2
taggedPointer[62681:1674928] __NSCFNumber-0x6000038d8760-3.2
但是打印结果的地址有点看不懂,看不到地址中能包含变量的值。我们去objc源码中追究一下:
在_read_images
中调用了下面的TaggedPointer的初始化方法
static void
initializeTaggedPointerObfuscator(void)
{
if (sdkIsOlderThan(10_14, 12_0, 12_0, 5_0, 3_0) ||
// Set the obfuscator to zero for apps linked against older SDKs,
// in case they're relying on the tagged pointer representation.
DisableTaggedPointerObfuscation) {
objc_debug_taggedpointer_obfuscator = 0;
} else {
// Pull random data into the variable, then shift away all non-payload bits.
arc4random_buf(&objc_debug_taggedpointer_obfuscator,
sizeof(objc_debug_taggedpointer_obfuscator));
objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
}
}
我们看到在新的系统中objc_debug_taggedpointer_obfuscator
通过mask被赋予了值。在_objc_encodeTaggedPointer
和_objc_decodeTaggedPointer
方法有使用到了这个值,和TaggedPoint对象的地址进行了与操作。
static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr);
}
static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}
这下我们应该明白了,TaggedPoint变量的地址通过objc_debug_taggedpointer_obfuscator
进行了encode,所以才无法通过地址直接看到值。我们只需要将变量的地址decode一下。
首先模仿系统的decode方法:
uintptr_t
_objc_decodeTaggedPointer_(id ptr)
{
return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}
然后调用decode方法对变量指针decode:
NSString *str2 = [NSString stringWithFormat:@"b"];
NSLog(@"0x%lx",_objc_decodeTaggedPointer_(str2));
NSNumber *number1 = @1;
NSNumber *number2 = @1;
NSNumber *number3 = @2.0;
NSNumber *number4 = @3.2;
NSLog(@"%p-%@ - 0x%lx",number1,number1,_objc_decodeTaggedPointer_(number1));
NSLog(@"0x%lx",_objc_decodeTaggedPointer_(number2));
NSLog(@"0x%lx",_objc_decodeTaggedPointer_(number3));
NSLog(@"0x%lx",_objc_decodeTaggedPointer_(number4));
打印结果如下:
taggedPointer[62961:1688303] 0xa000000000000621
taggedPointer[62961:1688303] 0xb000ee1dc1cb13c7-1 - 0xb000000000000012
taggedPointer[62961:1688303] 0xb000000000000012
taggedPointer[62961:1688303] 0xb000000000000025
taggedPointer[62961:1688303] 0x8e1dc1aca9f5
可以看到str的指针为 0xa000000000000621
。0xa代表字符串
number1的指针为0xb000000000000012
。0xb代表number类型,其中右边第二位代表值。
retain()
retain操作会走下面的
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
方法。
在此方法中首先判断是否为taggedPointer
if (isTaggedPointer()) return (id)this;
如果不是taggedPointer的话,引用计数存在散列表中,然后通过sidetable_retain
进行retain操作。
// 散列表的引用计数表 进行处理 ++
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();
}
sidetable_retain()
的定义如下:
id
objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
ASSERT(!isa.nonpointer);
#endif
SideTable& table = SideTables()[this];
table.lock();
size_t& refcntStorage = table.refcnts[this];
if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
refcntStorage += SIDE_TABLE_RC_ONE;
}
table.unlock();
return (id)this;
}
通过SideTables()[this]
获取到一个SideTables,可见,散列表是个集合,这是因为在进行散列表的操作的时候,需要保证效率和安全,如果全部对象都用一个散列表,那么查询和操作的速度肯定比较低。如果分步在多个散列表中,就可以提高效率,以及安全度。
引用计数加上SIDE_TABLE_RC_ONE
,我们可以找到SIDE_TABLE_RC_ONE
的定义:
#define SIDE_TABLE_WEAKLY_REFERENCED (1UL<<0)
#define SIDE_TABLE_DEALLOCATING (1UL<<1) // MSB-ward of weak bit
#define SIDE_TABLE_RC_ONE (1UL<<2) // MSB-ward of deallocating bit
可见,SIDE_TABLE_RC_ONE
是左移了两位,为什么左移两位呢?因为左边的第一位表示的是SIDE_TABLE_WEAKLY_REFERENCED
,也就是weak表;左边第二位是SIDE_TABLE_DEALLOCATING
,也就是析构表。
SideTable
的结构如下,主要包含锁、引用计数表、weak表。
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
.......
}
我们继续回到rootRetain
方法,看下如果是nonpointer的情况:
uintptr_t carry;
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++
nonponter的引用计数首先选择存在isa.bits中,那么其中的carry是干什么的呢?
carry的作用是:如果因为引用计数太大,isa中装不下了,那么会将引用计数的一半放到isa中,另外一个放到散列表中。
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;
}
if (slowpath(transcribeToSideTable)) {
// Copy the other half of the retain counts to the side table.
sidetable_addExtraRC_nolock(RC_HALF);
}
release()的过程相当于将retain()的反过来。
retainCount
我们都知道下面代码的打印结果是1,那么objc对象的存储的真正的引用计数是多少呢?
NSObject *objc = [NSObject alloc];
NSLog(@"%ld",objc.retainCount);
我们看下objc的源码,retainCount调用了_objc_rootRetainCount。
- (NSUInteger)retainCount {
return _objc_rootRetainCount(self);
}
_objc_rootRetainCount又调用了objc的rootRetainCount。
uintptr_t
_objc_rootRetainCount(id obj)
{
ASSERT(obj);
return obj->rootRetainCount();
}
然后来到rootRetainCount函数:
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();
}
如果是nonpointer,就会返回1+bits.extra_rc。所以虽然外面打印了1,但是bits.extra_rc实际上为0,只不过返回retainCount的时候默认加了1。所以我们说alloc出来的对象实际上的引用计数为0,我们外面打印的为1,是因为默认加1。
dealloc方法
我们来探究下dealloc方法里面做了些什么呢?
dealloc
方法调用了_objc_rootDealloc
,_objc_rootDealloc
调用了rootDealloc
:
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
函数。
static id
_object_dispose(id anObject)
{
if (anObject==nil) return nil;
objc_destructInstance(anObject);
anObject->initIsa(_objc_getFreedObjectClass ());
free(anObject);
return nil;
}
函数_object_dispose
在free之前调用了objc_destructInstance
。我们看下函数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;
}
1、移除cxx;
2、移除关联对象;
3、调用了clearDeallocating
;
下面我们看下clearDeallocating
的实现:
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());
}
如果不是nonpointer,清理散列表,也就是清理引用计数表和weak表;否则的话执行clearDeallocating_slow
。
NEVER_INLINE void
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();
}
同样是清理引用计数表和weak表。
总结:dealloc主要实现的内容:
1、移除cxx;
2、移除关联对象;
3、清理引用计数表和weak表;
4、free对象;
Timer的引用无法释放问题
ViewController中的下面代码会导致什么问题?
@interface TimerViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation TimerViewController
- (void)viewDidLoad {
self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
// 加runloop
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}
@end
当然是viewController无法释放。是怎么样的一种持有关系导致ViewController无法释放的呢?
因为timer依赖于RunLoop,需要加入到RunLoop中才能执行,而RunLoop又是全局的。所以RunLoop持有timer。通过timer的文档我们知道timer是强持有了target,也就是timer持有了self。所以timer释放不了,self也无法释放。
持有关系是RunLoop-->timer-->self。
我们再block的时候为了打断引用关系,使用了weakSelf的方式。那么这里能不能也使用weakSelf呢?
__weak typeof(self) weakSelf = self; // weak
self.timer = [NSTimer timerWithTimeInterval:1 target:weakSelf selector:@selector(fireHome) userInfo:nil repeats:YES];
// 加runloop
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
答案是依旧会无法释放。首先我们看解释一下weakSelf的作用:
weakSelf和self都指向同一个对象的两个不同的指针,也就是说它们指向的对象是同一个,但是它们的指针地址不一样。weakSelf虽然不会引起指向对象引用计数的增加,但是如果你强持有weakSelf,就相当于强持有了weakSelf所指向的对象。也就是虽然weakSelf不会导致self的引用计数加1,但是如果其他的对象强持有weakSelf,也就相当于强持有了self,从而会导致self的引用计数加1。例如下面的打印结果是什么呢?
NSObject *obj = [NSObject alloc];
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)obj));
__weak NSObject *weakObjc = obj;
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)obj));
NSObject *newWObjc = weakObjc;
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)obj));
打印结果为"1 1 2"。
但是为什么block那么传weakSelf就不会导致循环应用了呢?因为block的实现方法_Block_object_assign
:
void _Block_object_assign(void *destArg, const void *object, const int flags) {
switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
case BLOCK_FIELD_IS_OBJECT:
//里面什么都没做
_Block_retain_object(object);
*dest = object;
break;
......
我们看到指向object是*dest。也就是说dest保存的是object的指针。如果此处传进来的是weakSelf的话,dest指向的就是weakSelf的指针,而不是self对象。所以此处不会对weakSelf所指向的对象产生影响。也就是说weakSelf指向的对象是self,dest指向的对象是weakSelf的指针。如果此处是dest=object
,那么weakSelf也不能解决循环引用问题了。
解决Timer无法释放的问题
我们要解决上面提交的Timer无法释放的问题,那么就需要在合适的时机将timer调用invalidate并且置为nil。
方法一、didMoveToParentViewController
- (void)didMoveToParentViewController:(UIViewController *)parent{
if (parent == nil) {
[self.timer invalidate];
self.timer = nil;
}
}
在上面方法中判断parent == nil,也就是当pop的时候parent就为nil。所以可以在此处将timer销毁,那么ViewController的dealloc方法也就能正常执行了。
方法二、
使用中介者模,创建一个其他的对象,然后将timer的target指向这个中间者对象,给中间者对象添加方法,在方法的实现函数中处理timer的事件。
self.target = [[NSObject alloc] init];
class_addMethod([NSObject class], @selector(fireHome), (IMP)fireHomeObjc, "v@:");
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.target selector:@selector(fireHome) userInfo:nil repeats:YES];
然后timer会一直调用下面的fireHomeObjc
函数。
void fireHomeObjc(id obj){
NSLog(@"%s -- %@",__func__,obj);
}
- (void)fireHome{
num++;
NSLog(@"hello word - %d",num);
}
然后ViewController 返回的时候就会正常的调用dealloc方法了,只需要在dealloc方法中将timer销毁就可以了。
- (void)dealloc{
[self.timer invalidate];
self.timer = nil;
}
方式三、
方法三和方法二类似,都是使用中介者模式,只不过将方法二进行封装成了一个工具类TimerWapper
,该工具类的初始化方法如下:
- (instancetype)zf_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo{
if (self == [super init]) {
self.target = aTarget; // vc
self.aSelector = aSelector; // 方法 -- vc 释放
if ([self.target respondsToSelector:self.aSelector]) {
self.timer = [NSTimer scheduledTimerWithTimeInterval:ti target:self selector:@selector(timerFire) userInfo:userInfo repeats:yesOrNo];
}
}
return self;
}
注意target需要使用weak来修饰,防止出现循环引用
@property (nonatomic, weak) id target;
然后在timerFire中使用发送消息的方式调用ViewController的方法,判断当target不存在的时候,要及时销毁到timer。这样的在ViewController中也不需要处理timer的销毁了,全部都封装到工具类了,外面只需要调用工具类的方法就可。
- (void)timerFire {
if (self.target) {
void (*zf_msgSend)(void *,SEL, id) = (void *)objc_msgSend;
zf_msgSend((__bridge void *)(self.target), self.aSelector, self.timer);
} else {
[self.timer invalidate];
self.timer = nil;
}
}
方法四、使用NSProxy
NSProxy是个虚拟类,我们需要写个子类集成于它,他的特点是可以将自己未实现的方法转给其他对象去实现。自定义Proxy的实现如下:
#import "ZFProxy.h"
@interface ZFProxy()
@property (nonatomic, weak) id object;
@end
@implementation ZFProxy
+ (instancetype)proxyWithTransformObject:(id)object{
ZFProxy *proxy = [ZFProxy alloc];
proxy.object = object;
return proxy;
}
// 转移
-(id)forwardingTargetForSelector:(SEL)aSelector {
return self.object;
}
@end
我们在外面使用:
self.proxy = [ZFProxy proxyWithTransformObject:self];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(fireHome) userInfo:nil repeats:YES];
在ViewController中创建proxy,然后创建timer,将timer的target设置为proxy,但是proxy没有实现对象的方法,然后将方法的实现者重新指向self,最终会调用self的firHome方法。这样同样也不会导致self释放不了的问题了,因为timer强持有的是proxy。注意这个方法依然需要在self的dealloc方法中移除timer。