為什么不能runtime創建JSExport類型的Protocol?

JavaScriptCore引入后,js調用OC的方法有了新的實現方式。讓一個類遵循一個JSExport的協議,將想要暴露的方法在JSExport協議中聲明,即可在js中直接調用到OC的方法。
例如下面的代碼:

#import <JavaScriptCore/JavaScriptCore.h>

@protocol NSTestExport <JSExport>
-(NSString *)str;
-(void)setStr:(NSString *)str;
@end

@interface ViewController : UIViewController<NSTestExport>
@property (nonatomic, strong) NSString *str;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    _str = @"asdf";
        
    context = [[JSContext alloc] init];
    
    context.exceptionHandler = ^(JSContext *context, JSValue *exception) {
        NSLog(@"exception: %@",exception);
    };
    
    context[@"log"] = ^(NSString *msg){
        NSLog(@"log msg: %@",msg);
    };
    
    context[@"viewController"] = self;
    
    [context evaluateScript:@"viewController.setStr('dsfg')"];
    JSValue *string = [context evaluateScript:@"viewController.str()"];

     NSLog(@"log str: %@",[result toString]);
}

但是這樣的實現并不靈活,如果我有大量的類和其中的方法要在js中使用,那么我可能需要實現大量的JSExport協議,這樣會導致項目中的代碼量大大增加。于是想到了可以使用runtime動態創建這些協議,然后添加到class中。

1、首先讓我們制作一個擴展JSExport的新協議,假設我們有一個Class class我們要導出的變量:

const char *protocolName = class_getName(class);
    Protocol *protocol = objc_allocateProtocol(protocolName);
    protocol_addProtocol(protocol, objc_getProtocol("JSExport"));

2、然后我們從類中讀取方法列表和屬性列表,并將它們添加到protocol中:
實例方法:

{
    NSUInteger methodCount, classMethodCount;
    Method *methods, *classMethods;
    methods = class_copyMethodList(class, &methodCount);
    for (NSUInteger methodIndex = 0; methodIndex < methodCount; ++methodIndex) {
        Method method = methods[methodIndex];
        protocol_addMethodDescription(protocol, method_getName(method), method_getTypeEncoding(method), YES, YES);
    }
}

類方法:

{
    classMethods = class_copyMethodList(object_getClass(class), &classMethodCount);
    for (NSUInteger methodIndex = 0; methodIndex < classMethodCount; ++methodIndex) {
        Method method = classMethods[methodIndex];
        protocol_addMethodDescription(protocol, method_getName(method), method_getTypeEncoding(method), YES, NO);
    }
}

屬性:
添加屬性的方法基本和添加方法相同,但是我們還需要獲取每個屬性的特性,并添加到協議中

{
    NSUInteger propertyCount;
    objc_property_t *properties;
    properties = class_copyPropertyList(class, &propertyCount);
    for (NSUInteger propertyIndex = 0; propertyIndex < propertyCount; ++propertyIndex) {
        objc_property_t property = properties[propertyIndex];
        NSUInteger attributeCount;
//每個屬性的特性
        objc_property_attribute_t *attributes = property_copyAttributeList(property, &attributeCount);
        protocol_addProperty(protocol, property_getName(property), attributes, attributeCount, YES, YES);
        free(attributes);
    }
}

3、將新協議添加到類中

objc_registerProtocol(protocol);
//校驗protocol是否遵循JSExport協議
BOOL conform = protocol_conformsToProtocol(protocol, @protocol(JSExport));
NSLog(@"conform: %d",conform);
        
BOOL success = class_addProtocol(class, protocol);

4、然后理論上我們應該是可以在js中使用這個類中的方法了,接下來使用下面的代碼測試下。

context = [[JSContext alloc] init];
    
    context.exceptionHandler = ^(JSContext *context, JSValue *exception) {
        NSLog(@"exception: %@",exception);
    };
        
    context[@"viewController"] = self;
    
    [context evaluateScript:@"viewController.setStr('dsfg')"];

然后我們發現,js的執行拋了異常。為什么呢?我們的實現邏輯并沒有問題。
這里查看JavaScriptCore源代碼。

最后發現原因在與objCCallbackFunctionForMethod方法,改函數通過調用objCCallbackFunctionForInvocation返回了一個原生函數的指針JSObjectRef。objCCallbackFunctionForInvocation函數的調用語句如下:

objCCallbackFunctionForInvocation(context, invocation, isInstanceMethod ? CallbackInstanceMethod : CallbackClassMethod, isInstanceMethod ? cls : nil, _protocol_getMethodTypeEncoding(protocol, sel, YES, isInstanceMethod))。

這里使用了_protocol_getMethodTypeEncoding函數。到ObjcRuntimeExtras.h中看看函數的定義。

// Forward declare some Objective-C runtime internal methods that are not API.
const char *_protocol_getMethodTypeEncoding(Protocol *, SEL, BOOL isRequiredMethod, BOOL isInstanceMethod);

再到https://opensource.apple.com/source/objc4/objc4-551.1/runtime/objc-runtime-new.mm中找到了實現:

/***********************************************************************
 * _protocol_getMethodTypeEncoding
 * Return the @encode string for the requested protocol method.
 * Returns nil if the compiler did not emit any extended @encode data.
 * Locking: acquires runtimeLock
 **********************************************************************/
const char *
_protocol_getMethodTypeEncoding(Protocol *proto_gen, SEL sel,
                                BOOL isRequiredMethod, BOOL isInstanceMethod)
{
    protocol_t *proto = newprotocol(proto_gen);
    if (!proto) return nil;
    fixupProtocolIfNeeded(proto);
    const char *enc;
    rwlock_read(&runtimeLock);
    enc = protocol_getMethodTypeEncoding_nolock(proto, sel,
                                                isRequiredMethod,
                                                isInstanceMethod);
    rwlock_unlock_read(&runtimeLock);
    return enc;
}

函數注釋中寫名了,Returns nil if the compiler did not emit any extended @encode data.所以我們只能在編譯階段創建好JSExport

本文作者: ctinusdev
原文鏈接: https://ctinusdev.github.io/2017/08/05/CantnotCreateJSExportAtRuntime/
轉載請注明出處!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,702評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,143評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,553評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,620評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,416評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,940評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,024評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,170評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,709評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,597評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,291評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,029評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,407評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,663評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,403評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,746評論 2 370

推薦閱讀更多精彩內容