iOS JSBridge技術(shù)手段衍化整理

背景

之前一段時(shí)間在對(duì)項(xiàng)目里的 JSBridge 進(jìn)行整理和優(yōu)化效五,突然想到想整理一下 JSBridge 在 iOS 系統(tǒng)版本衍化過(guò)程中所出現(xiàn)過(guò)的主要技術(shù)手段韭畸。
絕大多數(shù) APP 都逃不開(kāi) H5 開(kāi)發(fā)或加載網(wǎng)頁(yè)的需求。JSBridge 應(yīng)用于 Web 和 Native 兩端的交互灌旧,所以也是 Hybrid APP (混合移動(dòng)端應(yīng)用)運(yùn)作的核心層級(jí)步绸。
可以說(shuō) JSBridge 是 iOS 開(kāi)發(fā)者的必修課蔚约。

JSBridge

JSBridge 即利用 JavaScript 語(yǔ)言,令 Web 和 Native 兩端可以進(jìn)行交互的橋接層烙如。一個(gè)完整的 JSBridge 方案需要對(duì)所有兩端交互的技術(shù)手段進(jìn)行選型么抗、優(yōu)化和整合。這里我們只討論 iOS 和 Web 端的交互手段亚铁。

JavaScriptCore

任何一個(gè)移動(dòng)端系統(tǒng)的 WebKit 都會(huì)默認(rèn)內(nèi)嵌各自的 JS 引擎蝇刀,它們的工作就是對(duì) JS 腳本進(jìn)行編譯與運(yùn)行( JS 虛擬機(jī),用來(lái)分析詞匯與語(yǔ)法生成 ByteCode (指令字節(jié)碼)并運(yùn)行徘溢,和負(fù)責(zé)運(yùn)行時(shí)的內(nèi)存空間開(kāi)辟吞琐、管理等等)。
JS 引擎是實(shí)現(xiàn) JSBridge 核心然爆,而 iOS/OS 端對(duì)應(yīng)的 JS 引擎是 JavaScriptCore 顽分。



在 iOS 7 以后,蘋(píng)果對(duì) WebKit 中的 JavaScriptCore 框架進(jìn)行 Objective-C 的封裝并提供給開(kāi)發(fā)者施蜜。所以在 iOS 端中卒蘸, JSBridge 使用的技術(shù)手段也以 iOS 7 為臨界點(diǎn)分為兩個(gè)階段。

Before iOS 7

iOS call Web

UIWebView 提供了 stringByEvaluatingJavaScriptFromString:

// NOTE: Returns the result of running a JavaScript script
- (NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;

用來(lái)在當(dāng)前網(wǎng)頁(yè)執(zhí)行一段 JS 腳本并以字符串的形式返回調(diào)用結(jié)果:

// NOTE: 獲取當(dāng)前url
NSString *currentURL = [webView stringByEvaluatingJavaScriptFromString:@"document.location.href"];
// NOTE: 獲取當(dāng)前網(wǎng)頁(yè)標(biāo)題
NSString *title = [webview stringByEvaluatingJavaScriptFromString:@"document.title"]; 

// NOTE: 注入自定義方法jsFunction()
NSString *jsFunction = @";(function() {function jsFunction(){return 'Hello World';})();";
[webView stringByEvaluatingJavaScriptFromString:js];//注入js方法
// NOTE: 調(diào)用自定義方法jsFunction并獲取回調(diào)結(jié)果
NSString *resultString = [webView stringByEvaluatingJavaScriptFromString:@"jsFunction()"];

Web call iOS

攔截 URL

iOS 7 之前翻默,基本上都是使用攔截 URL 的方案來(lái)實(shí)現(xiàn) Web call iOS缸沃。

1 . iframe.src (重定向)

HTML內(nèi)聯(lián)框架元素 <iframe>,將另一個(gè)HTML頁(yè)面嵌入到當(dāng)前頁(yè)面中修械。

function doSend(message, responseCallback) {
    messagingIframe.src = 'mizhua://';
}

- (BOOL)webView:(UIWebView *)webView 
shouldStartLoadWithRequest:(NSURLRequest *)request 
navigationType:(UIWebViewNavigationType)navigationType
{
    NSURL *url = request.URL;
    if ([url.scheme containsString:@"mizhua"]) {
        //do something here
        return NO;
    }
    return YES;
}

核心思路就是在 WebView 攔截 Web 端發(fā)起的請(qǐng)求趾牧。雙方提前約定好協(xié)議(例如:mizhua://host?dyaction=home&tab=0,約定了 Scheme肯污、URL 和入?yún)ⅲ┣痰ィ琲OS 端在對(duì)應(yīng)的 WebView delegate 方法過(guò)濾對(duì)應(yīng)協(xié)議的請(qǐng)求吨枉,拿到url參數(shù)后做對(duì)應(yīng)的處理。

缺陷

  • 發(fā)起 url request 有命中 url 長(zhǎng)度限制的隱患(如 browser 端的IE限定url長(zhǎng)度為2083字節(jié)哄芜,opera 是4050貌亭,或者服務(wù)端的限制),特別是帶參多的情況认臊;
  • WebView對(duì)調(diào)用 url request 有頻率限制圃庭,快速調(diào)用多次 url request 有令前一部分丟失攔截的風(fēng)險(xiǎn);
  • 比起用 JavaScriptCore 注入 api 并直接調(diào)用的方式失晴,url request 創(chuàng)建請(qǐng)求有一定的耗時(shí)剧腻;
  • 比起用 JavaScriptCore 注入 api 的方式,url request 無(wú)法直接 callback 處理結(jié)果涂屁。

優(yōu)化

  • 針對(duì) url 長(zhǎng)度限制的問(wèn)題书在,可以轉(zhuǎn) push 為 pull ,讓 iOS 端主動(dòng)拉取要 web 端所要調(diào)用方法的信息拆又,步驟如下:
    ① Web 端把要調(diào)用的方法和參數(shù)先轉(zhuǎn)換為 json 字符串(例如:{"funcName":"login","prama":{"username":"xiaoming"}})保存起來(lái)蕊温;
    ② 并預(yù)寫(xiě)好一個(gè)用來(lái)獲取該 json 字符串的方法(例如: function fetchMessage());
    ③ 然后發(fā)起一個(gè)簡(jiǎn)單的 url request (例如: mizhua://fetch_message )遏乔;
    ④ WebView 在攔截到 mizhua://fetch_message 請(qǐng)求后义矛,主動(dòng)調(diào)用 Web 端的 fetchMessage() 方法,拉取到 json 字符串并反序列化盟萨,得到要調(diào)用的方法的信息凉翻。
    相應(yīng)的偽代碼如下:
// ---------------------------- Web ----------------------------
var sendMessage;

callOC({ funcName:'login', prama:{'username': 'xiaoming'} })

function callOC(message) {
    sendMessage = message;
    messagingIframe.src = 'mizhua://fetch_message';
//  messagingIframe.src = 'mizhua://fetch_message?funcName=login&username=xiaoming...';
}

function fetchMessage() {
    var messageString = JSON.stringify(sendMessage);
    sendMessage = null;
    return messageString;
}
// ---------------------------- iOS ----------------------------
- (BOOL)webView:(UIWebView *)webView
shouldStartLoadWithRequest:(NSURLRequest *)request
 navigationType:(UIWebViewNavigationType)navigationType
{
    NSURL *url = request.URL;
    if ([url.scheme containsString:@"mizhua"] && [url.host containsString:@"fetch_message"]) {
        NSString *messageString = [self stringByEvaluatingJavaScriptFromString:@"fetchMessage();"];
        Message *obj = [self deserializationMessage:messageString];
        !obj.handleBlock?:obj.handleBlock();
        return NO;
    }
    return YES;
}
  • 針對(duì)丟失攔截的問(wèn)題,我們?cè)谇懊娣桨傅幕A(chǔ)上捻激,把要調(diào)用的函數(shù)信息保存到一個(gè)消息隊(duì)列 sendMessageQueue 中制轰,Native 調(diào)用 fetchMessage() 的時(shí)候,Web 端把 sendMessageQueue 里的所有方法取出來(lái)返回給客戶端胞谭,并清空隊(duì)列垃杖。
    這樣做的好處是當(dāng)發(fā)生丟失攔截時(shí),消息還會(huì)緩存在消息隊(duì)列中丈屹,等到下一次 web 端發(fā)起 mizhua://fetch_message 要調(diào)用 iOS 端的時(shí)候调俘,未執(zhí)行的 message 就會(huì)跟著其他消息一起被 native 獲取到。
  • 針對(duì) url request 無(wú)法直接 callback 的問(wèn)題旺垒,解決方案也是讓 iOS 端主動(dòng)調(diào)用 web 端預(yù)先注冊(cè)好的監(jiān)聽(tīng)回調(diào)函數(shù)彩库,不過(guò)需要讓這個(gè)回調(diào)函數(shù)和調(diào)用函數(shù)建立起聯(lián)系。
    我們可以在 web 端維護(hù)一個(gè)回調(diào)函數(shù)字典 responseCallbacks 先蒋,用來(lái)存儲(chǔ)所有注冊(cè)的回調(diào)函數(shù)骇钦,并以 web 端發(fā)起請(qǐng)求的時(shí)間戳為 key (callbackId),并將它寫(xiě)入到 message 中({"funcName":"login","prama":{"username":"xiaoming"}, "callbackId":"1555064744266"})竞漾,iOS 端檢測(cè)到 message 中帶有 callbackId 時(shí)眯搭,再將 callbackId 和返回值拼接起來(lái)窥翩,調(diào)用 web 端預(yù)寫(xiě)的處理函數(shù) handleCallback() ,由 web 端進(jìn)行回調(diào)分發(fā)鳞仙。

結(jié)合以上所有的優(yōu)化手段寇蚊,我們可以得到一個(gè)針對(duì) url 重定向的 JSBridge 整體技術(shù)方案:

// ---------------------------- Web ----------------------------
// call native 方法隊(duì)列
var sendMessageQueue = [];
// 回調(diào)函數(shù)字典,key 為時(shí)間戳生成的 callbackId
var responseCallbacks = {};

// 調(diào)用原生方法
// message 為消息內(nèi)容繁扎,一般包括funcName(原生方法名)幔荒、prama(入?yún)?糊闽、callbackId(回調(diào)標(biāo)記)等字段
// responseCallback 為回調(diào)函數(shù)梳玫,
function callOC(message, responseCallback) {
// step 1:設(shè)置回調(diào)函數(shù)的情況下,根據(jù)時(shí)間戳生成 callbackId 右犹,并將它們緩存到內(nèi)存
    if (responseCallback) {
        var callbackId = new Date().getTime();
        responseCallbacks[callbackId] = responseCallback;
        message['callbackId'] = callbackId;
    }
// step 2:消息入隊(duì)
    sendMessageQueue.push(message);
// step 3:發(fā)起讓 native 拉取消息列表的 url 請(qǐng)求
    messagingIframe.src = 'mizhua://fetch_message';
}

callOC({ funcName:'login', prama:{'username': 'xiaoming'} }, function(response) {
    log(response)
})

// 序列化消息隊(duì)列并返回給 native
function fetchMessageQueue() {
// step 5:序列化消息隊(duì)列 `sendMessageQueue`提澎,并清空隊(duì)列
    var messageQueueString = JSON.stringify(sendMessageQueue);
    sendMessageQueue = [];
// step 6:返回消息隊(duì)列的json string
    return messageQueueString;
}

// 監(jiān)聽(tīng) native 的回調(diào)
function handleCallback(messageJSON) {
    var message = JSON.parse(messageJSON);
    var responseCallback;
// step 9:處理回調(diào)并將對(duì)應(yīng) callbackId 的回調(diào)函數(shù)移出緩存
    if (message.responseId) {
        responseCallback = responseCallbacks[message.responseId];
        if (!responseCallback) {
            return;
        }
        responseCallback(message.responseData);
        delete responseCallbacks[message.responseId];
    }
}
// ---------------------------- iOS ----------------------------
- (BOOL)webView:(UIWebView *)webView
shouldStartLoadWithRequest:(NSURLRequest *)request
 navigationType:(UIWebViewNavigationType)navigationType
{
    NSURL *url = request.URL;
    if ([url.scheme containsString:@"mizhua"] && [url.host containsString:@"fetch_message"]) {
// step 4:native 識(shí)別到 fetch_message 請(qǐng)求后,從 web 端拉取消息列表
        NSString *messageQueueString = [self stringByEvaluatingJavaScriptFromString:@"fetchMessageQueue();"];
        NSArray <Message *>*messageList = [self deserializationMessageQueue:messageQueueString];
// step 7:反序列化出所有的 message 并依次執(zhí)行
        [messageList enumerateObjectsUsingBlock:^(Message * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            !obj.handleBlock?:obj.handleBlock();
// step 8:message 帶 callbackId 的情況下念链,需要調(diào)用回調(diào)方法
            if (obj.callbackId) {
                [self stringByEvaluatingJavaScriptFromString:@"handleCallback('%@')",@{@"callbackId":obj.callbackId,@"responseData":responseData}];
                
            }
        }];
        return NO;
    }
    return YES;
}


https://upload-images.jianshu.io/upload_images/1835011-6c02d5c848b2629b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240

2 . Ajax & NSURLProtocol

AJAX = Asynchronous JavaScript and XML
AJAX 可以在不重定向當(dāng)前頁(yè)面的情況下盼忌,與服務(wù)器交換數(shù)據(jù)并更新部分網(wǎng)頁(yè)內(nèi)容。

官方文檔對(duì)于 NSURLProtocol 的描述如下:

An abstract class that handles the loading of protocol-specific URL data.

它是一個(gè)描述 URL 加載過(guò)程的抽象類(lèi)掂墓,你可以通過(guò)子類(lèi)化它來(lái)重新定義新的或已經(jīng)存在的URL加載行為谦纱。
我們可以利用這個(gè)方案來(lái)攔截到 APP 的 URL請(qǐng)求,面向切面編程地應(yīng)用到網(wǎng)絡(luò)緩存君编、網(wǎng)絡(luò)請(qǐng)求監(jiān)控跨嘉、防止DNS劫持、重定向網(wǎng)絡(luò)請(qǐng)求等場(chǎng)景吃嘿。不過(guò)這里我們只討論攔截 URL 來(lái)實(shí)現(xiàn) JSBridge 的部分祠乃。
前端使用XMLHttpRequest發(fā)起請(qǐng)求,原生注冊(cè)自定義NSURLProtocol進(jìn)行攔截:

// 1.新建類(lèi)繼承自`NSURLProtocol`兑燥,并注冊(cè)
[NSURLProtocol registerClass:[DYURLProtocol class]];

// 2.前端調(diào)用原生
function callNative(action, data) {
    var xhr = new window.XMLHttpRequest(),
    url = 'mizhua://fetch_message';
    xhr.open('POST', url, false);
    xhr.send(JSON.stringify({
        action: action,
        data: data
    }));
    return xhr.responseText;
}

// 3.在`startLoading`代理方法攔截請(qǐng)求
@implementation DYURLProtocol
- (void)startLoading {
    NSURL *url = [[self request] URL];
    if (![url.host isEqualToString:@"__jsbridge__"]) return;
    // 4.處理JS調(diào)用Native
    NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:self.request.HTTPBody options:NSJSONReadingAllowFragments error:nil];
    NSString *action = dic[@"action"];
    NSString *data = dic[@"data"];
    // 5. 處理完成亮瓷,將結(jié)果返回給js
    NSHTTPURLResponse* response = [[NSHTTPURLResponse alloc] initWithURL:[[self request] URL] statusCode:statusCode HTTPVersion:@"HTTP/1.1" headerFields:@{@"Content-Type" : mimeType}];
    [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
    if (data != nil) {
        [[self client] URLProtocol:self didLoadData:data];
    }
    [[self client] URLProtocolDidFinishLoading:self];
}

缺陷

WKWebView 維護(hù)著自己的 NSURLProtocol ,并不在以上方案的 hook 范圍中降瞳。因此嘱支,在 WKWebView 上無(wú)法直接使用注冊(cè) NSURLProtocol 的方式攔截請(qǐng)求。

優(yōu)化

蘋(píng)果開(kāi)源的 WebKit2 源碼暴露了以下的私有API:

+ [WKBrowsingContextController registerSchemeForCustomProtocol:]

通過(guò)注冊(cè) http(s) scheme 可以攔截到對(duì)應(yīng)的http或https請(qǐng)求

Class cls = NSClassFromString(@"WKBrowsingContextController"); 
SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:"); 
if ([(id)cls respondsToSelector:sel]) { 
           // 注冊(cè)http(s) scheme, 把 http和https請(qǐng)求交給 NSURLProtocol處理 
           [(id)cls performSelector:sel withObject:@"http"]; 
           [(id)cls performSelector:sel withObject:@"https"]; 
}

After iOS 7

iOS 7 以后挣饥,蘋(píng)果對(duì) WebKit 中的 JavaScriptCore 框架進(jìn)行 Objective-C 的封裝并提供給開(kāi)發(fā)者斗塘。
簡(jiǎn)單說(shuō)一下 JavaScriptCore 對(duì) Objective-C 的封裝相關(guān)的幾個(gè)概念


JSValue

JSValue 是一個(gè)指向 JS 變量(var)的引用指針。使用 JSValue亮靴,可以讓數(shù)據(jù)類(lèi)型在 OC 和 JS 之間相互轉(zhuǎn)換馍盟。

   Objective-C type  |   JavaScript type
 --------------------+---------------------
         nil         |     undefined
        NSNull       |        null
       NSString      |       string
       NSNumber      |   number, boolean
     NSDictionary    |   Object object
       NSArray       |    Array object
        NSDate       |     Date object
       NSBlock       |   Function object 
          id         |   Wrapper object 
        Class        | Constructor object

JSContext

“Context” 一般理解為上下文。 JSContext 是 JS 語(yǔ)言的執(zhí)行環(huán)境茧吊。我們可以通過(guò) KVC 的方式獲取當(dāng)前 WebView 的 JSContext:

JSContext *context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

JSContext 中有一個(gè) JSValue 類(lèi)型的屬性贞岭,名字是 GlobalObject八毯。它是當(dāng)前所執(zhí)行的 JSContext 的全局對(duì)象,所有的 JS 變量(var)與函數(shù)(function)都在全局對(duì)象里瞄桨。例如在 WebKit 中话速, GlobalObject 就相當(dāng)于 web 端的 Window 對(duì)象。我們獲取到瀏覽器的 JSContext 芯侥,對(duì)它注入代碼泊交,其實(shí)就是在操作 Window。
一個(gè) JSContext 可以擁有多個(gè) JSValue 柱查,同時(shí) JSValue 和它對(duì)應(yīng)的 var 以及它其所屬的 JSContext 對(duì)象都是強(qiáng)引用的關(guān)系:


GC 機(jī)制

JS 不需要我們?nèi)ナ謩?dòng)管理內(nèi)存廓俭。JS 的內(nèi)存管理使用的是 GC 機(jī)制(Tracing Garbage Collection)。不同于 OC 的引用計(jì)數(shù)唉工,Tracing Garbage Collection 是由 GCRoot(Context)開(kāi)始維護(hù)的一條引用鏈研乒,一旦引用鏈無(wú)法觸達(dá)某對(duì)象節(jié)點(diǎn),這個(gè)對(duì)象就會(huì)被回收掉淋硝。如下圖所示:


單線程

JS 引擎是單線程雹熬,以消息隊(duì)列(TaskQueue)事件循環(huán)(EventLoop)機(jī)制進(jìn)行 task 的分發(fā),原理上可聯(lián)想到 OperationQueue 和 RunLoop谣膳。

iOS call Web

// NOTE: UIWebView 中竿报,我們可以通過(guò)KVC的方式獲取當(dāng)前 WebView 的 JSContext
JSContext *context = [uiWebView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
JSValue *value = [context evaluateScript:@"document.title"];
NSString *title = value.toString;

[context evaluateScript:@"fetchMessages();"];
//or
JSValue *fetchMessagesFunction = context[@"fetchMessages"];
[fetchMessagesFunction callWithArguments:nil];
// NOTE: 異步調(diào)用
[context[@"setTimeout"] callWithArguments:@[fetchMessagesFunction, @0]];

// NOTE: WKWebView 中,直接提供了evaluateJavaScript函數(shù)
[wkWebView evaluateJavaScript:@"document.title" completionHandler:^(NSString* title, NSError *error) {
}];

Web call iOS

UIWebView

向 JSContext 中注入 Block:

JSContext *context = [uiWebView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
context[@"fetchMessages"] = ^(NSArray<NSArray *> *calls) {
    // Native 邏輯
};
//Web 端直接調(diào)用 fetchMessages()

JavaScriptCore 會(huì)在 Window 中生成對(duì)應(yīng)的 fetchMessages()继谚。

踩坑

  • 不要在 Block 中直接使用相應(yīng)的 JSValue 或 JSContext 烈菌。
    前面說(shuō)到 JSValue 會(huì)強(qiáng)引用它所屬的 JSContext ,而 Block 會(huì)強(qiáng)引用它使用到的外部變量犬庇。
    針對(duì) JSValue 僧界,可以把 JSValue 當(dāng)做參數(shù)傳到 Block 中,來(lái)規(guī)避外部引用臭挽。
    針對(duì) JSContext捂襟,可以在 Block 中使用 [JSContext currentContext] 方法來(lái)獲取當(dāng)前的 JSContext 。
  • 需要在 Block 中 call Web (callWithArguments:欢峰、evaluateScript:) 的情況下葬荷,不要切換線程。
    Block 中的運(yùn)行線程就是 JS 引擎所使用的單線程纽帖,切換它有幾率會(huì)引發(fā)其他問(wèn)題宠漩。

缺陷

  • UIWebViewDelegate提供的有限的代理方法中,唯一能有效獲取頁(yè)面 javaScriptContext 加載完成時(shí)機(jī)的方法是webViewDidFinishLoad:懊直。意味著在整個(gè)頁(yè)面加載完成前扒吁,前端調(diào)用原生的方法不會(huì)生效。

優(yōu)化

在WebKit中室囊,蘋(píng)果提供了 WebFrameLoadDelegatedidCreateJavaScriptContext: 代理方法來(lái)定位javaScriptContext加載完成的時(shí)機(jī)雕崩,但只曝露給 OS 系統(tǒng)魁索。我們可以通過(guò)給NSObject加分類(lèi)實(shí)現(xiàn)該代理方法,不過(guò)這個(gè)方案也觸及了私有api

WKWebView (iOS 8)

@interface WKWebVIewVC ()<WKScriptMessageHandler>

@implementation WKWebVIewVC

- (void)viewDidLoad {
    [super viewDidLoad];
    WKWebViewConfiguration* configuration = [[WKWebViewConfiguration alloc] init];
    configuration.userContentController = [[WKUserContentController alloc] init];
    WKUserContentController *userCC = configuration.userContentController;
    [userCC addScriptMessageHandler:self name:@"fetchMessages"];
    WKWebView wkWebView = [[WKWebView alloc] initWithFrame:self.view.frame configuration:configuration];
}

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    if ([message.name isEqualToString:@"fetchMessages"]) {
        NSLog(@"前端傳遞的數(shù)據(jù) %@: ",message.body);
    }
}

項(xiàng)目中的 JSBridge 方案

目前項(xiàng)目里和前端配合盼铁,使用了 Kerkee 跨平臺(tái) hybrid 框架粗蔚,僅使用到里面的 JSBridge 模塊。
框架內(nèi)判斷 iOS 8 以上使用 WKWebView饶火,并使用向 JavaScriptContext 注入 API 的方案來(lái)實(shí)現(xiàn)鹏控;iOS 8 以前使用攔截 URL 重定向的方案來(lái)實(shí)現(xiàn),并有進(jìn)行 message queue 和 callback function handler 的優(yōu)化肤寝。

參考與拓展

WKWebView 彈窗攔截
NSURLProtocol
JavaScriptCore
JavaScriptCore 踩坑(內(nèi)存管理当辐、線程安全等)
WebViewJavascriptBridge
UIWebView-TS_JavaScriptContext
WKWebView 踩坑(Cookie、NSURLProtocol 等)
JavaScriptCore 美團(tuán)
JavaScriptCore nshipster
JavaScriptCore 加載時(shí)機(jī)
JavaScriptCore 線程安全

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末醒陆,一起剝皮案震驚了整個(gè)濱河市瀑构,隨后出現(xiàn)的幾起案子裆针,更是在濱河造成了極大的恐慌刨摩,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,080評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件世吨,死亡現(xiàn)場(chǎng)離奇詭異澡刹,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)耘婚,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,422評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén)罢浇,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人沐祷,你說(shuō)我怎么就攤上這事嚷闭。” “怎么了赖临?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,630評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵胞锰,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我兢榨,道長(zhǎng)嗅榕,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,554評(píng)論 1 284
  • 正文 為了忘掉前任吵聪,我火速辦了婚禮凌那,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘吟逝。我一直安慰自己帽蝶,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,662評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布块攒。 她就那樣靜靜地躺著励稳,像睡著了一般金砍。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上麦锯,一...
    開(kāi)封第一講書(shū)人閱讀 49,856評(píng)論 1 290
  • 那天恕稠,我揣著相機(jī)與錄音,去河邊找鬼扶欣。 笑死鹅巍,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的料祠。 我是一名探鬼主播骆捧,決...
    沈念sama閱讀 39,014評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼髓绽!你這毒婦竟也來(lái)了敛苇?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,752評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤顺呕,失蹤者是張志新(化名)和其女友劉穎枫攀,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體株茶,經(jīng)...
    沈念sama閱讀 44,212評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡来涨,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,541評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了启盛。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蹦掐。...
    茶點(diǎn)故事閱讀 38,687評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖僵闯,靈堂內(nèi)的尸體忽然破棺而出卧抗,到底是詐尸還是另有隱情,我是刑警寧澤鳖粟,帶...
    沈念sama閱讀 34,347評(píng)論 4 331
  • 正文 年R本政府宣布社裆,位于F島的核電站,受9級(jí)特大地震影響牺弹,放射性物質(zhì)發(fā)生泄漏浦马。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,973評(píng)論 3 315
  • 文/蒙蒙 一张漂、第九天 我趴在偏房一處隱蔽的房頂上張望晶默。 院中可真熱鬧,春花似錦航攒、人聲如沸磺陡。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,777評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)币他。三九已至坞靶,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蝴悉,已是汗流浹背彰阴。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,006評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留拍冠,地道東北人尿这。 一個(gè)月前我還...
    沈念sama閱讀 46,406評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像庆杜,于是被迫代替她去往敵國(guó)和親射众。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,576評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容