此文源於前幾日工做中遇到的一個問題,並跟同事就init
方法進行了相關討論。相關代碼以下:html
Person *myPerson = [Person alloc];
NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"@16@0:8"];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
invocation.target = myPerson;
invocation.selector = @selector(initPerson);
[invocation invoke];
__unsafe_unretained id retValue;
[invocation getReturnValue:&retValue];
複製代碼
正常來講,這段代碼運行起來沒有任何問題。然而,當Person的initPerson
方法返回nil
或者返回子類對象時,上述代碼就會EXC_BAD_ACCESS。但若是咱們把initPerson
方法前綴改爲其餘(好比:createPerson
),就不會crash。爲了查清緣由,便對init
方法進行了一次探索(說探索多少有些誇張)。app
經過符號斷點及反彙編等調試手段,發如今initPerson
方法結束的時候,person對象調用了一次release
,而上述示例代碼執行完,ARC爲了抵消[Person alloc]
這步操做,會對myPerson進行一次release
。也就是說,過渡釋放引發了crash。測試
那麼接下來,咱們就看下init
方法結束的時候,爲何要調用那次看似多餘的release
?優化
在clang文檔中找到這麼兩個東西:__attribute__((ns_consumes_self))
、 __attribute((ns_returns_retained))
。ui
據文檔描述,前者的做用是將ownership
從主調方轉移到被調方;然後者的做用是把ownership
從被調方轉移到主調方。具體原理以下:this
__attribute__((ns_consumes_self))
若某個方法被標記這個特性,調用方會在方法調用前對receiver
進行一次retain
(也可能會被編譯器優化掉),而被調方會在方法結束的時候對self
進行一次release
。好比下面代碼:spa
// 主調方
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
Person *myPerson = [[Person alloc] init];
[myPerson noninitPerson]; // 以非init方法來測試
return YES;
}
// 被調方
@interface Person : NSObject
- (void)noninitPerson __attribute__((ns_consumes_self));
@end
@implementation Person
- (void)noninitPerson {
}
@end
複製代碼
經過Hopper反彙編,僞代碼以下:調試
// 主調方
bool -[AppDelegate application:didFinishLaunchingWithOptions:](void * self, void * _cmd, void * arg2, void * arg3) {
var_28 = [[Person alloc] init];
rax = [var_28 retain]; // 調用前retain
[rax noninitPerson]; // 開始調用
objc_storeStrong(var_28, 0x0);
return rax;
}
// 被調方
void -[Person noninitPerson](void * self, void * _cmd {
objc_storeStrong(self, 0x0); // 調用完被調方負責release
return;
}
複製代碼
而init
開頭的方法會被隱式地標記這個特性,文檔中有描述:code
The implicit self parameter of a method may be marked as consumed by adding __ attribute __((ns_consumes_self)) to the method declaration. Methods in the
init
family are treated as if they were implicitly marked with this attribute.htm
__attribute__((ns_returns_retained))
若方法標記這個特性,表示主調方但願獲得一個retainCount+1的對象,即被調方可能會進行一次retain
將全部權移交給主調方,主調方會進行一次release
(可能會被編譯器優化掉)來負責釋放。
僞代碼以下:
// 主調方
var_28 = [[Person alloc] init];
rax = [var_28 running];
[rax release]; // 主調方負責釋放
// 被調方
void * -[Person running](void * self, void * _cmd) {
rax = [self retain]; // 若這裏返回一個新分配的對象,則無需retain
return rax;
}
複製代碼
一樣地,init
開頭的方法也會被標記這個特性,文檔裏亦有體現:
Methods in the
alloc
,copy
,init
,mutableCopy
, andnew
families are implicitly marked __ attribute __((ns_returns_retained)).
這麼多的retain
、release
,多少有些凌亂,既然已知init
方法會被標記__attribute__((ns_returns_retained))
和__attribute__((ns_consumes_self))
,那咱們乾脆看下init
方法反彙編後的代碼:
// 主調方
bool -[AppDelegate application:didFinishLaunchingWithOptions:](void * self, void * _cmd, void * arg2, void * arg3) {
var_28 = [[Person alloc] init];
objc_storeStrong(var_28, 0x0);
// 優化掉了一對retain/release
return rax;
}
// 被調方
void * -[Person init](void * self, void * _cmd) {
// 忽略一些無關指令
var_18 = [self retain]; // 對應__attribute__((ns_returns_retained))
objc_storeStrong(self, 0x0); // 對應__attribute__((ns_consumes_self))
rax = var_18;
return rax;
}
複製代碼
到這裏,咱們基本瞭解了init
方法原理,那麼離文章開頭那段代碼crash又如何解釋呢?咱們對代碼稍做修改,讓init
方法返回nil
,再看下:
// 主調方
bool -[AppDelegate application:didFinishLaunchingWithOptions:](void * self, void * _cmd, void * arg2, void * arg3) {
var_28 = [[Person alloc] init];
objc_storeStrong(var_28, 0x0);
return rax;
}
// 被調方
void * -[Person init](void * self, void * _cmd) {
// 由於返回nil,因此這裏的retain不存在了,而下面的self依然要消費掉
objc_storeStrong(self, 0x0); // 對應__attribute__((ns_consumes_self))
return 0x0;
}
複製代碼
至此,過分釋放的緣由也就清楚了,那麼該怎麼解決呢?
回到文章開頭,再看下代碼,不難發現,咱們只要模仿ARC在init
方法調用前插入個retain
,並在主調方快結束的時候再插入個release
便可。
Person *myPerson = [Person alloc];
NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"@16@0:8"];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
invocation.target = myPerson;
invocation.selector = @selector(initPerson);
CFBridgingRetain(myPerson); // 代替ARC將owneship將傳遞給被調方
[invocation invoke];
__unsafe_unretained id retValue;
[invocation getReturnValue:&retValue];
CFBridgingRelease((__bridge CFTypeRef)retValue); // 代替ARC來釋放ns_returns_retained結果
複製代碼
若是init
方法返回nil
,即retValue=nil
,則CFBridgingRelease
不會生效,上面插的那個CFBridgingRetain
也就完美抵消掉了init
方法結束時的release
。
參考資料:
clang.llvm.org/docs/Automa… opensource.apple.com/source/lldb…