IOS面試考察(九):性能優化相關問題github
@[TOC]面試
runtime是iOS開發最核心的知識了,若是下面的問題都解決了,那麼對runtime的理解已經很深了。 runtime已經開源了,這有一份別人調試好可運行的源碼objc-runtime,也能夠去官網找objc4objective-c
官方的代碼下載下來要讓它運行起來,是須要花費一點時間去填坑的。我這裏提供一下已經填好坑,能夠直接編譯運行的objc4_750代碼:swift
先思考一下下面的這些問題,看你能回答出多少:數組
- runtime怎麼添加屬性、方法等
- runtime 如何實現 weak 屬性
- runtime如何經過selector找到對應的IMP地址?(分別考慮類方法和實例方法)
- 使用runtime Associate方法關聯的對象,須要在主對象dealloc的時候釋放麼?
- _objc_msgForward函數是作什麼的?直接調用它將會發生什麼?
- 可否向編譯後獲得的類中增長實例變量?可否向運行時建立的類中添加實例變量?爲何?
- 簡述下Objective-C中調用方法的過程(runtime)
- 什麼是method swizzling(俗稱黑魔法)
咱們先來簡單瞭解一下runtime的3個簡單問題:緩存
- 什麼是runtime?
- 爲啥要用runtime?
- runtime有什麼做用?
- runtime本質上是一套比較底層的C語言,C++ ,彙編組成的API。咱們有稱爲運行時,在runtime的底層不少實現爲了性能效率方面,都直接用匯編代碼。
- 咱們平時編寫的OC代碼,須要runtime來建立類和對象,進行消息發送和轉發,其實最終會轉換成Runtime的C語言代碼。
- runtime是將數據類型的肯定由編譯時推遲到了運行時。
- OC是一門動態語言,它會將一些工做放在代碼的運行時纔去處理而並不是編譯時,所以編譯器是不夠,咱們還須要一個運行時系統來處理編譯後的代碼。
- runtime基本是用C語言和彙編語言寫的,蘋果和GNU各自維護一個開源的runtime版本,這兩個版本之間都高度的保持一致。
- 消息傳遞、轉發<消息機制>
- 訪問私有變量 --eg:(UITextFiled 的修改)
- 交換系統方法 --eg:(攔截--防止button連續點擊)
- 動態增長方法
- 爲分類增長屬性
- 字典轉模型 -- eg:(YYModel, MJModel)
接下來咱們來給出runtime面試問題的解答,可能不完善,歡迎補充。安全
ivar表示成員變量 class_addIvar class_addMethod class_addProperty class_addProtocol class_replaceProperty性能優化
NSObject
添加一個name屬性,動態添加屬性 -> runtime思路:bash
- 給NSObject添加分類,在分類中添加屬性。問題:@property在分類中做用:僅僅是生成get,set方法聲明,不會生成get,set方法實現和下劃線成員屬性,因此要在.m文件實現setter/getter方法,用static保存下滑線屬性,這樣一來,當對象銷燬時,屬性沒法銷燬。
- 用runtime動態添加屬性:本質是讓屬性與某個對象產生一段關聯 使用場景:給系統的類添加屬性時。
實現代碼以下:
#import <objc/message.h>
@implementation NSObject (Property)
//static NSString *_name; //經過這樣去保存屬性無法作到對象銷燬,屬性也銷燬,static依然會讓屬性存在緩存池中,因此須要動態的添加成員屬性
// 只要想調用runtime方法,思考:誰的事情
-(void)setName:(NSString *)name
{
// 保存name
// 動態添加屬性 = 本質:讓對象的某個屬性與值產生關聯
/* object:保存到哪一個對象中 key:用什麼屬性保存 屬性名 value:保存值 policy:策略,strong,weak objc_setAssociatedObject(<#id object#>, <#const void *key#>, <#id value#>, <#objc_AssociationPolicy policy#>) */
objc_setAssociatedObject(self, "name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// _name = name;
}
- (NSString *)name
{
return objc_getAssociatedObject(self, "name");
// return _name;
}
@end
複製代碼
開發中,從網絡數據中解析出字典數組,將數組轉爲模型時,若是有幾百個key須要用,要寫不少@property成員屬性,下面提供一個萬能的方法,可直接將字典數組轉爲所有@property成員屬性,打印出來,這樣直接複製在模型中就行了。
#import "NSDictionary+PropertyCode.h"
@implementation NSDictionary (PropertyCode)
//經過這個方法,自動將字典轉成模型中須要用的屬性代碼
// 私有API:真實存在,可是蘋果沒有暴露出來,不給你。如BOOL值,不知道類型,打印得知是__NSCFBoolean,可是沒法敲出來,只能用NSClassFromString(@"__NSCFBoolean")
// isKindOfClass:判斷下是不是當前類或者子類,BOOL是NSNumber的子類,要先判斷BOOL
- (void)createPropetyCode
{
// 模型中屬性根據字典的key
// 有多少個key,生成多少個屬性
NSMutableString *codes = [NSMutableString string];
// 遍歷字典
[self enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull value, BOOL * _Nonnull stop) {
NSString *code = nil;
// NSLog(@"%@",[value class]);
if ([value isKindOfClass:[NSString class]]) {
code = [NSString stringWithFormat:@"@property (nonatomic, strong) NSString *%@;",key];
} else if ([value isKindOfClass:NSClassFromString(@"__NSCFBoolean")]){
code = [NSString stringWithFormat:@"@property (nonatomic, assign) BOOL %@;",key];
} else if ([value isKindOfClass:[NSNumber class]]) {
code = [NSString stringWithFormat:@"@property (nonatomic, assign) NSInteger %@;",key];
} else if ([value isKindOfClass:[NSArray class]]) {
code = [NSString stringWithFormat:@"@property (nonatomic, strong) NSArray *%@;",key];
} else if ([value isKindOfClass:[NSDictionary class]]) {
code = [NSString stringWithFormat:@"@property (nonatomic, strong) NSDictionary *%@;",key];
}
// 拼接字符串
[codes appendFormat:@"%@\n",code];
}];
NSLog(@"%@",codes);
}
@end
複製代碼
YYModel
, MJModel
.實現代碼以下:
@implementation Status
+ (instancetype)statusWithDict:(NSDictionary *)dict{
// 建立模型
Status *s = [[self alloc] init];
// 字典value轉模型屬性保存
[s setValuesForKeysWithDictionary:dict];
// s.reposts_count = dict[@"reposts_count"];
// 4️⃣MJExtension:能夠字典轉模型,並且能夠不用與字典中屬性一一對應,runtime,遍歷模型中有多少個屬性,直接去字典中取出對應value,給模型賦值
// 1️⃣setValuesForKeysWithDictionary:方法底層實現:遍歷字典中全部key,去模型中查找對應的屬性,把值給模型屬性賦值,即調用下面方法:
/* [dict enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { // source // 這行代碼纔是真正給模型的屬性賦值 [s setValue:dict[@"source"] forKey:@"source"]; //底層實現是: 2️⃣ [s setValue:dict[@"source"] forKey:@"source"]; 1.首先會去模型中查找有沒有setSource方法,直接調用set方法 [s setSource:dict[@"source"]]; 2.去模型中查找有沒有source屬性,source = dict[@"source"] 3.去查找有沒有_source屬性,_source = dict[@"source"] 4.調用對象的 setValue:forUndefinedKey:直接報錯 [s setValue:obj forKey:key]; }]; */
return s;
}
// 3️⃣用KVC,不想讓系統報錯,重寫系統方法思想:
// 1.想給系統方法添加功能
// 2.不想要系統實現
- (void)setValue:(id)value forUndefinedKey:(NSString *)key
{
}
@end
複製代碼
#import "NSObject+Model.h"
#import <objc/message.h>
// class_copyPropertyList(<#__unsafe_unretained Class cls#>, <#unsigned int *outCount#>) 獲取屬性列表
@implementation NSObject (Model)
/** 字典轉模型 @param dict 傳入須要轉模型的字典 @return 賦值好的模型 */
+ (instancetype)modelWithDict:(NSDictionary *)dict
{
id objc = [[self alloc] init];
//思路: runtime遍歷模型中屬性,去字典中取出對應value,在給模型中屬性賦值
// 1.獲取模型中全部屬性 -> 保存到類
// ivar:下劃線成員變量 和 Property:屬性
// 獲取成員變量列表
// class:獲取哪一個類成員變量列表
// count:成員變量總數
//這個方法獲得一個裝有成員變量的數組
//class_copyIvarList(<#__unsafe_unretained Class cls#>, <#unsigned int *outCount#>)
int count = 0;
// 成員變量數組 指向數組第0個元素
Ivar *ivarList = class_copyIvarList(self, &count);
// 遍歷全部成員變量
for (int i = 0; i < count; i++) {
// 獲取成員變量 user
Ivar ivar = ivarList[i];
// 獲取成員變量名稱,即將C語言的字符轉爲OC字符串
NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 獲取成員變量類型,用於獲取二級字典的模型名字
NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
// 將type這樣的字符串@"@\"User\"" 轉成 @"User"
type = [type stringByReplacingOccurrencesOfString:@"@\"" withString:@""];
type = [type stringByReplacingOccurrencesOfString:@"\"" withString:@""];
// 成員變量名稱轉換key,即去掉成員變量前面的下劃線
NSString *key = [ivarName substringFromIndex:1];
// 從字典中取出對應value dict[@"user"] -> 字典
id value = dict[key];
// 二級轉換
// 而且是自定義類型,才須要轉換
if ([value isKindOfClass:[NSDictionary class]] && ![type containsString:@"NS"]) { // 只有是字典才須要轉換
Class className = NSClassFromString(type);
// 字典轉模型
value = [className modelWithDict:value];
}
// 給模型中屬性賦值 key:user value:字典 -> 模型
if (value) {
[objc setValue:value forKey:key];
}
}
return objc;
}
@end
複製代碼
在Model的基類中重寫方法:
- (id)initWithCoder:(NSCoder *)aDecoder {
if (self = [super init]) {
unsigned int outCount;
Ivar * ivars = class_copyIvarList([self class], &outCount); for (int i = 0; i < outCount; i ++) {
Ivar ivar = ivars[i];
NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
[self setValue:[aDecoder decodeObjectForKey:key] forKey:key];
}
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
unsigned int outCount;
Ivar * ivars = class_copyIvarList([self class], &outCount); for (int i = 0; i < outCount; i ++) {
Ivar ivar = ivars[i];
NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
[aCoder encodeObject:[self valueForKey:key] forKey:key];
}
}
複製代碼
可能的面試題:有沒有使用performSelector,其實主要想問你有沒有動態添加過方法。
動態添加方法代碼實現:
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
Person *p = [[Person alloc] init];
// 默認person,沒有實現eat方法,能夠經過performSelector調用,可是會報錯。
// 動態添加方法就不會報錯
[p performSelector:@selector(eat)];
}
@end
@implementation Person
// void(*)()
// 默認方法都有兩個隱式參數,
void eat(id self,SEL sel)
{
NSLog(@"%@ %@",self,NSStringFromSelector(sel));
}
// 當一個對象調用未實現的方法,會調用這個方法處理,而且會把對應的方法列表傳過來.
// 恰好能夠用來判斷,未實現的方法是否是咱們想要動態添加的方法
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(eat)) {
// 動態添加eat方法
// 第一個參數:給哪一個類添加方法
// 第二個參數:添加方法的方法編號
// 第三個參數:添加方法的函數實現(函數地址)
// 第四個參數:函數的類型,(返回值+參數類型) v:void @:對象->self :表示SEL->_cmd
class_addMethod(self, @selector(eat), eat, "v@:");
}
return [super resolveInstanceMethod:sel];
}
@end
複製代碼
OC的分類容許給分類添加屬性,但不會自動生成getter、setter方法 因此常規的僅僅添加以後,調用的話會crash。
關聯對象不是爲類\對象添加屬性或者成員變量(由於在設置關聯後也沒法經過ivarList或者propertyList取得) ,而是爲類添加一個相關的對象,一般用於存儲類信息,例如存儲類的屬性列表數組,爲未來字典轉模型的方便。
runtime關聯對象的API:
//關聯對象
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
//獲取關聯的對象
id objc_getAssociatedObject(id object, const void *key)
//移除關聯的對象
void objc_removeAssociatedObjects(id object)
複製代碼
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 給系統NSObject類動態添加屬性name
NSObject *objc = [[NSObject alloc] init];
objc.name = @"孔雨露";
NSLog(@"%@",objc.name);
}
@end
// 定義關聯的key
static const char *key = "name";
@implementation NSObject (Property)
- (NSString *)name
{
// 根據關聯的key,獲取關聯的值。
return objc_getAssociatedObject(self, key);
}
- (void)setName:(NSString *)name
{
// 第一個參數:給哪一個對象添加關聯
// 第二個參數:關聯的key,經過這個key獲取
// 第三個參數:關聯的value
// 第四個參數:關聯的策略
objc_setAssociatedObject(self, key, name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
複製代碼
/** * 刪除點擊 * @param recId 購物車ID */
- (void)shopCartCell:(BSShopCartCell *)shopCartCell didDeleteClickedAtRecId:(NSString *)recId
{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"" message:@"確認要刪除這個寶貝" delegate:self cancelButtonTitle:@"取消" otherButtonTitles:@"肯定", nil];
/* objc_setAssociatedObject方法的參數解釋: 第一個參數: id object, 當前對象 第二個參數: const void *key, 關聯的key,是c字符串 第三個參數: id value, 被關聯的對象的值 第四個參數: objc_AssociationPolicy policy關聯引用的規則 */
// 傳遞多參數
objc_setAssociatedObject(alert, "suppliers_id", @"1", OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_setAssociatedObject(alert, "warehouse_id", @"2", OBJC_ASSOCIATION_RETAIN_NONATOMIC);
alert.tag = [recId intValue];
[alert show];
}
/** * 肯定刪除操做 */
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
if (buttonIndex == 1) {
NSString *warehouse_id = objc_getAssociatedObject(alertView, "warehouse_id");
NSString *suppliers_id = objc_getAssociatedObject(alertView, "suppliers_id");
NSString *recId = [NSString stringWithFormat:@"%ld",(long)alertView.tag];
}
}
複製代碼
weak策略代表該屬性定義了一種「非擁有關係」 (nonowning relationship)。爲這種屬性設置新值時,設置方法既不保留新值,也不釋放舊值。此特質同assign相似;然而在屬性所指的對象遭到摧毀時,屬性值也會清空(nil out)
runtime對註冊的類,會進行佈局,會將 weak 對象放入一個 hash 表中。用 weak 指向的對象內存地址做爲 key,當此對象的引用計數爲0的時候會調用對象的 dealloc 方法,假設 weak 指向的對象內存地址是a,那麼就會以a爲key,在這個 weak hash表中搜索,找到全部以a爲key的 weak 對象,從而設置爲 nil。
在ARC環境不管是強指針仍是弱指針都無需在 dealloc 設置爲 nil , ARC 會自動幫咱們處理 即使是編譯器不幫咱們作這些,weak也不須要在dealloc中置nil 在屬性所指的對象遭到摧毀時,屬性值也會清空
objc// 模擬下weak的setter方法,大體以下
- (void)setObject:(NSObject *)object{
objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN);
[object cyl_runAtDealloc:^{ _object = nil; }];
}
複製代碼
先看下 runtime 裏源碼的實現:具體完整實現參照 objc/objc-weak.h 。
/** * The internal structure stored in the weak references table. * It maintains and stores * a hash set of weak references pointing to an object. * If out_of_line==0, the set is instead a small inline array. */
#define WEAK_INLINE_COUNT 4
struct weak_entry_t {
DisguisedPtr<objc_object> referent;
union {
struct {
weak_referrer_t *referrers;
uintptr_t out_of_line : 1;
uintptr_t num_refs : PTR_MINUS_1;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
struct {
// out_of_line=0 is LSB of one of these (don't care which)
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
};
};
};
/** * The global weak references table. Stores object ids as keys, * and weak_entry_t structs as their values. */
struct weak_table_t {
weak_entry_t *weak_entries;
size_t num_entries;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
複製代碼
咱們能夠設計一個函數(僞代碼)來表示上述機制:
objc_storeWeak(&a, b)
函數:
objc_storeWeak
函數把第二個參數--賦值對象(b)的內存地址做爲鍵值key,將第一個參數--weak
修飾的屬性變量(a)的內存地址(&a)做爲value
,註冊到weak
表中。若是第二個參數(b)爲0(nil),那麼把變量(a)的內存地址(&a)從weak
表中刪除,
你能夠把objc_storeWeak(&a, b)
理解爲:objc_storeWeak(value, key)
,而且當key
變nil
,將value
置nil
。
在b非nil時,a和b指向同一個內存地址,在b變nil時,a變nil。此時向a發送消息不會崩潰:在Objective-C中向nil發送消息是安全的。
而若是a是由 assign 修飾的,則: 在 b 非 nil 時,a 和 b 指向同一個內存地址,在 b 變 nil 時,a 仍是指向該內存地址,變野指針。此時向 a 發送消息極易崩潰。
下面咱們將基於objc_storeWeak(&a, b)
函數,使用僞代碼模擬「runtime如何實現weak屬性」:
// 使用僞代碼模擬:runtime如何實現weak屬性
id obj1;
objc_initWeak(&obj1, obj);
/*obj引用計數變爲0,變量做用域結束*/
objc_destroyWeak(&obj1);
複製代碼
下面對用到的兩個方法objc_initWeak
和objc_destroyWeak
作下解釋:
整體說來,做用是: 經過
objc_initWeak
函數初始化「附有weak
修飾符的變量(obj1)」,在變量做用域結束時經過objc_destoryWeak
函數釋放該變量(obj1)。
下面分別介紹下方法的內部實現:
objc_initWeak
函數的實現是這樣的:在將「附有weak
修飾符的變量(obj1)」初始化爲0(nil)後,會將「賦值對象」(obj)做爲參數,調用objc_storeWeak
函數。
obj1 = 0;
obj_storeWeak(&obj1, obj);
複製代碼
也就是說:weak
修飾的指針默認值是 nil (在Objective-C中向nil發送消息是安全的)。 而後obj_destroyWeak
函數將0(nil)做爲參數,調用objc_storeWeak
函數。objc_storeWeak(&obj1, 0);
前面的源代碼與下列源代碼相同。
// 使用僞代碼模擬:runtime如何實現weak屬性
id obj1;
obj1 = 0;
objc_storeWeak(&obj1, obj);
/* ... obj的引用計數變爲0,被置nil ... */
objc_storeWeak(&obj1, 0);
複製代碼
objc_storeWeak
函數把第二個參數--賦值對象(obj)的內存地址做爲鍵值,將第一個參數--weak修飾的屬性變量(obj1)的內存地址註冊到 weak 表中。若是第二個參數(obj)爲0(nil),那麼把變量(obj1)的地址從 weak 表中刪除,
使用僞代碼是爲了方便理解,下面咱們「真槍實彈」地實現下:
咱們從setter方法入手:
(注意如下的 cyl_runAtDealloc
方法實現僅僅用於模擬原理,若是想用於項目中,還須要考慮更復雜的場景,想在實際項目使用的話,可使用ChenYilong 寫的一個庫 CYLDeallocBlockExecutor )
- (void)setObject:(NSObject *)object
{
objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN);
[object cyl_runAtDealloc:^{
_object = nil;
}];
}
複製代碼
也就是有兩個步驟:
(1) 在setter方法中作以下設置:
objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN);
複製代碼
(2), 在屬性所指的對象遭到摧毀時,屬性值也會清空(nil out)。作到這點,一樣要藉助 runtime:
//要銷燬的目標對象
id objectToBeDeallocated;
//能夠理解爲一個「事件」:當上面的目標對象銷燬時,同時要發生的「事件」。
id objectWeWantToBeReleasedWhenThatHappens;
objc_setAssociatedObject(objectToBeDeallocted,
someUniqueKey,
objectWeWantToBeReleasedWhenThatHappens,
OBJC_ASSOCIATION_RETAIN);
複製代碼
知道了思路,咱們就開始實現 cyl_runAtDealloc 方法,實現過程分兩部分:
// 這個類,能夠理解爲一個「事件」:當目標對象銷燬時,同時要發生的「事件」。藉助block執行「事件」。
typedef void (^voidBlock)(void);
@interface CYLBlockExecutor : NSObject
- (id)initWithBlock:(voidBlock)block;
@end
@interface CYLBlockExecutor() {
voidBlock _block;
}
@implementation CYLBlockExecutor
- (id)initWithBlock:(voidBlock)aBlock
{
self = [super init];
if (self) {
_block = [aBlock copy];
}
return self;
}
- (void)dealloc
{
_block ? _block() : nil;
}
@end
複製代碼
// 利用runtime實現cyl_runAtDealloc方法
#import "CYLBlockExecutor.h"
const void *runAtDeallocBlockKey = &runAtDeallocBlockKey;
@interface NSObject (CYLRunAtDealloc)
- (void)cyl_runAtDealloc:(voidBlock)block;
@end
@implementation NSObject (CYLRunAtDealloc)
- (void)cyl_runAtDealloc:(voidBlock)block
{
if (block) {
CYLBlockExecutor *executor = [[CYLBlockExecutor alloc] initWithBlock:block];
objc_setAssociatedObject(self,
runAtDeallocBlockKey,
executor,
OBJC_ASSOCIATION_RETAIN);
}
}
@end
複製代碼
使用方法: 導入#import "CYLNSObject+RunAtDealloc.h"
,而後就可使用了:
NSObject *foo = [[NSObject alloc] init];
[foo cyl_runAtDealloc:^{
NSLog(@"正在釋放foo!");
}];
複製代碼
runtime
對註冊的類, 會進行佈局,對於weak
對象會放入一個hash
表中。 用weak
指向的對象內存地址做爲key
,當此對象的引用計數爲0的時候會dealloc
,假如weak
指向的對象內存地址是a,那麼就會以a爲鍵, 在這個weak
表中搜索,找到全部以a爲鍵的weak
對象,從而設置爲 nil。
咱們能夠設計一個函數(僞代碼)來表示上述機制:
objc_storeWeak(&a, b)
函數:
objc_storeWeak
函數把第二個參數--賦值對象(b)的內存地址做爲鍵值key,將第一個參數--weak
修飾的屬性變量(a)的內存地址(&a)做爲value,註冊到 weak
表中。若是第二個參數(b)爲0(nil),那麼把變量(a)的內存地址(&a)從weak
表中刪除,
你能夠把objc_storeWeak(&a, b
)理解爲:objc_storeWeak(value, key)
,而且當key
變nil
,將value
置nil
。
在b非nil
時,a和b指向同一個內存地址,在b變nil時,a變nil。此時向a發送消息不會崩潰:在Objective-C中向nil發送消息是安全的。
而若是a是由assign
修飾的,則: 在b非nil時,a和b指向同一個內存地址,在b變nil時,a仍是指向該內存地址,變野指針。此時向a發送消息極易崩潰。
下面咱們將基於objc_storeWeak(&a, b)
函數,使用僞代碼模擬「runtime如何實現weak屬性」:
id obj1;
objc_initWeak(&obj1, obj);
/*obj引用計數變爲0,變量做用域結束*/
objc_destroyWeak(&obj1);
複製代碼
下面對用到的兩個方法objc_initWeak
和objc_destroyWeak
作下解釋:
整體說來,做用是: 經過objc_initWeak
函數初始化「附有weak修飾符的變量(obj1)」,在變量做用域結束時經過objc_destoryWeak
函數釋放該變量(obj1)。
下面分別介紹下方法的內部實現:
objc_initWeak
函數的實現是這樣的:在將「附有weak
修飾符的變量(obj1)」初始化爲0(nil)後,會將「賦值對象」(obj)做爲參數,調用objc_storeWeak
函數。
obj1 = 0;
obj_storeWeak(&obj1, obj);
複製代碼
也就是說:weak 修飾的指針默認值是 nil (在Objective-C中向nil發送消息是安全的) 而後obj_destroyWeak函數將0(nil)做爲參數,調用objc_storeWeak
函數。
objc_storeWeak(&obj1, 0);
複製代碼
id obj1;
obj1 = 0;
objc_storeWeak(&obj1, obj);
/* ... obj的引用計數變爲0,被置nil ... */
objc_storeWeak(&obj1, 0);
複製代碼
objc_storeWeak
函數把第二個參數--賦值對象(obj)的內存地址做爲鍵值,將第一個參數--weak修飾的屬性變量(obj1)的內存地址註冊到 weak
表中。若是第二個參數(obj)爲0(nil),那麼把變量(obj1)的地址從weak
表中刪除。
- 每個類對象中都一個對象方法列表(對象方法緩存)
- 類方法列表是存放在類對象中isa指針指向的元類對象中(類方法緩存)
- 方法列表中每一個方法結構體中記錄着方法的名稱,方法實現,以及參數類型,其實selector本質就是方法名稱,經過這個方法名稱就能夠在方法列表中找到對應的方法實現.
- 當咱們發送一個消息給一個NSObject對象時,這條消息會在對象的類對象方法列表裏查找
- 當咱們發送一個消息給一個類時,這條消息會在類的Meta Class對象的方法列表裏查找
元類,就像以前的類同樣,它也是一個對象,全部的元類都使用根元類(繼承體系中處於頂端的類的元類)做爲他們的類。這就意味着全部NSObject的子類(大多數類)的元類都會以NSObject的元類做爲他們的類,根據這個規則,全部的元類使用根元類做爲他們的類,根元類的元類則就是它本身。也就是說基類的元類的isa指針指向他本身。
不管在MRC下仍是ARC下均不須要, 被關聯的對象在生命週期內要比對象自己釋放的晚不少,它們會在被 NSObject -dealloc 調用的object_dispose()方法中釋放 補充:對象的內存銷燬時間表,分四個步驟
- 調用 -release :引用計數變爲零 對象正在被銷燬,生命週期即將結束. 不能再有新的 __weak 弱引用,不然將指向 nil. 調用 [self dealloc]
- 父類調用 -dealloc 繼承關係中最直接繼承的父類再調用 -dealloc 若是是 MRC 代碼 則會手動釋放實例變量們(iVars) 繼承關係中每一層的父類 都再調用 -dealloc
- NSObject 調 -dealloc 只作一件事:調用 Objective-C runtime 中object_dispose() 方法
- 調用 object_dispose() 爲 C++ 的實例變量們(iVars)調用 destructors 爲 ARC 狀態下的 實例變量們(iVars) 調用 -release 解除全部使用 runtime Associate方法關聯的對象 解除全部 __weak 引用 調用 free()
_objc_msgForward是IMP類型,用於消息轉發的:當向一個對象發送一條消息,但它並無實現的時候,_objc_msgForward會嘗試作消息轉發。 直接調用_objc_msgForward是很是危險 的事,這是把雙刃刀,若是用很差會直接致使程序Crash,可是若是用得好,能作不少很是酷的事 JSPatch就是直接調用_objc_msgForward來實現其核心功能的 詳細解說參見這裏的第一個問題解答
咱們能夠這樣建立一個_objc_msgForward對象:IMP msgForwardIMP = _objc_msgForward;
。
咱們知道objc_msgSend在「消息傳遞」中的做用。 在「消息傳遞」過程當中,objc_msgSend的動做比較清晰:
- 首先在 Class 中的緩存查找 IMP (沒緩存則初始化緩存),
- 若是沒找到,則向父類的 Class 查找。
- 若是一直查找到根類仍舊沒有實現,則用_objc_msgForward函數指針代替 IMP 。
- 最後,執行這個 IMP 。
咱們能夠在objc4_750源碼中的objc-runtime-new.mm
文件中搜索_objc_msgForward
裏面有一個lookUpImpOrForward
的說明
/*********************************************************************** * lookUpImpOrForward. * The standard IMP lookup. * initialize==NO tries to avoid +initialize (but sometimes fails) * cache==NO skips optimistic unlocked lookup (but uses cache elsewhere) * Most callers should use initialize==YES and cache==YES. * inst is an instance of cls or a subclass thereof, or nil if none is known. * If cls is an un-initialized metaclass then a non-nil inst is faster. * May return _objc_msgForward_impcache. IMPs destined for external use * must be converted to _objc_msgForward or _objc_msgForward_stret. * If you don't want forwarding at all, use lookUpImpOrNil() instead. **********************************************************************/
複製代碼
對 objc-runtime-new.mm
文件裏與_objc_msgForward
有關的三個函數使用僞代碼展現下:
id objc_msgSend(id self, SEL op, ...) {
if (!self) return nil;
IMP imp = class_getMethodImplementation(self->isa, SEL op);
imp(self, op, ...); //調用這個函數,僞代碼...
}
//查找IMP
IMP class_getMethodImplementation(Class cls, SEL sel) {
if (!cls || !sel) return nil;
IMP imp = lookUpImpOrNil(cls, sel);
if (!imp) return _objc_msgForward; //_objc_msgForward 用於消息轉發
return imp;
}
IMP lookUpImpOrNil(Class cls, SEL sel) {
if (!cls->initialize()) {
_class_initialize(cls);
}
Class curClass = cls;
IMP imp = nil;
do { //先查緩存,緩存沒有時重建,仍舊沒有則向父類查詢
if (!curClass) break;
if (!curClass->cache) fill_cache(cls, curClass);
imp = cache_getImp(curClass, sel);
if (imp) break;
} while (curClass = curClass->superclass);
return imp;
}
複製代碼
雖然Apple沒有公開_objc_msgForward的實現源碼,可是咱們仍是能得出結論:
_objc_msgForward
是一個函數指針(和IMP
的類型同樣),是用於消息轉發的:當向一個對象發送一條消息,但它並無實現的時候,_objc_msgForward
會嘗試作消息轉發。objc_msgSend
在「消息傳遞」中的做用: 在「消息傳遞」過程當中,objc_msgSend
的動做比較清晰: (1) 首先在Class
中的緩存查找IMP
(沒緩存則初始化緩存), (2) 若是沒找到,則向父類的Class
查找。 (3) 若是一直查找到根類仍舊沒有實現,則用_objc_msgForward
函數指針代替IMP
。 (4) 最後,執行這個IMP
。
爲了展現消息轉發的具體動做,這裏嘗試向一個對象發送一條錯誤的消息,並查看一下_objc_msgForward
是如何進行轉發的。
首先開啓調試模式、打印出全部運行時發送的消息: 能夠在代碼裏執行下面的方法:(void)instrumentObjcMessageSends(YES);
由於該函數處於 objc-internal.h 內,而該文件並不開放,因此調用的時候先聲明,目的是告訴編譯器程序目標文件包含該方法存在,讓編譯經過:
OBJC_EXPORT void
instrumentObjcMessageSends(BOOL flag)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
複製代碼
或者斷點暫停程序運行,並在 gdb 中輸入下面的命令:call (void)instrumentObjcMessageSends(YES)
以第二種爲例,操做以下所示:
以後,運行時發送的全部消息都會打印到/tmp/msgSend-xxxx文件裏了。
終端中輸入命令前往:open /private/tmp
可能看到有多條,找到最新生成的,雙擊打開
在模擬器上執行執行如下語句(這一套調試方案僅適用於模擬器,真機不可用,關於該調試方案的拓展連接: Can the messages sent to an object in Objective-C be monitored or printed out? ),向一個對象發送一條錯誤的消息:
結合《NSObject官方文檔》,排除掉 NSObject 作的事,剩下的就是_objc_msgForward消息轉發作的幾件事:
- 調用
resolveInstanceMethod:
方法 (或resolveClassMethod
:)。容許用戶在此時爲該Class
動態添加實現。若是有實現了,則調用並返回YES
,那麼從新開始objc_msgSend
流程。這一次對象會響應這個選擇器,通常是由於它已經調用過class_addMethod
。若是仍沒實現,繼續下面的動做。- 調用
forwardingTargetForSelector
:方法,嘗試找到一個能響應該消息的對象。若是獲取到,則直接把消息轉發給它,返回非 nil 對象。不然返回 nil ,繼續下面的動做。注意,這裏不要返回self
,不然會造成死循環。- 調用
methodSignatureForSelector
:方法,嘗試得到一個方法簽名。若是獲取不到,則直接調用doesNotRecognizeSelector
拋出異常。若是能獲取,則返回非nil
:建立一個NSlnvocation
並傳給forwardInvocation
:。- 調用
forwardInvocation
:方法,將第3步獲取到的方法簽名包裝成Invocation
傳入,如何處理就在這裏面了,並返回非nil。- 調用
doesNotRecognizeSelector
: ,默認的實現是拋出異常。若是第3步沒能得到一個方法簽名,執行該步驟。
上面前4個方法均是模板方法,開發者能夠override
,由 runtime
來調用。最多見的實現消息轉發:就是重寫方法3和4,吞掉一個消息或者代理給其餘對象都是沒問題的。 也就是說_objc_msgForward在進行消息轉發的過程當中會涉及如下這幾個方法:
- resolveInstanceMethod:方法 (或 resolveClassMethod:)。
- forwardingTargetForSelector:方法
- methodSignatureForSelector:方法
- forwardInvocation:方法
- doesNotRecognizeSelector: 方法
![]()
直接調用_objc_msgForward是很是危險的事,若是用很差會直接致使程序Crash,可是若是用得好,能作不少很是酷的事。
就好像跑酷,幹得好,叫「耍酷」,幹很差就叫「做死」。
正如前文所說:_objc_msgForward
是 IMP
類型,用於消息轉發的:當向一個對象發送一條消息,但它並無實現的時候,_objc_msgForward
會嘗試作消息轉發。
如何調用_objc_msgForward? _objc_msgForward隸屬 C 語言,有三個參數 :
-- | _objc_msgForward參數 | 類型 |
---|---|---|
1 | 所屬對象 | id類型 |
2 | 方法名 | SEL類型 |
3 | 可變參數 | 可變參數類型 |
首先了解下如何調用 IMP 類型的方法,IMP類型是以下格式:
爲了直觀,咱們能夠經過以下方式定義一個 IMP類型 :
typedef void (*voidIMP)(id, SEL, ...)
複製代碼
一旦調用_objc_msgForward
,將跳過查找 IMP
的過程,直接觸發「消息轉發」,
若是調用了_objc_msgForward
,即便這個對象確實已經實現了這個方法,你也會告訴objc_msgSend
:「我沒有在這個對象裏找到這個方法的實現」
想象下objc_msgSend
會怎麼作?一般狀況下,下面這張圖就是你正常走objc_msgSend
過程,和直接調用_objc_msgForward
的先後差異:
不能向編譯後獲得的類中增長實例變量;能向運行時建立的類中添加實例變量;
由於編譯後的類已經註冊在runtime中,類結構體中的objc_ivar_list 實例變量的鏈表和instance_size實例變量的內存大小已經肯定,同時runtime 會調用class_setIvarLayout 或 class_setWeakIvarLayout來處理strong weak引用,因此不能向存在的類中添加實例變量 運行時建立的類是能夠添加實例變量,調用 class_addIvar函數,可是得在調用objc_allocateClassPair以後,objc_registerClassPair以前,緣由同上。
Runtime 鑄就了Objective-C 是動態語言的特性,使得C語言具有了面向對象的特性,在程序運行期建立,檢查,修改類、對象及其對應的方法,這些操做均可以使用runtime中的對應方法實現。
Objective-C是動態語言,每一個方法在運行時會被動態轉爲消息發送,即:objc_msgSend(receiver, selector),整個過程介紹以下:
- objc在向一個對象發送消息時,runtime庫會根據對象的isa指針找到該對象實際所屬的類
- 而後在該類中的方法列表以及其父類方法列表中尋找方法運行
- 若是,在最頂層的父類(通常也就NSObject)中依然找不到相應的方法時,程序在運行時會掛掉並拋出異常unrecognized selector sent to XXX
- 可是在這以前,objc的運行時會給出三次拯救程序崩潰的機會。這三次機會分別是: (1)動態方法解析過程當中的:對象方法動態解析(
+(BOOL)resolveInstanceMethod:(SEL)sel
)和 類方法動態解析 (+(BOOL)resolveClassMethod:(SEL)sel
) (2)若是動態解析失敗,則會進入消息轉發流程,消息轉發又分爲:快速轉發和慢速轉發兩種方式。 (3)快速轉發的實現是forwardingTargetForSelector
,讓其餘能響應要查找消息的對象來幹活。 (4)慢速轉發的實現是methodSignatureForSelector
和forwardInvocation
的結合,提供了更細粒度的控制,先返回方法簽名給Runtime
,而後讓anInvocation
來把消息發送給提供的對象,最後由Runtime
提取結果真後傳遞給原始的消息發送者。 (5)若是在3次挽救機會:resolveInstanceMethod
,forwardingTargetForSelector
,forwardInvocation
都沒有處理時,就會報unrecognized selector sent to XXX
異常。此時程序會崩潰。
resolveInstanceMethod
方法又稱爲對象方法動態解析,它的流程大體以下:
- 檢查是否實現了
+(BOOL)resolveInstanceMethod:(SEL)sel
類方法,若是沒有實現則直接返回(經過cls->ISA()
是拿到元類,由於類方法是存儲在元類上的對象方法)- 若是當前實現了
+(BOOL)resolveInstanceMethod:(SEL)sel
類方法,則經過objc_msgSend
手動調用該類方法。- 完成調用後,再次查詢
cls
中的imp
。- 若是
imp
找到了,則輸出動態解析對象方法成功的日誌。- 若是
imp
沒有找到,則輸出雖然實現了+(BOOL)resolveInstanceMethod:(SEL)sel
,而且返回了YES
。
對應源碼以下:
static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// Resolver not implemented.
return;
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveInstanceMethod adds to self a.k.a. cls
IMP imp = lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/);
if (resolved && PrintResolving) {
if (imp) {
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}
複製代碼
_class_resolveClassMethod
走類方法動態解析流程:
- 判斷是不是元類,若是不是,直接退出。
- 檢查是否實現了
+(BOOL)resolveClassMethod:(SEL)sel
類方法,若是沒有實現則直接返回(經過cls-
是由於當前cls
就是元類,由於類方法是存儲在元類上的對象方法)- 若是當前實現了
+(BOOL)resolveClassMethod:(SEL)sel
類方法,則經過objc_msgSend
手動調用該類方法,注意這裏和動態解析對象方法不一樣,這裏須要經過元類和對象來找到類,也就是_class_getNonMetaClass
。- 完成調用後,再次查詢
cls
中的imp
。- 若是
imp
找到了,則輸出動態解析對象方法成功的日誌。- 若是
imp
沒有找到,則輸出雖然實現了+(BOOL)resolveClassMethod:(SEL)sel
,而且返回了YES
,但並無查找到imp
的日誌
對應源碼以下:
static void _class_resolveClassMethod(Class cls, SEL sel, id inst)
{
assert(cls->isMetaClass());
if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// Resolver not implemented.
return;
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(_class_getNonMetaClass(cls, inst),
SEL_resolveClassMethod, sel);
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveClassMethod adds to self->ISA() a.k.a. cls
IMP imp = lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/);
if (resolved && PrintResolving) {
if (imp) {
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveClassMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}
複製代碼
forwardingTargetForSelector
方法 又稱爲快速消息轉發:
forwardingTargetForSelector
是一種快速的消息轉發流程,它直接讓其餘對象來響應未知的消息。forwardingTargetForSelector
不能返回self
,不然會陷入死循環,由於返回self
又回去當前實例對象身上走一遍消息查找流程,顯然又會來到forwardingTargetForSelector
。forwardingTargetForSelector
適用於消息轉發給其餘能響應未知消息的對象,也就是最終返回的內容必須和要查找的消息的參數和返回值一致,若是想要不一致,就須要走其餘的流程。
快速消息轉發是經過彙編來實現的,根據 lookUpImpOrForward
源碼咱們能夠看到當動態解析沒有成功後,會直接返回一個 _objc_msgForward_impcache
。 在源碼中全局搜索_objc_msgForward_impcache
能夠定位到objc-msg-arm64.s
彙編源碼 以下:
STATIC_ENTRY __objc_msgForward_impcache
// No stret specialization.
b __objc_msgForward
END_ENTRY __objc_msgForward_impcache
ENTRY __objc_msgForward
adrp x17, __objc_forward_handler@PAGE
ldr p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17
END_ENTRY __objc_msgForward
複製代碼
forwardInvocation
對應慢速消息轉發 methodSignatureForSelector
方法簽名:
forwardInvocation
方法有兩個任務: (1): 查找能夠響應inInvocation
中編碼的消息的對象。對於全部消息,此對象沒必要相同。 (2): 使用anInvocation
將消息發送到該對象。anInvocation
將保存結果,運行時系統將提取結果並將其傳遞給原始發送者。forwardInvocation
方法的實現不只僅能夠轉發消息。forwardInvocation
還能夠用於合併響應各類不一樣消息的代碼,從而避免了必須爲每一個選擇器編寫單獨方法的麻煩。forwardInvocation
方法在對給定消息的響應中還可能涉及其餘幾個對象,而不是僅將其轉發給一個對象。NSObject
的forwardInvocation
實現:只會調用dosNotRecognizeSelector
:方法,它不會轉發任何消息。所以,若是選擇不實現`forwardInvocation,將沒法識別的消息發送給對象將引起異常。
下面舉例來講明: 假設咱們調用[dog walk]方法,那麼它會經歷以下過程:
- 編譯器會把
[dog walk]
轉化爲objc_msgSend(dog,SEL)
,SEL爲@selector(walk)。- Runtime會在dog對象所對應的Dog類的方法緩存列表裏查找方法的SEL
- 若是沒有找到,則在Dog類的方法分發表查找方法的SEL。(類由對象isa指針指向,方法分發表即methodList)
- 若是沒有找到,則在其父類(設Dog類的父類爲Animal類)的方法分發表裏查找方法的SEL(父類由類的superClass指向)
- 若是沒有找到,則沿繼承體系繼續下去,最終到達NSObject類。
- 若是在234的其中一步中找到,則定位了方法實現的入口,執行具體實現
- 若是最後仍是沒有找到,會面臨兩種狀況:``(1) 若是是使用
[dog walk]
的方式調用方法````(2) 使用[dog performSelector:@selector(walk)]
的方式調用方法`
若是是一個沒有定義的方法,那麼它會經歷動態方法解析,消息轉發流程
- 根據對象的isa去對應的類查找方法,isa:判斷去哪一個類查找對應的方法 指向方法調用的類
- 根據傳入的方法編號SEL,裏面有個哈希列表,在列表中找到對應方法Method(方法名)
- 根據方法名(函數入口)找到函數實現,函數實如今方法區
簡單說就是進行方法交換
- 在Objective-C中調用一個方法,實際上是向一個對象發送消息,查找消息的惟一依據是selector的名字。利用Objective-C的動態特性,能夠實如今運行時偷換selector對應的方法實現,達到給方法掛鉤的目的
- 每一個類都有一個方法列表,存放着方法的名字和方法實現的映射關係,selector的本質其實就是方法名,IMP有點相似函數指針,指向具體的Method實現,經過selector就能夠找到對應的IMP
![]()
- 交換方法的幾種實現方式: (1) 利用 method_exchangeImplementations 交換兩個方法的實現 (2)利用 class_replaceMethod 替換方法的實現 (3)利用 method_setImplementation 來直接設置某個方法的IMP
![]()
有三種方法解決這個需求問題:
- 解決方式 自定義UIImage類,缺點:每次用要導入本身的類
- 解決方法:UIImage分類擴充一個這樣方法,缺點:須要導入,沒法寫super和self,會幹掉系統方法,解決:給系統方法加個前綴,與系統方法區分,如:xmg_imageNamed:
- 交互方法實現,步驟: 1.提供分類 2.寫一個有這樣功能方法 3.用系統方法與這個功能方法交互實現,在+load方法中實現
若是用方法2,每一個調用imageNamed方法的,都要改爲xmg_imageNamed:才能擁有這個功能,很麻煩。解決:用runtime交換方法 就比較好。
注意:在分類必定不要重寫系統方法,就直接把系統方法幹掉,若是真的想重寫,在系統方法前面加前綴,方法裏面去調用系統方法
思想:何時須要自定義,系統功能不完善,就自定義一個這樣類,去擴展這個類
方法交換實現代碼以下:
/#import "UIImage+Image.h"
/#import <objc/message.h>
@implementation UIImage (Image)
// 加載類的時候調用,確定只會調用一次
+(void)load
{
// 交互方法實現xmg_imageNamed,imageNamed
/** 獲取類方法名 @param Class cls,#> 獲取哪一個類方法 description#> @param SEL name#> 方法編號 description#> @return 返回Method(方法名) class_getClassMethod(<#__unsafe_unretained Class cls#>, <#SEL name#>) */
/** 獲取對象方法名 @param Class cls,#> 獲取哪一個對象方法 description#> @param SEL name#> 方法編號 description#> @return 返回Method(方法名) class_getInstanceMethod(<#__unsafe_unretained Class cls#>, <#SEL name#>) */
Method imageNameMethod = class_getClassMethod(self, @selector(imageNamed:));
Method xmg_imageNameMethod = class_getClassMethod(self, @selector(xmg_imageNamed:));
//用runtime對imageNameMethod和xmg_imageNameMethod方法進行交換
method_exchangeImplementations(imageNameMethod, xmg_imageNameMethod);
}
//外界調用imageNamed:方法,實際上是調用下面方法,調用xmg_imageNamed就是調用imageNamed:
+ (UIImage *)xmg_imageNamed:(NSString *)name
{
//已經把xmg_imageNamed換成imageNamed,因此下面實際上是調用的imageNamed:
UIImage *image = [UIImage xmg_imageNamed:name];
if (image == nil) {
NSLog(@"加載失敗");
}
return image;
}
@end
複製代碼
上面主要是關於runtime流程的一些問題,接下來會有更加深刻的須要深刻理解objc4相關的源碼。其中關於消息轉發機制多是最多見的問題了。
- 介紹下runtime的內存模型(isa、對象、類、metaclass、結構體的存儲信息等)
- 爲何要設計metaclass
- class_copyIvarList & class_copyPropertyList區別
- class_rw_t 和 class_ro_t 的區別
- category如何被加載的,兩個category的load方法的加載順序,兩個category的同名方法的加載順序
- category & extension區別,能給NSObject添加Extension嗎,結果如何
- 消息轉發機制,消息轉發機制和其餘語言的消息機制優劣對比
- 在方法調用的時候,方法查詢-> 動態解析-> 消息轉發 以前作了什麼
- IMP、SEL、Method的區別和使用場景
- load、initialize方法的區別什麼?在繼承關係中他們有什麼區別
- 說說消息轉發機制的優劣
實例對象 isa 指向類對象 類對象指 isa 向元類對象 元類對象的 isa 指向元類的基類
純指針,指向內存地址 NON_POINTER_ISA,除了內存地址,還存有一些其餘信息
union isa_t
{
Class cls;
uintptr_t bits;
# if __arm64__ // arm64架構
# define ISA_MASK 0x0000000ffffffff8ULL //用來取出33位內存地址使用(&)操做
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
struct {
uintptr_t nonpointer : 1; //0:表明普通指針,1:表示優化過的,能夠存儲更多信息。
uintptr_t has_assoc : 1; //是否設置過關聯對象。若是沒設置過,釋放會更快
uintptr_t has_cxx_dtor : 1; //是否有C++的析構函數
uintptr_t shiftcls : 33; // MACH_VM_MAX_ADDRESS 0x1000000000 內存地址值
uintptr_t magic : 6; //用於在調試時分辨對象是否未完成初始化
uintptr_t weakly_referenced : 1; //是否有被弱引用指向過
uintptr_t deallocating : 1; //是否正在釋放
uintptr_t has_sidetable_rc : 1; //引用計數器是否過大沒法存儲在ISA中。若是爲1,那麼引用計數會存儲在一個叫作SideTable的類的屬性中
uintptr_t extra_rc : 19; //裏面存儲的值是引用計數器減1
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
};
# elif __x86_64__ // arm86架構,模擬器是arm86
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
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;
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
};
# else
# error unknown architecture for packed isa
# endif
}
複製代碼
typedef struct objc_class *Class; struct objc_class {
Class isa OBJC_ISA_AVAILABILITY; //isa指針,指向metaclass(該類的元類)
#if !__OBJC2__
Class super_class //指向objc_class(該類)的super_class(父類)
const char *name //objc_class(該類)的類名
long version //objc_class(該類)的版本信息,初始化爲0,能夠經過runtime函數class_setVersion和class_getVersion進行修改和讀取
long info //一些標識信息,如CLS_CLASS表示objc_class(該類)爲普通類。ClS_CLASS表示objc_class(該類)爲metaclass(元類)
long instance_size //objc_class(該類)的實例變量的大小
struct objc_ivar_list *ivars //用於存儲每一個成員變量的地址 struct objc_method_list **methodLists //方法列表,與info標識關聯 struct objc_cache *cache //指向最近使用的方法的指針,用於提高效率 struct objc_protocol_list *protocols //存儲objc_class(該類)的一些協議 #endif } OBJC2_UNAVAILABLE; typedef struct objc_object *id; struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
複製代碼
struct
objc_classs
結構體裏存放的數據稱爲元數據(metadata
),經過成員變量的名稱咱們能夠猜想裏面存放有指向父類的指針、類的名字、版本、實例大小、實例變量列表、方法列表、緩存、遵照的協議列表等,這些信息就足夠建立一個實例了,該結構體的第一個成員變量也是isa
指針,這就說明了Class
自己其實也是一個對象,咱們稱之爲類對象,類對象在編譯期產生用於建立實例對象,是單例。
objec_object
(對象)中isa指針指向的類結構稱爲objec_class
(該對象的類),其中存放着普通成員變量與對象方法 (「-」開頭的方法)。
objec_class
(類)中isa
指針指向的類結構稱爲metaclass
(該類的元類),其中存放着static
類型的成員變量與static
類型的方法 (「+」開頭的方法)。
元類(metaclass
):在oc中,每個類實際上也是一個對象。也有一個isa指針。由於類也是一個對象,因此也必須是另一個類的實例,這個類就是元類(metaclass
),即元類的對象就是這個類,元類保存了類方法的列表
在oc語言中,每個類實際上也是一個對象。每個類也有一個isa指針。每個類也能夠接收消息。由於類也是一個對象,因此也是另一個類的實例,這個類就是元類(metaclass
)。元類也是一個對象,全部的元類的isa指針都會指向一個根元類。根元類的isa指針又會指向他本身,這樣就造成了一個閉環。
以下圖展現了類的繼承和isa指向的關係:
isa指針的指向:
- 一個objc對象的
isa
指針指向他的類對象,類對象的isa指針指向他的元類,元類的isa指針指向根元類,全部的元類isa都指向同一個根元類,根元類的isa指針指向根元類自己。根元類super class
父類指向NSObject
類對象。根metaclass
(元類)中的superClass
指針指向根類,由於根metaclass
(元類)是經過繼承根類產生的。- 實例對象的
isa
指針, 指向他的類對象,類對象的isa
指針, 指向他的元類。系統判斷一個對象屬於哪一個類,也是經過這個對象的isa
指針的指向來判斷。對象中的成員變量,存儲在對象自己,對象的實例方法,存儲在他的isa
指針所指向的對象中。- 對象在調用減號方法的時候,系統會在對象的
isa
指針所指向的類對象中找方法,這一段在kvo
的實現原理中就能看到,kvo
的實現原理就是系統動態的生成一個類對象,這個類是監聽對象的類的子類,在生成的子類中重寫了監聽屬性的set方法,以後將監聽對象的isa
指針指向系統動態生成的這個類,當監聽對象調用set方法時,因爲監聽對象的isa
指針指向的是剛剛動態生成的類,因此在執行的set方法也是子類中重寫的set方法,這就是kvo的實現原理。同理,咱們也能夠經過runtime
中的方法設置某個對象isa
指針指向的類對象,讓對象調用一些本來不屬於他的方法。
- 類對象由編譯器建立。任何直接或間接繼承了
NSObject
的類,它的實例對象中都有一個isa
指針,指向它的類對象。這個類對象中存儲了關於這個實例對象所屬的類的定義的一切:包括變量,方法,遵照的協議等等。所以,類對象能訪問全部關於這個類的信息,利用這些信息能夠產生一個新的實例,可是類對象不能訪問任何實例對象的內容。類對象沒有本身的實例變量。
例如:咱們建立p對象:Person * p = [Person new] ;
在建立一個對象p以前,在堆內存中就先存在了一個類(Person)(類對象),類對象在編繹時系統會爲咱們自動建立。在類第一次加載進內存時建立。
建立一個對象以後,在堆內存中會建立了一個p對象,該對象包含了一個isa
指針的成員變量(第一個屬性),isa
指針則指向在堆裏面存在的類對象, 在棧內存裏建立了一個該類的指針p,p指針指向的是isa
地址,isa
指向Person.
OC調用方法,運行的時候編譯器會將代碼轉化爲
objc_msgSend(obj, @selector (selector))
,在objc_msgSend
函數中首先經過obj
(對象)的isa
指針找到obj
(對象)對應的class(類)。在class
(類)中先去cache
中經過SEL(方法的編號)查找對應method(方法),若cache中未找到,再去methodLists
中查找,若methodists
中未找到,則去superClass
中查找,若能找到,則將method(方法)加入到cache中,以方便下次查找,並經過method(方法)中的函數指針跳轉到對應的函數中去執行。若是仍然找不到,則繼續經過 super_class向上一級父類結構體中查找,直至根class
- 當咱們調用某個類方法時,它首先經過本身的
isa
指針指向的objc_class
中的isa
指針找到元類,並從其methodLists
中查找該類方法,若是找不到則會經過元類)的super_class指針找到父類的元類結構體,而後從methodLists
中查找該方法,若是仍然找不到,則繼續經過super_class
向上一級父類結構體中查 找,直至根元類;- C語言函數編譯的時候就會決定調用哪一個函數,OC是一種動態語言,他會盡量把代碼的從編譯連接推遲到運行時,這就是oc運行時多態。 給一個對象發送消息,並不會當即執行,而是在運行的時候在去尋找他對應的實現而OC的函數,屬於動態調用過程,在編譯期並不能決定真正調用哪一個函數,只有在真正運行時纔會根據函數的名稱找到對應的函數來調用。
參考:www.jianshu.com/p/8345a79fd… juejin.im/post/5e0dbb… github.com/ChenYilong/… www.jianshu.com/p/0bf8787db…