一、内存布局
4G内存一般只用到3G,还有1G给内核用,还有一部分区域给保留区,用于保留字段,为啥是3G呢,因为内核去内存地址是从0xC0000000,把他转换成10进制就是310241024*1024,就是3GB大小
1、栈区:
函数、方法、变量,一般为0X7开头,从上图我看出来栈是向下增长,堆区是向上增长,并且栈区大小比较小,但是速度很快,因为栈区是通过寄存器直接访问
因为栈区存储的是方法的地址,所以方法一般存储在栈区
2、堆区:
通过alloc分配的对象,block的Copy,一般为0x6开头,一般速度比较慢,这是因为访问堆区内存时候一般是要通过先从指针中督导栈区的内存地址,然后再到堆区去访问
从图中我们看到堆区是0x6开头,堆区是以0x7开头
3、BSS段:
未初始化的全局变量,静态变量,一般为0x1开头
4、数据段.data:
初始化的全局变量,静态变量,一般为0x1开头
5、text:
代码段,
6、全局变量和局部变量
全局变量存储在相应的全局储存区,局部变量在栈区,
定义全局变量:
问题一: block是否可以直接修改全局变量?
[UIView animateWithDuration:1 animations:^{
lgname = @"KC";
}];
答案:可以,因为全局变量作用空间很大
问题二:
LGPerson.h文件中定义一个静态变量
static int personNum = 100;
NS_ASSUME_NONNULL_BEGIN
@interface LGPerson : NSObject
- (void)run;
+ (void)eat;
@end
@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 @"";
}
然后我们在其他类中运行下面代码:
NSLog(@"************静态区安全测试************");
// 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-03-19 15:26:35.981841+0800 001---五大区Demo[36035:17676321] vc:0x10e428484--100
2020-03-19 15:26:35.982291+0800 001---五大区Demo[36035:17676321] vc:0x10e428484--10000
2020-03-19 15:26:35.982801+0800 001---五大区Demo[36035:17676321] LGPerson内部:-0x10e428460--101
2020-03-19 15:26:35.983590+0800 001---五大区Demo[36035:17676321] vc:0x10e428484--10000
2020-03-19 15:26:35.983961+0800 001---五大区Demo[36035:17676321] LGPerson内部:LGPerson-0x10e428460--102
2020-03-19 15:26:35.984398+0800 001---五大区Demo[36035:17676321] vc:0x10e428484--10000
2020-03-19 15:26:35.984869+0800 001---五大区Demo[36035:17676321] LGPerson内部:-0x10e428488--100
为什么是这样结果呢,因为对于静态全局变量如果定义了,在任何一个文件中引用,就会在相应文件中生成一个静态变量,所以不同文件的静态变量不是同一个,并且类和分类也算两个文件
假如想全局都是一个变量,那么就在static前面加上一个extern,这样就可以多个文件访问同一个变量
二、内存管理方案
1、TaggedPoint:小对象-NSNumber,NSDate
面试题1
//MARK: - taggedPointer 面试题
- (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);
});
}
}
上面代码,APP启动时候执行taggedPointerDemo方法,然后点击屏幕,程序是否崩溃,如果不点击呢
答案:
点击的话会崩溃~,不点击的话不会崩溃
第一点因为多线程,第二点执行了get和set方法
我们打开汇编,然后发现崩在objc_release函数里面
我们知道在set方法时候,系统是对新值retain,然后对旧值release。因为是多线程,所以release和retain可能在多个线程操作,就有可能一个对象还没release完成,又有一个线程对该对象进行release,这样就可能会造成对一个引用计数器为0的对象多次release造成崩溃
但是为啥不点击就不会崩溃呢,比如taggedPointerDemo也是多线程操作的,我们在taggedPointerDemo里面加一个断点,然后看一下 self.nameStr是什么
我们发现nameStr的类型是NSTaggedPointString类型
我们再看一下下面代码的nameStr是什么类型
我们发现这里是NSCFString类型,为啥两个是不一样的呢?
我们看一下objc_release的源码
void
objc_release(id obj)
{
if (!obj) return;
if (obj->isTaggedPointer()) return;
return obj->release();
}
我们发现进行release之前会判断一下isTaggedPointer,也就是如果是就不会release,也就是说对于TaggedPointer对象不会做release操作,所以taggedPointerDemo里面不会崩溃,而下面代码需要做release操作,所以在多线程中会崩溃
我们知道对于对象,我们一般都是一个指针,指针里面保存这地址,但是TaggedPointer对象里面不是简单的指针,我们做一个实验:
NSString *str1 = [NSString stringWithFormat:@"aaaaaaaaa"];
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);
打印:
2020-03-19 18:17:30.538962+0800 002---taggedPointer[14010:3206189] 0x9ed5adeddd03ac9d-aaaaaaaaa
2020-03-19 18:17:30.539038+0800 002---taggedPointer[14010:3206189] 0x9e57a5cd5f0b8a35-b
2020-03-19 18:17:30.539091+0800 002---taggedPointer[14010:3206189] __NSCFNumber-0x8e57a5cd5f0b8c06-1
2020-03-19 18:17:30.539113+0800 002---taggedPointer[14010:3206189] __NSCFNumber-0x8e57a5cd5f0b8c06-1
2020-03-19 18:17:30.539128+0800 002---taggedPointer[14010:3206189] __NSCFNumber-0x8e57a5cd5f0b8c31-2
2020-03-19 18:17:30.539145+0800 002---taggedPointer[14010:3206189] __NSCFNumber-0x283f70220-3.2
我们发现上面对象的指针后面都跟上了相应对象的值
我们到objc源码的_read_images方法里面有一个initializeTaggedPointerObfuscator()
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;
}
}
在iOS10.14之前objc_debug_taggedpointer_obfuscator为0,之后的话,objc_debug_taggedpointer_obfuscator得到一个随机数,然后进行了一个和_OBJC_TAG_MASK进行&操作,然后我们全局搜一下objc_debug_taggedpointer_obfuscator干了啥
static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}
static inline bool
_objc_taggedPointersEnabled(void)
{
extern uintptr_t objc_debug_taggedpointer_mask;
return (objc_debug_taggedpointer_mask != 0);
}
根据函数名,我们知道这是一个编码和解码的操作,但是我们发现编码和解码都是对传进来的ptr进行^操作,这是因为一个数^同一个数两次,那么结果又变成原来了,例如
1000 0001
^0001 1000
=1001 1001
^0001 1000
=1000 0001
既然这样操作,我们尝试对上面编码进行解码操作,引入:
extern uintptr_t objc_debug_taggedpointer_obfuscator;
定义函数:
uintptr_t
_objc_decodeTaggedPointer_(id ptr)
{
return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}
然后对上面的NSnumber进行一下解码操作
NSString *str1 = [NSString stringWithFormat:@"aaaaaaaaa"];
NSString *str2 = [NSString stringWithFormat:@"b"];
// NSLog(@"%p - %@",str1,str1);
// NSLog(@"%p - %@",str2,str2);
NSLog(@"str1:0x%lx",_objc_decodeTaggedPointer_(str1));
NSLog(@"str2:0x%lx",_objc_decodeTaggedPointer_(str2));
NSNumber *number1 = @1;
NSNumber *number2 = @2;
NSNumber *number3 = @2.0;
NSNumber *number4 = @3.2;
NSLog(@"number1:0x%lx",_objc_decodeTaggedPointer_(number1));
NSLog(@"number2:0x%lx",_objc_decodeTaggedPointer_(number2));
NSLog(@"number3:0x%lx",_objc_decodeTaggedPointer_(number3));
NSLog(@"number4:0x%lx",_objc_decodeTaggedPointer_(number4));
打印:
2020-03-19 19:10:25.416204+0800 002---taggedPointer[14097:3228538] str1:0xa082082082082089
2020-03-19 19:10:25.416272+0800 002---taggedPointer[14097:3228538] str2:0xa000000000000621
2020-03-19 19:10:25.416299+0800 002---taggedPointer[14097:3228538] number1:0xb000000000000012
2020-03-19 19:10:25.416316+0800 002---taggedPointer[14097:3228538] number2:0xb000000000000022
2020-03-19 19:10:25.416334+0800 002---taggedPointer[14097:3228538] number3:0xb000000000000025
2020-03-19 19:10:25.416347+0800 002---taggedPointer[14097:3228538] number4:0x46c632356a2fcf06
我们发现number1和number2的倒数第二位正好是他本身的值,前面0xb代表的意思数组int类型
所以对于taggedpointer对象,它的值是保存在指针里面,而不是在堆区,这样既节省了内存空间,因为不需要retain和release,所以也让访问的速度大大加快,据说访问taggedpointer对象是正常对象的速度的3倍
而什么样的对象是在taggedpointer呢,一般8到10位以下的字符串可以,并且英文和汉字长度是不一样的
2、nonpointer_isa:非指针isa
我们知道isa是一个8位指针,而8*8是64个字节,假如只存一个指针很浪费,所以存了其他信息
三、ARC&MRC
我们知道OC里面对对象的管理是通过引用计数器,那么这个引用计数器是如何管理的呢?
我们先看一下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;
do {
transcribeToSideTable = false;
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa.bits);
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,如果不是那么那引用计数器就存在散列表里面,我们看下散列表结构:
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
void SideTableLockAll() {
SideTables().lockAll();
}
也就是把所有散列表都放一起,这是为什么呢,
因为我们在访问表的时候都是需要加锁解锁,假如将所有的对象的引用计数都放一个表里,那一旦一个对象访问该表就能访问到其他对象的引用计数,这样很不安全,并且多个对象访问一个表也很影响性能,据了解系统总共能开64张表
散列表一般都是一个哈希表,为啥采用哈希,不采用列表或数组呢
- 列表:
因为列表都有一个指向,如果插入的话只需把相应表的指针更改一下即可,删除也类似,但是查询的话就需要从头到尾一个个去查询,这样效率很慢
- 数组:
查询只需通过下标即可,但是插入的话比较慢,插入的时候需要判断数组大小够不够,如果不够需要重新开辟空间,然后将原来数据整体移动,这样就比较耗性能
所以对于SideTables的结构,本身是一个哈希表,然后结合链表的结构来存储不同对象引用计数
再看源码
然后会调用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;
}
我们看一下SIDE_TABLE_RC_ONE
发现是对1UL左移两位,因为前面两位不是引用相关的,根据图我们知道第一位是weak表,第二位是是否在dealloc,所以要左移两位
上面是对于非nonpointer对象的操作,再往下就是对nonpointer对象的操作,我们发现就是对bits里面的第45开始的数据进行加减操作,也就是isa的extra_rc,carry的意思就是如果溢出了,就要单独对表操作将isa 的extra_rc的一半存储在extra_rc,另一半存在在引用计数表里
为啥对extra_rc操作呢,因为对extra_rc操作速度比较快,要是对引用计数表操作,是需要加锁解锁操作性能比较差
# define RC_ONE (1ULL<<45)
四、引用计数--retainCount
NSObject *objc = [NSObject alloc];
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)objc));
打印是1
alloc出来的对象引用计数为0,因为在alloc方法里面没有对引用计数操作,为啥打印是1呢,我们看一下retainCount的源码,
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();
}
我们发现获取retainCount时候是对extra_rc和散列表里面的count加起来后在加1,然后返回的,所以alloc出来的虽然引用计数为0,但是为了不给程序员误解,所以都单独加了个1
我们在看一下dealloc:
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);
}
}
dealloc前的操作,散列表情况,weak、cxx、关联对象、isa 64的数据
id
object_dispose(id obj)
{
if (!obj) return nil;
// weak
// cxx
// 关联对象
// ISA 64
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);
obj->clearDeallocating();
}
return obj;
}
void object_cxxDestruct(id obj)
{
if (!obj) return;
if (obj->isTaggedPointer()) return;
object_cxxDestructFromClass(obj, obj->ISA());
}
//清空关联对象
void
_object_remove_assocations(id object)
{
ObjectAssociationMap refs{};
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.get());
AssociationsHashMap::iterator i = associations.find((objc_object *)object);
if (i != associations.end()) {
refs.swap(i->second);
associations.erase(i);
}
}
// release everything (outside of the lock).
for (auto &i: refs) {
i.second.releaseHeldValue();
}
}
具体流程:
五、循环引用中的弱引用
timer和block造成循环引用的区别
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));
// self -> timer -weakSelf-> self
__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];
// runloop -> timer -> self
// self -> timer -> self
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));
打印:
2020-03-20 11:27:50.365812+0800 003-强引用[15407:3487193] 8
2020-03-20 11:27:50.366002+0800 003-强引用[15407:3487193] 9
当我们在控制器中执行一个timer,这样会造成循环引用,哪怕给timer传进去的是self也不行,这是为什么呢?
因为timer需要依赖runloop才能运行,所以runloop会强拥有timer,而通过上面打印我们发现timer也强拥有了WeakSelf,虽然weakSelf是弱指针,但是timer强引用的是weakSelf指向的对象,所以会造成self的引用计数器加一了,这就造成timer释放不了的话,那self也释放不了,但是为什么block用weakSelf就能避免循环引用呢?
我们来看看block如何持有对象的
其中_Block_retain_object没有对object做任何操作,并且dest拿到的是object对象的指针地址,操作的是object这个指针的地址,而不是object这个对象,也就是说block里面持有的是传进来的object这个指针,而不是object指针指向的对象,所以如果传进来的是弱指针weakSelf的话,及时weakself一直存在也不会对weakSelf指向的对象造成影响,而如果是强指针,那么如果这个强指针不注销的话,那么这个强指针指向的对象就不会release,所以会造成循环引用
这就是timer和block造成循环引用的区别
如何处理timer的循环引用
对于上面那种情况,怎么破除循环引用呢
第一种方式:
在合适的地方调用下面代码
[self.timer invalidate];
self.timer = nil;
下面代码就让timer从runloop中释放出来了,并且置为空,让timer不会再强持有self
第二种:
用中间者形式:
self.timerWapper = [[LGTimerWapper alloc] lg_initWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
#import "LGTimerWapper.h"
#import <objc/message.h>
@interface LGTimerWapper()
@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL aSelector;
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation LGTimerWapper
- (instancetype)lg_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]) {
Method method = class_getInstanceMethod([self.target class], aSelector);
const char *type = method_getTypeEncoding(method);
class_addMethod([self class], aSelector, (IMP)fireHomeWapper, type);
// 难点: lgtimer
// 无法响应
// 时间点: timer invalid
// vc -> lgtimerwarpper
// runloop -> timer -> lgtimerwarpper
self.timer = [NSTimer scheduledTimerWithTimeInterval:ti target:self selector:aSelector userInfo:userInfo repeats:yesOrNo];
}
}
return self;
}
// 一直跑 runloop
void fireHomeWapper(LGTimerWapper *warpper){
if (warpper.target) {
void (*lg_msgSend)(void *,SEL, id) = (void *)objc_msgSend;
lg_msgSend((__bridge void *)(warpper.target), warpper.aSelector,warpper.timer);
}else{ // warpper.target
[warpper.timer invalidate];
warpper.timer = nil;
}
}
- (void)lg_invalidate{
[self.timer invalidate];
self.timer = nil;
}
- (void)dealloc{
NSLog(@"%s",__func__);
}
也就是给self一个类型为LGTimerWapper的属性,让属性作为target传到timer里面,LGTimerWapper弱引用self,然后在LGTimerWapper内部让self去执行方法,当self被注销后,在LGTimerWapper内部让timer进行处理
第三种:LGProxy,虚基类方式
self.proxy = [LGProxy proxyWithTransformObject:self];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(fireHome) userInfo:nil repeats:YES];
#import "LGProxy.h"
@interface LGProxy()
@property (nonatomic, weak) id object;
@end
@implementation LGProxy
+ (instancetype)proxyWithTransformObject:(id)object{
LGProxy *proxy = [LGProxy alloc];
proxy.object = object;
return proxy;
}
// 仅仅添加了weak类型的属性还不够,为了保证中间件能够响应外部self的事件,需要通过消息转发机制,让实际的响应target还是外部self,这一步至关重要,主要涉及到runtime的消息机制。
// 转移
-(id)forwardingTargetForSelector:(SEL)aSelector {
return self.object;
}
//// sel - imp -
//// 消息转发 self.object
//- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
//
// if (self.object) {
// }else{
// NSLog(@"麻烦收集 stack111");
// }
// return [self.object methodSignatureForSelector:sel];
//
//}
//
//- (void)forwardInvocation:(NSInvocation *)invocation{
//
// if (self.object) {
// [invocation invokeWithTarget:self.object];
// }else{
// NSLog(@"麻烦收集 stack");
// }
//
//}
这个就是通过消息转发机制,然后self去执行方法,但是timer却不持有他