目錄
runtime相關問題之內存部分的關聯屬性或者hook相關的Method Swizzle
經過前兩期內容 我們這期來講一下 內存部分的剩餘問題 主要包含如下:
-
Method Swizzle
注意事項 - 屬性修飾符atomic的內部實現是怎麼樣的?能保證線程安全嗎
- iOS 中內省的幾個方法有哪些?內部實現原理是什麼
-
class
、objc_getClass
、object_getclass
方法有什麼區別?
Method Swizzle
注意事項
-
需要注意的是交換方法實現後的副作用,
method_exchangeImplementations()
.交換方法函數最終會以objc_msgSend()
方式調用,副作用主要集中在第一個參數 如下示例
objc_msgSend(payment, @selector(quantity))
方法交換後再去調用quantity方法將有可能會crash.解決這種副作用的方式是使用method_setImplementation()
來替換原來的交換方式,這樣才最爲合理, 具體原理請參照 Objc 黑科技 - Method Swizzle 的一些注意事項
-
避免交換父類方法
如果當前類沒有實現被交換的方法且父類實現了,此時父類的實現會被交換,若此父類的多個繼承者都在交換時會引起多次交換導致混亂,同時調用父類方法有可能因爲找不到方法簽名而crash.
所以交換前都應該check能否爲當前類添加被交換的函數的新的實現IMP,這個過程大概分爲3步驟-
class_addMethod
check能否添加方法
-
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
給類cls的SEL添加一個實現IMP, 返回YES則表明類cls並未實現此方法,返回NO則表明類已實現了此方法。注意:添加成功與否,完全由該類本身來決定,與父類有無該方法無關。
-
class_replaceMethod
替換類cls的SEL的函數實現爲imp
class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
const char * _Nullable types)
-
method_exchangeImplementations
最終方法交換
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)
- 交換方法應在+load方法
這個前面講消息轉發的時候講過,+load不是消息轉發的方式實現的且在運行時初始化過程中類被加載的時候調用,而且父類,當前類,category,子類等 都會調用一次.所以這裏最適合寫方法交換的hook(Method Swizzle).
-
交換的分類方法應該添加自定義前綴,避免衝突
這個毫無疑問,方法名稱一樣的時候會出現,分類的方法會覆蓋類中同名的方法.
屬性修飾符atomic的內部實現是怎麼樣的?能保證線程安全嗎?
atomic內部實現
id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
...
id *slot = (id*) ((char*)self + offset);
if (!atomic) return *slot;
// Atomic retain release world
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
id value = objc_retain(*slot);
slotlock.unlock();
return objc_autoreleaseReturnValue(value);
}
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
...
if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
objc_release(oldValue);
}
property
的 atomic
是採用 spinlock_t
自旋鎖實現的.
能保證線程安全嗎?
atomic
通過這種方法.在運行時僅僅是保證了set
,get
方法的原子性.所以使用atomic並不能保證線程安全。
iOS 中內省的幾個方法有哪些?內部實現原理是什麼?
首先要明白一個名詞 introspection
反省,內省的意思,在iOS開發中我們會稱它爲反射.
內省方法 例如常用的NSObject
中的isKindOfClass:
通過實例對象判斷class
這就是一種內省方法或者叫反射方法,但我認爲NSClassFromString()
這個應該也算一種反射方法.
iOS 中內省的幾個方法
我們從NSObject.h中看下吧
- (BOOL)isKindOfClass:(Class)aClass; //判斷是否是這個類或者這個類的子類的實例
- (BOOL)isMemberOfClass:(Class)aClass; //判斷是否是這個類的實例
- (BOOL)conformsToProtocol:(Protocol *)aProtocol; //判斷是否遵守某個協議
+ (BOOL)conformsToProtocol:(Protocol *)protocol; //判斷某個類是否遵守某個協議
- (BOOL)respondsToSelector:(SEL)aSelector; //判讀實例是否有這樣方法
+ (BOOL)instancesRespondToSelector:(SEL)aSelector; //判斷類是否有這個方法
...
內部實現原理
1.isKindOfClass:
+ (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = self->ISA(); tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
- (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
類方法是通過ISA()函數拿到指向元類的存儲isa指針數據的地址bit位按位與上相關掩碼的方式判斷當前是否是某個類的子類.
實例方法是通過objc_object::getIsa()
函數通過存儲的tag_ext
表形式拿到isa對於的class來取出class平check來實現的.
2.isMemberOfClass:
+ (BOOL)isMemberOfClass:(Class)cls {
return self->ISA() == cls;
}
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}
這倆方法非常簡單直接 拿到isa指針對比
3.conformsToProtocol:
+ (BOOL)conformsToProtocol:(Protocol *)protocol {
if (!protocol) return NO;
for (Class tcls = self; tcls; tcls = tcls->superclass) {
if (class_conformsToProtocol(tcls, protocol)) return YES;
}
return NO;
}
- (BOOL)conformsToProtocol:(Protocol *)protocol {
if (!protocol) return NO;
for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
if (class_conformsToProtocol(tcls, protocol)) return YES;
}
return NO;
}
兩個方法最終還是去isa->data()->protocols 拿到相關協議然後判斷是否存在相關協議 如下代碼:
BOOL class_conformsToProtocol(Class cls, Protocol *proto_gen)
{
protocol_t *proto = newprotocol(proto_gen);
if (!cls) return NO;
if (!proto_gen) return NO;
mutex_locker_t lock(runtimeLock);
checkIsKnownClass(cls);
ASSERT(cls->isRealized())
for (const auto& proto_ref : cls->data()->protocols) {
protocol_t *p = remapProtocol(proto_ref);
if (p == proto || protocol_conformsToProtocol_nolock(p, proto)) {
return YES;
}
}
return NO;
}
這裏可以清晰的看到for循環 取出相關protocol指針 然後通過指針和傳入的參數生成的
proto
對比
4.respondsToSelector:
+ (BOOL)respondsToSelector:(SEL)sel {
return class_respondsToSelector_inst(self, sel, self->ISA());
}
- (BOOL)respondsToSelector:(SEL)sel {
return class_respondsToSelector_inst(self, sel, [self class]);
}
這個源碼比較麻煩 我簡單敘述一下吧 實際上調用棧比較深就是一直尋找到當前實例能響應哪些方法,當前類沒有就去父類,父類沒有則直到元類.
respondsToSelector:
|__ class_respondsToSelector_inst()
|__ lookUpImpOrNil()
|__ lookUpImpOrForward()
返回IMP結果
這就是整個消息轉發的過程 就不在這裏贅述了.感興趣回看一下第二章 消息轉發部分
我上述列舉了一些常用的內省方法,其它的都方法基本沒什麼特別之處都是拿到isa各種操作內部的獲取相關屬性的函數返回結.
class
、objc_getClass
、object_getclass
方法有什麼區別?
我用xcode隨便建了一個demo 打印一下viewcontrooller的內容
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Class cls1 = [self class];
Class cls2 = object_getClass(cls1);
Class cls3 = objc_getClass(object_getClassName([self class]));
NSLog(@"%p",cls1);
NSLog(@"%p",cls2);
NSLog(@"%p",cls3);
}
@end
輸出
2020-08-31 16:15:48.150285+0800 ClassDemo[5582:55836] 0x10205b3b0
2020-08-31 16:15:48.150456+0800 ClassDemo[5582:55836] 0x10205b3d8
2020-08-31 16:15:48.150575+0800 ClassDemo[5582:55836] 0x10205b3b0
我簡單列舉了一張表格
class |
object_getclass() |
objc_getClass() |
|
---|---|---|---|
傳入參數 | N/a | id類型 | 類名的字符串 |
操作對象 | obj | 這個id的isa指針所指向的Class | 這個類的類對象 |
實例對象時 | 和object_getclass() 一致 |
和class 一致 |
N/a |
類對象/元類對象時 | 返回的消息對象本身 | 返回的是下一個對象 | N/a |
原因:因爲class返回的是self,而object_getClass返回的是isa指向的對象
總結
以上就是"一套高效的iOS面試題之runtime相關問題3"中的內存剩餘部分,問題答案雖然簡短 但是每道題都問的非常到位,值得一看!
推薦
收錄:原文地址