一、摘要
1.閱讀該篇,需要對runtime底層及類物件資料結構有一定了解,本篇僅著重講解方法快取的演算法;
2.以下以類物件來論述,元類物件以此類推;
二、類物件資料結構
//rumtime原始碼

//小碼哥圖片

說明:其中cache_t型別變數cache就是用來快取曾經調度過的方法;
三、方法調度原理
Person *per = [[Person alloc] init];
createCaches(ORIGINAL_MASK);
handleMethod("test1", @selector(test1), [per test1]); handleMethod("test2", @selector(test2), [per test2]); handleMethod("test1", @selector(test1), [per test1]); handleMethod("test3", @selector(test3), [per test3]); handleMethod("test4", @selector(test4), [per test4]); handleMethod("test5", @selector(test5), [per test5]); handleMethod("test4", @selector(test4), [per test4]); handleMethod("test6WithHeight:age:", @selector(test6WithHeight:age:), [per test6WithHeight:1.7 age:30]); handleMethod("test7WithName:", @selector(test7WithName:), [per test7WithName:@"張三"]); free(methodCaches);
如上所示:
1.實體物件per調test1/2/3等方法時,runtime底層本質是通過msgSend向per物件發送訊息;
2.系統會通過per的isa指標找到其類物件,然后優先到該類物件的cache里面去查找,如果能找到則直接呼叫;如果沒有找到則再到struct_rw_t中的methods方法串列中查找;如果還沒找到,則通過superClass指標到父類中查找(查找順序同前所述);如果一級父類沒找到,則一直往上級父類查找,直到根父類;如果根父類也沒有,則回傳空;
四、cache快取演算法
1.方法底層結構

說明:cache內部包含三個變數:buckets(散串列),_mask(散串列的長度-1),_occupied(已經快取的方法數量);bucket_t包含兩個變數:類似于字典的鍵值對,_key是
方法SEL(整型資料),_imp快取函式的記憶體地址;
2.演算法思路——散串列(空間換時間):
1)用散串列(即陣列)來快取呼叫的方法,先開辟固定長度的記憶體(此處設定為3),陣列元素則為鍵值對的結構體;
//創建散串列
void createCaches(mask_t mask) { //創建散串列 struct bucket_t *originalBuckets = (struct bucket_t *)malloc(sizeof(struct bucket_t)*mask); for (int i = 0; i < mask; i++) { originalBuckets[i]._name = ""; originalBuckets[i]._key = 0; originalBuckets[i]._imp = NULL; originalBuckets[i]._types = "null"; } methodCaches = (struct cache_t *)malloc(sizeof(struct cache_t)); methodCaches->_mask = (mask_t)(mask-1); methodCaches->_occupied = 0; methodCaches->_buckets = originalBuckets; }
2)用_mask與_key進行按位與運算,得到每個元素的下標index——這樣得出的index不會大于_mask(原因:可以看位域那篇文章),同時為亂數;
3)每次調方法,會先進行按位與計算得出下標A,然后查找該下標位置處是否快取了方法:如果有且快取的方法跟被調方法相同,則直接呼叫快取中的方法;如果沒有,則下標-1進行遍歷查找(為0時,直接回到陣列末尾,再-1繼續查找),直至回到A處,如果找到了,則直接調,如果沒有找到,則將該方法進行快取;
//查找核心代碼
//inline關鍵字:C++關聯函式,表示在呼叫該函式處,直接替換成函式體內的代碼(好處:避免頻繁呼叫該函式導致記憶體消耗) static inline mask_t cache_next(mask_t i, mask_t mask) { return i ? i-1 : mask; } //查找方法 IMP findSEL(SEL selector) { mask_t begin = _mask & (long long)selector; mask_t i = begin; do {//如果查到直接回傳,否則-1往回查找,直到又回到begin位置處 if (_buckets[i]._key == (long long)selector) { return _buckets[i]._imp; } } while ((i = cache_next(i, _mask)) != begin); return NULL;//沒有找到,回傳null }
4)如果A處是空,則直接快取至A處,否則-1查找遍歷空余位置(原理同上);
//快取核心代碼
void saveSEL(char const*method, SEL selector, IMP methodIMP, char const*types) { //散串列是否為空 if (methodCaches->_buckets && methodCaches->_mask+1 > 0) { mask_t begin = methodCaches->_mask & (long long)selector; mask_t i = begin; do { if (methodCaches->_buckets[i]._imp == NULL) { methodCaches->_buckets[i]._name = method; methodCaches->_buckets[i]._key = (long long)selector; methodCaches->_buckets[i]._imp = methodIMP; methodCaches->_buckets[i]._types = types; methodCaches->_occupied++; return ;//保存成功 } } while ((i = cache_next(i, methodCaches->_mask)) != begin); } }
5)如果散串列存滿了,則需擴容:陣列長度擴大2倍,并且會清空散串列,重新做快取操作;
void expandCaches() { //清空記憶體 mask_t lastMask = methodCaches->_mask; free(methodCaches->_buckets); free(methodCaches); mask_t newMaskt = (lastMask+1)*2; createCaches(newMaskt); }
補充:
1)在bucket_t中加入了兩個成員變數:_name(方法閱讀具體調的是哪個方法),_types(描述方法回傳值、形參型別,及所有引數所占位元組總數和每個引數的記憶體起始位置);
2)通過@encode獲取資料型別編碼,_types具體描述如下:
// "i24@0:8i16f20" // 0id 8SEL 16int 20float == 24 /*說明 1.每個方法默認隱式自帶兩個引數:self自身(id型別),@selector()方法(SEL型別); 2.每種型別可通過@encode(型別名稱)指令翻譯; 3.引數含義: 1>符號: i表示回傳值型別; @表示id型別; :表示SEL型別; i(第二個)表示age變數型別; f表示height變數型別; 2>數字: 24表示所有型別所占位元組數:8(id為指標型別)+8(SEL為指標型別)+4(age)+4(height); 0表示self自身是從第零個位元組開始——以此類推:8表示selector從第八個位元組開始,,,height是從第20個位元組開始; */ - (int)test:(int)age height:(float)height;
五、總結
iOS系統runtime方法快取核心思想為:用散串列來快取,用空間來換時間,通過按位與計算來確定方法下標索引;
注意:如果工程打開碰到以下錯誤,則按下面操作解決

GitHub
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/4176.html
標籤:iOS
上一篇:iOS-關于GCD信號量那些事兒
