先上一內存佈局圖 c++
其餘面試
地址開頭數組
答:有。全局變量存在全局區(bbs區/data區),局部變量存在棧區安全
static NSString *K_Name = @"lala";
- (void)test {
[UIView animateWithDuration:1 animations:^{
K_Name = @"dingding";
}];
}
複製代碼
答:能夠,全局變量能夠全局訪問bash
##### Person
//注意personNum是定義在Person.h文件中的靜態變量
static int personNum = 100;
@interface Person : NSObject
- (void)run;
+ (void)eat;
@end
@implementation Person
- (void)run{
personNum ++;
NSLog(@"Person內部:%@-%p--%d",self,&personNum,personNum);
}
+ (void)eat{
personNum ++;
NSLog(@"Person內部:%@-%p--%d",self,&personNum,personNum);
}
@end
#### 分類 Person (ca)
#import "Person.h"
@interface Person (ca)
- (void)cate_method;
@end
@implementation Person (ca)
- (void)cate_method{
NSLog(@"Person內部:%@-%p--%d",self,&personNum,personNum);
}
@end
複製代碼
題目數據結構
#### ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"vc:%p--%d",&personNum,personNum); // 100
personNum = 10000;
NSLog(@"vc:%p--%d",&personNum,personNum); // 10000
[[Person new] run]; // 100 + 1 = 101
NSLog(@"vc:%p--%d",&personNum,personNum); // 10000
[Person eat]; // 102
NSLog(@"vc:%p--%d",&personNum,personNum); // 10000
[[Person alloc] cate_method];
}
複製代碼
打印:多線程
vc:0x102730484--100
vc:0x102730484--10000
Person內部:-0x102730460--101
vc:0x102730484--10000
Person內部:LGPerson-0x102730460--102
vc:0x102730484--10000
Person內部:-0x102730488--100
複製代碼
答:首先static變量時能夠邊修改的;static變量的做用域與對象、類、分類不要緊,只與文件有關係;app
apple在內存管理方面提供了三種方案(TaggetPointer、NONPOINTER_ISA、散列表),嚴謹的說應該是三種方案共同管理內存。less
首先理解一個操做:對同一個數值^(異或)操做兩次,獲得的仍是原來的數值。eg:async
1000 0001
^ 0001 1000
------------
1001 1001
1001 1001
^ 0001 1000
------------
1000 0001
複製代碼
下面是TaggedPointer 對象的源碼,能夠看到,也是對同一個數值(objc_debug_taggedpointer_obfuscator)進行^異或操做
// 存值
static inline void * _Nonnull
_objc_makeTaggedPointer(objc_tag_index_t tag, uintptr_t value) {
//objc_tag_index_t 是一個枚舉值 用於標定不一樣tag類型,根據不一樣的類型的tag值進行不一樣的左右移和MASK操做
if (tag <= OBJC_TAG_Last60BitPayload) {
//將類型tag和值Value進行打包(左右移和MASK操做),獲得一個result
uintptr_t result =
(_OBJC_TAG_MASK |
((uintptr_t)tag << _OBJC_TAG_INDEX_SHIFT) |
((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT));
// 用 result進行encode ^ 或操做,返回一個指針
return _objc_encodeTaggedPointer(result);
} else {//下面操做同樣
uintptr_t result =
(_OBJC_TAG_EXT_MASK |
((uintptr_t)(tag - OBJC_TAG_First52BitPayload) << _OBJC_TAG_EXT_INDEX_SHIFT) |
((value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_LSHIFT));
return _objc_encodeTaggedPointer(result);
}
}
//取值
static inline uintptr_t
_objc_getTaggedPointerValue(const void * _Nullable ptr) {
//將指針ptr先decode ^ 操做取出value(就是上面的result)
uintptr_t value = _objc_decodeTaggedPointer(ptr);
//value解包(左右移和MASK操做)獲得value
uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;
if (basicTag == _OBJC_TAG_INDEX_MASK) {
return (value << _OBJC_TAG_EXT_PAYLOAD_LSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_RSHIFT;
} else {
return (value << _OBJC_TAG_PAYLOAD_LSHIFT) >> _OBJC_TAG_PAYLOAD_RSHIFT;
}
}
extern uintptr_t objc_debug_taggedpointer_obfuscator;
//encode ^ 操做
static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr) {
return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr);
}
//decode ^ 操做
static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr) {
return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}
//objc_tag_index_t 是一個枚舉值 用於標定不一樣tag類型
enum objc_tag_index_t : uint16_t {
// 60-bit payloads
OBJC_TAG_NSAtom = 0,
OBJC_TAG_1 = 1,
OBJC_TAG_NSString = 2, //NSString類的tag爲2
OBJC_TAG_NSNumber = 3, //NSNumber類的tag爲2
OBJC_TAG_NSIndexPath = 4,
OBJC_TAG_NSManagedObjectID = 5,
OBJC_TAG_NSDate = 6,
// 60-bit reserved
OBJC_TAG_RESERVED_7 = 7,
// 52-bit payloads
OBJC_TAG_Photos_1 = 8,
OBJC_TAG_Photos_2 = 9,
OBJC_TAG_Photos_3 = 10,
OBJC_TAG_Photos_4 = 11,
OBJC_TAG_XPC_1 = 12,
OBJC_TAG_XPC_2 = 13,
OBJC_TAG_XPC_3 = 14,
OBJC_TAG_XPC_4 = 15,
OBJC_TAG_NSColor = 16,
OBJC_TAG_UIColor = 17,
OBJC_TAG_CGColor = 18,
OBJC_TAG_NSIndexSet = 19,
OBJC_TAG_First60BitPayload = 0,
OBJC_TAG_Last60BitPayload = 6,
OBJC_TAG_First52BitPayload = 8,
OBJC_TAG_Last52BitPayload = 263,
OBJC_TAG_RESERVED_264 = 264
};
複製代碼
源碼結論
TaggedPointer對象
在存值時,先將對象類型tag
和值value
打包(左右移和MASK操做)處理獲得一個result
,而後result
和一個隨機的常量值objc_debug_taggedpointer_obfuscator
進行^異或
操做返回一個包裝過的TaggedPointer指針
。TaggedPointer指針
和一個隨機的常量值objc_debug_taggedpointer_obfuscator
進行^異或
操做返回result
,而後result
解包獲得值value
。//MARK: - taggedPointer 面試題
@property (nonatomic, strong) NSString *nameStr;
- (void)taggedPointer_NOCrash {
dispatch_queue_t queue = dispatch_queue_create("com.fun.cn", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i<10000; i++) {
dispatch_async(queue, ^{
self.nameStr = [NSString stringWithFormat:@"fun"];
NSLog(@"%@",self.nameStr);
});
}
}
- (void)taggedPointer_Crash {
dispatch_queue_t queue = dispatch_queue_create("com.fun1.cn", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i<10000; i++) {
dispatch_async(queue, ^{
self.nameStr = [NSString stringWithFormat:@"fun 好好學習.每天向上。發展體育運動,加強國民體質"];
NSLog(@"%@",self.nameStr);
});
}
}
複製代碼
答:首先 這兩句代碼self.nameStr = [NSString stringWithFormat:@"fun 好好學習.每天向上。發展體育運動,加強國民體質"]; NSLog(@"%@",self.nameStr);
表明着get/set
方法;對象的get/set
方法內部操做就是先新值 retian,後舊值 release
;讓咱們看下retian/release
源碼實現;
void
objc_storeStrong(id *location, id obj) {
id prev = *location;
if (obj == prev) {
return;
}
objc_retain(obj);
*location = obj;
objc_release(prev);
}
__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();
}
複製代碼
很明顯正常對象的get/set
方法內部操做就是先新值 retian,後舊值 release
,在多線程訪問的狀況下,新值 retian,舊值 release
調用錯亂,致使野指針或者空,因此崩潰; 可是若是是對象是taggedPointer對象時,對象不會進行retian/release
操做,因此不會崩潰;而self.nameStr = [NSString stringWithFormat:@"fun"];
nameStr是 NSTaggedPointerString
類型;self.nameStr = [NSString stringWithFormat:@"fun 好好學習.每天向上。發展體育運動,加強國民體質"];
nameStr是——NSCFString
類型。
在最新的objc2源碼中可知萬物皆objc_object
對象,在objc_object
對象內部有一個isa
屬性;這個isa有多是純指針,也有可能除包含指針外還包含其餘信息,例如對象的引用計數、是否被弱引用...這時這個isa
就是NONPOINTER_ISA
。isa
是isa_t
類型的聯合體,其內部經過位域技術儲存不少了對象的信息。
isa_t
源碼源碼中有註釋就再也不贅述了
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
// __arm64__ defined in isa.h
//這裏把 ISA_BITFIELD 內部的宏直接寫進來了 以arm64構架說明
/*表示是否對isa開啓指針優化 。0表明是純isa指針,1表明除了地址外,還包含了類的一些信息、對象的引用計數等。*/
uintptr_t nonpointer : 1;
/*關聯對象標誌位*/
uintptr_t has_assoc : 1;
/*該對象是否有C++或Objc的析構器,若是有析構函數,則須要作一些析構的邏輯處理,若是沒有,則能夠更快的釋放對象*/
uintptr_t has_cxx_dtor : 1;
/*存在類指針的值,開啓指針優化的狀況下,arm64位中有33位來存儲類的指針*/
uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/
/*判斷當前對象是真的對象仍是一段沒有初始化的空間*/
uintptr_t magic : 6;
/*是否被指向或者曾經指向一個ARC的弱變量,沒有弱引用的對象釋放的更快*/
uintptr_t weakly_referenced : 1;
/*是否正在釋放*/
uintptr_t deallocating : 1;
/*是否有輔助的引用計數散列表*/
uintptr_t has_sidetable_rc : 1;
/*表示該對象的引用計數值,滿了就會存在sidetable 中*/
uintptr_t extra_rc : 19;
};
#endif
};
複製代碼
源碼中的extra_rc
就是用來存儲引用計數的,具體原理放到下面的引用計數部分說明。
系統維護了一張全局的Hash表,裏面存了一張張SideTable散列表,而這個散列表中就儲存了對象的引用計數以及弱引用狀況。
SideTable
內部結構
struct SideTable {
spinlock_t slock;//鎖,用於控制數據訪問安全
RefcountMap refcnts;//引用計數表s
weak_table_t weak_table;//弱引用計數表s
...
...
};
複製代碼
結構解釋
在isa_t
中extra_rc
位引用計數滿了後會把一半的引用計數放到某個散列表SideTable
中的引用計數表中;具體操做以下:
由於一個對象可能擁有多個弱應用屬性
咱們把系統維護的Hash表當成是一個男生宿舍樓
對象:學生
對象的內存地址:學生的姓名
hash表:一棟宿舍樓
SideTable:寢室
spinlock_t:寢室門上的鎖,管理學生出入
RefcountMap:某一個寢室
引用計數:寢室某個學生擁有的書本數
weak_table_t:另外一個寢室
weak_table:學生s
弱引用狀況就是學生擁有的女友數量的狀況
複製代碼
因此,不一樣的對象(學生)可能在同一個SideTable(寢室)中 -不一樣點:引用計數相似於一維數組,而弱引用狀況是一個二維數組
在上面內存管理方案中已經介紹了TaggetPointer、NONPOINTER_ISA、散列表。下面說下引用計數具體如何工做的。引用計數的核心就是對象的retain、release方法
直接看源碼,內有詳細的解釋,就再也不贅述
-(id) retain {
return _objc_rootRetain(self);
}
NEVER_INLINE id
_objc_rootRetain(id obj) {
ASSERT(obj);
return obj->rootRetain();
}
ALWAYS_INLINE id
objc_object::rootRetain() {
return rootRetain(false, false);
}
ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, bool handleOverflow) {
//判斷是不是TaggedPointer,是的話直接返回
if (isTaggedPointer()) return (id)this;
//用於記錄鎖狀態
bool sideTableLocked = false;
bool transcribeToSideTable = false;
//初始化isa_t 用於後面賦值
isa_t oldisa;
isa_t newisa;
// 真正 retain 引用計數處理
do {
transcribeToSideTable = false;
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
//是否用到了sidetable輔助處理引用計數
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;
//sidetable內部處理引用計數
else return sidetable_retain();
}
// don't check newisa.fast_rr; we already called any RR overrides //判斷對象是否正在dealloc if (slowpath(tryRetain && newisa.deallocating)) { ClearExclusive(&isa.bits); if (!tryRetain && sideTableLocked) sidetable_unlock(); return nil; } //其實就是對isa的extra_rc變量進行+1,前面說到isa會存不少東西 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. //留一半的引用計數 //準備複製另外一半引用計數到sideTable 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))); //是否將引用計數遷移到sidetable中 if (slowpath(transcribeToSideTable)) { // Copy the other half of the retain counts to the side table. //從newisa.extra_rc複製一半的引用計數到sidetable中 sidetable_addExtraRC_nolock(RC_HALF); } if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock(); return (id)this; } id objc_object::sidetable_retain() { #if SUPPORT_NONPOINTER_ISA ASSERT(!isa.nonpointer); #endif //經過this內存地址拿到對應的SideTable SideTable& table = SideTables()[this]; //加鎖 table.lock(); //又經過this內存地址拿到對象的refcntStorage(內存存有引用計數) size_t& refcntStorage = table.refcnts[this]; if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) { //引用計數 +1 refcntStorage += SIDE_TABLE_RC_ONE; } //解鎖 table.unlock(); //返回對象 return (id)this; } 複製代碼
源碼簡介 1.判斷是否是TaggedPointer對象,若是是直接返回,不作引用計數處理 2.若是是NONPOINTER_ISA對象,那就對isa.extra_rc進行+1; 3.若是isa.extra_rc滿了,就取一半複製到sideTable中輔助儲存
release方法內部就是引用引用計數減一,就再也不解釋,基本和retain差很少,本身看源碼
AutoReleasePool 是ARC引入的,用於管理對象的引用計數。
Autorelease pool implementation
翻譯:
AutoreleasePoolPage
內部結構
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)
{
}
};
複製代碼
AutoreleasePoolPage
是個繼承於AutoreleasePoolPageData
結構體的類AutoreleasePoolPage
的結構是否完整begin()
話外
AutoreleasePoolPage
最多能放504個對象指針+一個特殊指針(邊界)AutoreleasePoolPage
能放505個對象指針結構圖
示例代碼
int main(int argc, char * argv[]) {
@autoreleasepool {
}
return 0;
}
複製代碼
轉成cpp文件查看
clang -rewrite-objc main.m -o mian.cpp
複製代碼
mian.cpp。這裏只展現部分重點代碼。
struct __AtAutoreleasePool {
//構造函數
__AtAutoreleasePool() {
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
//析構函數
~__AtAutoreleasePool() {
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
void * atautoreleasepoolobj;
};
int main(int argc, char * argv[]) {
/* @autoreleasepool */ {
__AtAutoreleasePool __autoreleasepool; //__AtAutoreleasePool實例化
}
return 0;
}
複製代碼
能夠看到原來的代碼被自動加上了__AtAutoreleasePool
實例化代碼,而且調用了構造和析構函數。
這是個構造函數,繼續查看源碼
//返回一個 AutoreleasePoolPage 對象
void *
objc_autoreleasePoolPush(void) {
return AutoreleasePoolPage::push();
}
static inline void *push()
{
id *dest;
if (slowpath(DebugPoolAllocation)) {
// Each autorelease pool starts on a new pool page.
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {
//這裏討論快速方法
dest = autoreleaseFast(POOL_BOUNDARY);
}
ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}
######### autoreleaseFast()
static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage();
//是否有hotPage 且沒有滿
if (page && !page->full()) {
//沒滿就添加
return page->add(obj);
} else if (page) {//有hotPage,但滿了
滿了就開分頁;
return autoreleaseFullPage(obj, page);
} else {//沒有hotPage,內部也是新的page
return autoreleaseNoPage(obj);
}
}
######### add()
id *add(id obj)
{
ASSERT(!full());
unprotect();
id *ret = next; // faster than `return next-1` because of aliasing
*next++ = obj;//將next指針指向當前對象指針
protect();
return ret;
}
######### autoreleaseFullPage()
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
// The hot page is full.
// Step to the next non-full page, adding a new page if necessary.
// Then add the object to that page.
ASSERT(page == hotPage());
ASSERT(page->full() || DebugPoolAllocation);
do {//內部遞歸尋找最後一頁(判斷是否有page->child),找到後開新的page
if (page->child) page = page->child;
else page = new AutoreleasePoolPage(page);
} while (page->full());
//將新page置爲HotPage
setHotPage(page);
return page->add(obj);
}
複製代碼
push簡單總結
depth ++
深度增長這裏的ctxt 實際上是AutoreleasePoolPage
void objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
############ pop()
static inline void
pop(void *token)
{
AutoreleasePoolPage *page;
id *stop;//用於保存釋放中止的標記
//判斷 token(對象指針)是不是空的標識指針,是的話就表明沒有對象被放入這個池子裏
if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
// Popping the top-level placeholder pool.
page = hotPage();
if (!page) {
// Pool was never used. Clear the placeholder.
return setHotPage(nil);
}
// Pool was used. Pop its contents normally.
// Pool pages remain allocated for re-use as usual.
page = coldPage();
token = page->begin();
} else {
//找到對象指針所在的page
page = pageForPointer(token);
}
//將對象指針標記爲中止指針
stop = (id *)token;
if (*stop != POOL_BOUNDARY) {
// 第一個節點 - 沒有父節點,越界保護判斷
if (stop == page->begin() && !page->parent) {
// Start of coldest page may correctly not be POOL_BOUNDARY:
// 1. top-level pool is popped, leaving the cold page in place
// 2. an object is autoreleased with no pool
} else {
// Error. For bincompat purposes this is not
// fatal in executables built with old SDKs.
return badPop(token);
}
}
if (slowpath(PrintPoolHiwat || DebugPoolAllocation || DebugMissingPools)) {
return popPageDebug(token, page, stop);
}
//開始pop
return popPage<false>(token, page, stop);
}
######## popPage()
static void
popPage(void *token, AutoreleasePoolPage *page, id *stop)
{
if (allowDebug && PrintPoolHiwat) printHiwat();
//內部進行對象release
page->releaseUntil(stop);
// 殺page,即刪除空的child page,
if (allowDebug && DebugPoolAllocation && page->empty()) {
// special case: delete everything during page-per-pool debugging
AutoreleasePoolPage *parent = page->parent;
page->kill();
setHotPage(parent);
} else if (allowDebug && DebugMissingPools && page->empty() && !page->parent) {
// special case: delete everything for pop(top)
// when debugging missing autorelease pools
page->kill();
setHotPage(nil);
} else if (page->child) {
// hysteresis: keep one empty child if page is more than half full
if (page->lessThanHalfFull()) {
page->child->kill();
}
else if (page->child->child) {
page->child->child->kill();
}
}
}
######### releaseUntil() //釋放對象操做
void releaseUntil(id *stop)
{
// Not recursive: we don't want to blow out the stack // if a thread accumulates a stupendous amount of garbage //循環遍歷,直到stop對象 while (this->next != stop) { // Restart from hotPage() every time, in case -release // autoreleased more objects AutoreleasePoolPage *page = hotPage(); //若是page空了,就拿parent page while (page->empty()) { page = page->parent; setHotPage(page); } page->unprotect(); //next 指針 -- id obj = *--page->next; memset((void*)page->next, SCRIBBLE, sizeof(*page->next)); page->protect(); //不等於邊界指針 if (obj != POOL_BOUNDARY) { //對象s釋放 objc_release(obj); } } //將當前page設置爲HotPage setHotPage(this); #if DEBUG // we expect any children to be completely empty for (AutoreleasePoolPage *page = child; page; page = page->child) { ASSERT(page->empty()); } #endif } 複製代碼
pop簡單總結
//再來看看在ARC環境下,這個被隱藏的autorelease()方法作了什麼
- (id)autorelease {
//將self放到_objc_rootAutorelease()
return _objc_rootAutorelease(self);
}
_objc_rootAutorelease(id obj) {
ASSERT(obj);
//調用rootAutorelease()
return obj->rootAutorelease();
}
id objc_object::rootAutorelease() {
//TaggedPointer 不須要管理引用計數
if (isTaggedPointer()) return (id)this;
if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;
return rootAutorelease2();
}
id
objc_object::rootAutorelease2() {
ASSERT(!isTaggedPointer());
return AutoreleasePoolPage::autorelease((id)this);
}
//重點
static inline id autorelease(id obj) {
ASSERT(obj);
ASSERT(!obj->isTaggedPointer());
//仍是會調用autoreleaseFast()方法
id *dest __unused = autoreleaseFast(obj);
ASSERT(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj);
return obj;
}
複製代碼
autorelease簡單總結
autorelease方法其實實現和push方法同樣,不一樣的是在push前會判斷對象是否能夠是TaggedPointer對象;
timer