前言
2016年4月21日,阿里巴巴在Qcon大會上宣佈跨平臺移動開發工具Weex開放內測邀請。Weex能夠完美兼顧效能與動態性,讓移動開發者通過簡捷的前端語法寫出Native級別的效能體驗,並支援iOS、安卓、YunOS及Web等多端部署。
近一年來,ReactNative 和 Weex 這些跨平臺技術對Native開發者來說,衝擊是巨大的。Native在開發App的時候存在一些弊端,比如客戶端需要頻繁更新,iOS更新時間還要受到稽核的牽制;iOS、Android和前端同時開發同一個需求,在人員成本上消耗大;Hybrid的效能和Native相比又差了一點。
ReactNative 和 Weex的出現,就是為了解決這些痛點的。
從4月21號宣佈內測以後,短短兩週就有超過5000名開發者申請。
2016年6月30日阿里巴巴正式宣佈Weex開源。號稱可以用Web方式,開發Native級效能體驗的億級應用匠心打造跨平臺移動開發工具Weex在開源首日就登上Github趨勢榜首位,截止目前為止,Weex在GitHub上的Star數已經到達了13393了。成為中國2016年在Github上最熱門的開源專案之一。
目錄
- 1.Weex概述
- 2.Weex工作原理
- 3.Weex在iOS上是如何跑起來的
- 4.關於Weex,ReactNative,JSPatch
一. Weex概述
Weex從出生那天起,彷彿就是和ReactNative是“一對”。
ReactNative宣稱“Learn once, write anywhere”,而Weex宣稱“Write Once, Run Everywhere”。Weex從出生那天起,就被給予了一統三端的厚望。ReactNative可以支援iOS、Android,而Weex可以支援iOS、Android、HTML5。一統三端就解決了前言裡面說的第二個痛點,同時開發浪費人員成本的問題。
Native移動開發者只需要在本地匯入Weex的SDK,就可以通過HTML/CSS/JavaScript網頁的這套程式語言來開發Native級別的Weex介面。這意味著可以直接用現有Web開發的編輯器和IDE的程式碼補全、提示、檢查等功能。從而也給前端人員開發Native端,較低的開發成本和學習成本。
Weex是一種輕量級、可擴充套件、高效能框架。整合也很方便,可以直接在HTML5頁面嵌入,也可嵌在原生UI中。由於和ReactNative一樣,都會呼叫Native端的原生控制元件,所以在效能上比Hybrid高出一個層次。這就解決了前言裡面所說的第三個痛點,效能問題。
Weex非常輕量,體積小巧,語法簡單,方便接入和上手。ReactNative官方只允許將ReactNative基礎js庫和業務JS一起打成一個JS bundle,沒有提供分包的功能,所以如果想節約流量就必須製作分包打包工具。而Weex預設打的JS bundle只包含業務JS程式碼,體積小很多,基礎JS庫包含在Weex SDK中,這一點Weex與Facebook的React Native和微軟的Cordova相比,Weex更加輕量,體積小巧。把Weex生成的JS bundle輕鬆部署到伺服器端,然後Push到客戶端,或者客戶端請求新的資源即可完成釋出。如此快速的迭代就解決了前言裡面說的第一個痛點,釋出無法控制時間,
Weex中Native元件和API都可以橫向擴充套件,業務方可去中心化橫向靈活化定製元件和功能模組。並且還可以直接複用Web前端的工程化管理和監控效能等工具。
知乎上有一個關於Weex 和 ReactNative很好的對比文章weex&ReactNative對比,推薦大家閱讀。
Weex在2017年2月17日正式釋出v0.10.0,這個里程碑的版本開始完美的相容Vue.js開發Weex介面。
Weex又於2017年2月24 遷移至 Apache 基金會,阿里巴巴會基於 Apache 的基礎設施繼續迭代。並啟用了全新的 GitHub 倉庫:github.com/apache/incu…
故以下原始碼分析都基於v0.10.0這個版本。
二. Weex工作原理
上圖是官方給的一張原理圖,Weex是如何把JS打包成JS Bundle的原理本篇文章暫時不涉及。本篇文章會詳細分析Weex是如何在Native端工作的。筆者把Native端的原理再次細分,如下圖:
Weex可以通過自己設計的DSL,書寫.we檔案或者.vue檔案來開發介面,整個頁面書寫分成了3段,template、style、script,借鑑了成熟的MVVM的思想。
Weex在效能方面,為了儘可能的提升客戶端的效能,DSL的Transformer全部都放在了伺服器端實現,Weex會在伺服器端將XML + CSS + JavaScript 程式碼全部都轉換成JS Bundle。伺服器將JS Bundle部署到Server上和CDN上。
Weex和React Native不同的是,Weex把JS Framework內建在SDK裡面,用來解析從伺服器上下載的JS Bundle,這樣也減少了每個JS Bundle的體積,不再有React Native需要分包的問題。客戶端請求完JS Bundle以後,傳給JS Framework,JS Framework解析完成以後會輸出Json格式的Virtual DOM,客戶端Native只需要專心負責 Virtual DOM 的解析和佈局、UI 渲染。然而這一套解析,佈局,渲染的邏輯SDK基本實現了。
最後Weex支援三端一致,伺服器上的一份JS Bundle,通過解析,實現iOS/Android/HTML5 三端的一致性。
三. Weex在iOS上是如何跑起來的
經過上一章的分析,我們知道了Weex的整體流程,由於筆者前端知識匱乏,所以從.we或者.vue檔案到JS bundle前端這部分的原始碼分析本文暫時不涉及,等筆者熟悉前端以後,這塊還會再補上來。
分析之前先說明一點,Weex的所有原始碼其實已經開源了,至於SDK的Demo裡面還依賴了一個ATSDK.framework,這個是沒有開源的。ATSDK.framework這個其實是Weex效能監控的外掛。
就是上圖中的那個灰色的框框的外掛。這個外掛有些大廠有自己的APM,阿里暫時沒有開源這塊,但是對Weex所有功能是不影響的。
那麼接下來就詳細分析一下在iOS Native端,Weex是如何跑起來的。直接上原始碼分析。
(一). Weex SDK初始化
這是Native端想把Weex跑起來的第一步。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
self.window.backgroundColor = [UIColor whiteColor];
// 在這裡進行初始化SDK
[self initWeexSDK];
self.window.rootViewController = [[WXRootViewController alloc] initWithRootViewController:[self demoController]];
[self.window makeKeyAndVisible];
return YES;
}複製程式碼
在application: didFinishLaunchingWithOptions:函式裡面初始化SDK。這裡會初始化很多東西。可能有人會問了,初始化寫在這裡,還初始化這麼多東西,不會卡App的啟動時間麼?帶著這個問題繼續往下看吧。
#pragma mark weex
- (void)initWeexSDK
{
[WXAppConfiguration setAppGroup:@"AliApp"];
[WXAppConfiguration setAppName:@"WeexDemo"];
[WXAppConfiguration setExternalUserAgent:@"ExternalUA"];
[WXSDKEngine initSDKEnvironment];
[WXSDKEngine registerHandler:[WXImgLoaderDefaultImpl new] withProtocol:@protocol(WXImgLoaderProtocol)];
[WXSDKEngine registerHandler:[WXEventModule new] withProtocol:@protocol(WXEventModuleProtocol)];
[WXSDKEngine registerComponent:@"select" withClass:NSClassFromString(@"WXSelectComponent")];
[WXSDKEngine registerModule:@"event" withClass:[WXEventModule class]];
[WXSDKEngine registerModule:@"syncTest" withClass:[WXSyncTestModule class]];
#if !(TARGET_IPHONE_SIMULATOR)
[self checkUpdate];
#endif
#ifdef DEBUG
[self atAddPlugin];
[WXDebugTool setDebug:YES];
[WXLog setLogLevel:WXLogLevelLog];
#ifndef UITEST
[[ATManager shareInstance] show];
#endif
#else
[WXDebugTool setDebug:NO];
[WXLog setLogLevel:WXLogLevelError];
#endif
}複製程式碼
上述就是要在application: didFinishLaunchingWithOptions:裡面初始化的全部內容。我們一行一行的來解讀。
WXAppConfiguration是一個用來記錄App配置資訊的單例物件。
@interface WXAppConfiguration : NSObject
@property (nonatomic, strong) NSString * appGroup;
@property (nonatomic, strong) NSString * appName;
@property (nonatomic, strong) NSString * appVersion;
@property (nonatomic, strong) NSString * externalUA;
@property (nonatomic, strong) NSString * JSFrameworkVersion;
@property (nonatomic, strong) NSArray * customizeProtocolClasses;
/**
* AppGroup的名字或者公司組織名,預設值為nil
*/
+ (NSString *)appGroup;
+ (void)setAppGroup:(NSString *) appGroup;
/**
* app的名字, 預設值是main bundle裡面的CFBundleDisplayName
*/
+ (NSString *)appName;
+ (void)setAppName:(NSString *)appName;
/**
* app版本資訊, 預設值是main bundle裡面的CFBundleShortVersionString
*/
+ (NSString *)appVersion;
+ (void)setAppVersion:(NSString *)appVersion;
/**
* app外面使用者代理的名字, 所有Weex的請求頭都會設定使用者代理user agent欄位,預設值為nil
*/
+ (NSString *)externalUserAgent;
+ (void)setExternalUserAgent:(NSString *)userAgent;
/**
* JSFrameworkVersion的版本
*/
+ (NSString *)JSFrameworkVersion;
+ (void)setJSFrameworkVersion:(NSString *)JSFrameworkVersion;
/*
* 自定義customizeProtocolClasses
*/
+ (NSArray*)customizeProtocolClasses;
+ (void)setCustomizeProtocolClasses:(NSArray*)customizeProtocolClasses;
@end複製程式碼
注意WXAppConfiguration的所有方法都是加號的類方法,內部實現是用WXAppConfiguration的單例實現的,這裡用類方法是為了我們方便呼叫。
接下來是初始化SDK的實質程式碼了。
[WXSDKEngine initSDKEnvironment];複製程式碼
關於初始化的具體實現,見下面,裡面標註了註釋:
+ (void)initSDKEnvironment
{
// 打點記錄狀態
WX_MONITOR_PERF_START(WXPTInitalize)
WX_MONITOR_PERF_START(WXPTInitalizeSync)
// 載入本地的main.js
NSString *filePath = [[NSBundle bundleForClass:self] pathForResource:@"main" ofType:@"js"];
NSString *script = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil];
// 初始化SDK環境
[WXSDKEngine initSDKEnvironment:script];
// 打點記錄狀態
WX_MONITOR_PERF_END(WXPTInitalizeSync)
// 模擬器版本特殊程式碼
#if TARGET_OS_SIMULATOR
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[WXSimulatorShortcutManager registerSimulatorShortcutWithKey:@"i" modifierFlags:UIKeyModifierCommand | UIKeyModifierAlternate action:^{
NSURL *URL = [NSURL URLWithString:@"http://localhost:8687/launchDebugger"];
NSURLRequest *request = [NSURLRequest requestWithURL:URL];
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request
completionHandler:
^(NSData *data, NSURLResponse *response, NSError *error) {
// ...
}];
[task resume];
WXLogInfo(@"Launching browser...");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self connectDebugServer:@"ws://localhost:8687/debugger/0/renderer"];
});
}];
});
#endif
}複製程式碼
這裡整個SDKEnvironment的初始化分成了四個步驟,WXMonitor監視器記錄狀態,載入本地的main.js,WXSDKEngine的初始化,模擬器WXSimulatorShortcutManager連線本地server。接下來一步步的分析。
1. WXMonitor監視器記錄狀態
WXMonitor是一個普通的物件,它裡面只儲存了一個執行緒安全的字典WXThreadSafeMutableDictionary。
@interface WXThreadSafeMutableDictionary<KeyType, ObjectType> : NSMutableDictionary
@property (nonatomic, strong) dispatch_queue_t queue;
@property (nonatomic, strong) NSMutableDictionary* dict;
@end複製程式碼
在這個字典初始化的時候會初始化一個queue。
- (instancetype)init
{
self = [self initCommon];
if (self) {
_dict = [NSMutableDictionary dictionary];
}
return self;
}
- (instancetype)initCommon
{
self = [super init];
if (self) {
NSString* uuid = [NSString stringWithFormat:@"com.taobao.weex.dictionary_%p", self];
_queue = dispatch_queue_create([uuid UTF8String], DISPATCH_QUEUE_CONCURRENT);
}
return self;
}複製程式碼
每次生成一次WXThreadSafeMutableDictionary,就會有一個與之記憶體地址向對應的Concurrent的queue相對應。
這個queue就保證了執行緒安全。
- (NSUInteger)count
{
__block NSUInteger count;
dispatch_sync(_queue, ^{
count = _dict.count;
});
return count;
}
- (id)objectForKey:(id)aKey
{
__block id obj;
dispatch_sync(_queue, ^{
obj = _dict[aKey];
});
return obj;
}
- (NSEnumerator *)keyEnumerator
{
__block NSEnumerator *enu;
dispatch_sync(_queue, ^{
enu = [_dict keyEnumerator];
});
return enu;
}
- (id)copy{
__block id copyInstance;
dispatch_sync(_queue, ^{
copyInstance = [_dict copy];
});
return copyInstance;
}複製程式碼
count、objectForKey:、keyEnumerator、copy這四個操作都是同步操作,用dispatch_sync保護執行緒安全。
- (void)setObject:(id)anObject forKey:(id<NSCopying>)aKey
{
aKey = [aKey copyWithZone:NULL];
dispatch_barrier_async(_queue, ^{
_dict[aKey] = anObject;
});
}
- (void)removeObjectForKey:(id)aKey
{
dispatch_barrier_async(_queue, ^{
[_dict removeObjectForKey:aKey];
});
}
- (void)removeAllObjects{
dispatch_barrier_async(_queue, ^{
[_dict removeAllObjects];
});
}複製程式碼
setObject:forKey:、removeObjectForKey:、removeAllObjects這三個操作加上了dispatch_barrier_async。
WXMonitor在整個Weex裡面擔任的職責是記錄下各個操作的tag值和記錄成功和失敗的原因。WXMonitor封裝了各種巨集來方便方法的呼叫。
#define WX_MONITOR_PERF_START(tag) [WXMonitor performancePoint:tag willStartWithInstance:nil];
#define WX_MONITOR_PERF_END(tag) [WXMonitor performancePoint:tag didEndWithInstance:nil];
#define WX_MONITOR_INSTANCE_PERF_START(tag, instance) [WXMonitor performancePoint:tag willStartWithInstance:instance];
#define WX_MONITOR_INSTANCE_PERF_END(tag, instance) [WXMonitor performancePoint:tag didEndWithInstance:instance];
#define WX_MONITOR_PERF_SET(tag, value, instance) [WXMonitor performancePoint:tag didSetValue:value withInstance:instance];
#define WX_MONITOR_INSTANCE_PERF_IS_RECORDED(tag, instance) [WXMonitor performancePoint:tag isRecordedWithInstance:instance]
// 上面這些巨集都會分別對應下面這些具體的方法實現。
+ (void)performancePoint:(WXPerformanceTag)tag willStartWithInstance:(WXSDKInstance *)instance;
+ (void)performancePoint:(WXPerformanceTag)tag didEndWithInstance:(WXSDKInstance *)instance;
+ (void)performancePoint:(WXPerformanceTag)tag didSetValue:(double)value withInstance:(WXSDKInstance *)instance;
+ (BOOL)performancePoint:(WXPerformanceTag)tag isRecordedWithInstance:(WXSDKInstance *)instance;複製程式碼
整個操作被定義成2類,一個是全域性的操作,一個是具體的操作。
typedef enum : NSUInteger {
// global
WXPTInitalize = 0,
WXPTInitalizeSync,
WXPTFrameworkExecute,
// instance
WXPTJSDownload,
WXPTJSCreateInstance,
WXPTFirstScreenRender,
WXPTAllRender,
WXPTBundleSize,
WXPTEnd
} WXPerformanceTag;複製程式碼
在WXSDKInstance初始化之前,所有的全域性的global操作都會放在WXMonitor的WXThreadSafeMutableDictionary中。當WXSDKInstance初始化之後,即WXPerformanceTag中instance以下的所有操作都會放在WXSDKInstance的performanceDict中,注意performanceDict並不是執行緒安全的。
舉個例子:
+ (void)performancePoint:(WXPerformanceTag)tag willStartWithInstance:(WXSDKInstance *)instance
{
NSMutableDictionary *performanceDict = [self performanceDictForInstance:instance];
NSMutableDictionary *dict = [[NSMutableDictionary alloc] initWithCapacity:2];
dict[kStartKey] = @(CACurrentMediaTime() * 1000);
performanceDict[@(tag)] = dict;
}複製程式碼
所有的操作都會按照時間被記錄下來:
WX_MONITOR_PERF_START(WXPTInitalize)
WX_MONITOR_PERF_START(WXPTInitalizeSync)複製程式碼
WXThreadSafeMutableDictionary字典裡面會存類似這些資料:
{
0 = {
start = "146297522.903652";
};
1 = {
start = "146578019.356428";
};
}複製程式碼
字典裡面會根據操作的tag作為key值。一般WX_MONITOR_PERF_START和WX_MONITOR_PERF_END是成對出現的,初始化結束以後就會呼叫WX_MONITOR_PERF_END。最終字典裡面會儲存成下面的樣子:
{
0 = {
end = "148750673.312226";
start = "148484241.723654";
};
1 = {
end = "148950673.312226";
start = "148485865.699819";
};
}複製程式碼
WXMonitor裡面還會記錄一些成功和失敗的資訊:
#define WX_MONITOR_SUCCESS_ON_PAGE(tag, pageName) [WXMonitor monitoringPointDidSuccess:tag onPage:pageName];
#define WX_MONITOR_FAIL_ON_PAGE(tag, errorCode, errorMessage, pageName) \
NSError *error = [NSError errorWithDomain:WX_ERROR_DOMAIN \
code:errorCode \
userInfo:@{NSLocalizedDescriptionKey:(errorMessage?:@"No message")}]; \
[WXMonitor monitoringPoint:tag didFailWithError:error onPage:pageName];
#define WX_MONITOR_SUCCESS(tag) WX_MONITOR_SUCCESS_ON_PAGE(tag, nil)
#define WX_MONITOR_FAIL(tag, errorCode, errorMessage) WX_MONITOR_FAIL_ON_PAGE(tag, errorCode, errorMessage, nil)
// 上面這些巨集都會分別對應下面這些具體的方法實現。
+ (void)monitoringPointDidSuccess:(WXMonitorTag)tag onPage:(NSString *)pageName;
+ (void)monitoringPoint:(WXMonitorTag)tag didFailWithError:(NSError *)error onPage:(NSString *)pageName;複製程式碼
這些函式暫時這裡沒有用到,暫時先不解析了。
2. 載入本地的main.js
SDK裡面會帶一個main.js,直接開啟這個檔案會看到一堆經過webpack壓縮之後的檔案。這個檔案的原始檔在github.com/apache/incu…目錄下。對應的入口檔案是 html5/render/native/index.js
import { subversion } from '../../../package.json'
import runtime from '../../runtime'
import frameworks from '../../frameworks/index'
import services from '../../services/index'
const { init, config } = runtime
config.frameworks = frameworks
const { native, transformer } = subversion
for (const serviceName in services) {
runtime.service.register(serviceName, services[serviceName])
}
runtime.freezePrototype()
runtime.setNativeConsole()
// register framework meta info
global.frameworkVersion = native
global.transformerVersion = transformer
// init frameworks
const globalMethods = init(config)
// set global methods
for (const methodName in globalMethods) {
global[methodName] = (...args) => {
const ret = globalMethods[methodName](...args)
if (ret instanceof Error) {
console.error(ret.toString())
}
return ret
}
}複製程式碼
這一段js是會被當做入參傳遞給WXSDKManager。它也就是Native這邊的js framework。
3. WXSDKEngine的初始化
WXSDKEngine的初始化就是整個SDK初始化的關鍵。
+ (void)initSDKEnvironment:(NSString *)script
{
if (!script || script.length <= 0) {
WX_MONITOR_FAIL(WXMTJSFramework, WX_ERR_JSFRAMEWORK_LOAD, @"framework loading is failure!");
return;
}
// 註冊Components,Modules,Handlers
[self registerDefaults];
// 執行JsFramework
[[WXSDKManager bridgeMgr] executeJsFramework:script];
}複製程式碼
總共幹了兩件事情,註冊Components,Modules,Handlers 和 執行JSFramework。
先來看看是怎麼註冊的。
+ (void)registerDefaults
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self _registerDefaultComponents];
[self _registerDefaultModules];
[self _registerDefaultHandlers];
});
}複製程式碼
在WXSDKEngine初始化的時候就分別註冊了這三樣東西,Components,Modules,Handlers。
先看Components:
+ (void)_registerDefaultComponents
{
[self registerComponent:@"container" withClass:NSClassFromString(@"WXDivComponent") withProperties:nil];
[self registerComponent:@"div" withClass:NSClassFromString(@"WXComponent") withProperties:nil];
[self registerComponent:@"text" withClass:NSClassFromString(@"WXTextComponent") withProperties:nil];
[self registerComponent:@"image" withClass:NSClassFromString(@"WXImageComponent") withProperties:nil];
[self registerComponent:@"scroller" withClass:NSClassFromString(@"WXScrollerComponent") withProperties:nil];
[self registerComponent:@"list" withClass:NSClassFromString(@"WXListComponent") withProperties:nil];
[self registerComponent:@"header" withClass:NSClassFromString(@"WXHeaderComponent")];
[self registerComponent:@"cell" withClass:NSClassFromString(@"WXCellComponent")];
[self registerComponent:@"embed" withClass:NSClassFromString(@"WXEmbedComponent")];
[self registerComponent:@"a" withClass:NSClassFromString(@"WXAComponent")];
[self registerComponent:@"select" withClass:NSClassFromString(@"WXSelectComponent")];
[self registerComponent:@"switch" withClass:NSClassFromString(@"WXSwitchComponent")];
[self registerComponent:@"input" withClass:NSClassFromString(@"WXTextInputComponent")];
[self registerComponent:@"video" withClass:NSClassFromString(@"WXVideoComponent")];
[self registerComponent:@"indicator" withClass:NSClassFromString(@"WXIndicatorComponent")];
[self registerComponent:@"slider" withClass:NSClassFromString(@"WXSliderComponent")];
[self registerComponent:@"web" withClass:NSClassFromString(@"WXWebComponent")];
[self registerComponent:@"loading" withClass:NSClassFromString(@"WXLoadingComponent")];
[self registerComponent:@"loading-indicator" withClass:NSClassFromString(@"WXLoadingIndicator")];
[self registerComponent:@"refresh" withClass:NSClassFromString(@"WXRefreshComponent")];
[self registerComponent:@"textarea" withClass:NSClassFromString(@"WXTextAreaComponent")];
[self registerComponent:@"canvas" withClass:NSClassFromString(@"WXCanvasComponent")];
[self registerComponent:@"slider-neighbor" withClass:NSClassFromString(@"WXSliderNeighborComponent")];
}複製程式碼
在WXSDKEngine初始化的時候會預設註冊這23種基礎元件。這裡就舉一個最複雜的元件WXWebComponent,來看看它是如何被註冊的。
首先需要說明的一點,
+ (void)registerComponent:(NSString *)name withClass:(Class)clazz
{
[self registerComponent:name withClass:clazz withProperties: @{@"append":@"tree"}];
}複製程式碼
registerComponent:withClass:方法和registerComponent:withClass:withProperties:方法的區別在於最後一個入參是否傳@{@"append":@"tree"},如果被標記成了@"tree",那麼在syncQueue堆積了很多工的時候,會被強制執行一次layout。
所以上面23種基本元件裡面,只有前5種,container,div,text,image,scroller,list是沒有被標記成@"tree",剩下的18種都是有可能強制執行一次layout。
+ (void)registerComponent:(NSString *)name withClass:(Class)clazz withProperties:(NSDictionary *)properties
{
if (!name || !clazz) {
return;
}
WXAssert(name && clazz, @"Fail to register the component, please check if the parameters are correct !");
// 1.WXComponentFactory註冊元件的方法
[WXComponentFactory registerComponent:name withClass:clazz withPros:properties];
// 2.遍歷出所有非同步的方法
NSMutableDictionary *dict = [WXComponentFactory componentMethodMapsWithName:name];
dict[@"type"] = name;
// 3.把元件註冊到WXBridgeManager中
if (properties) {
NSMutableDictionary *props = [properties mutableCopy];
if ([dict[@"methods"] count]) {
[props addEntriesFromDictionary:dict];
}
[[WXSDKManager bridgeMgr] registerComponents:@[props]];
} else {
[[WXSDKManager bridgeMgr] registerComponents:@[dict]];
}
}複製程式碼
註冊元件全部都是通過WXComponentFactory完成註冊的。WXComponentFactory是一個單例。
@interface WXComponentFactory : NSObject
{
NSMutableDictionary *_componentConfigs;
NSLock *_configLock;
}
@property (nonatomic, strong) NSDictionary *properties;
@end複製程式碼
在WXComponentFactory中,_componentConfigs會儲存所有的元件配置,註冊的過程也是生成_componentConfigs的過程。
- (void)registerComponent:(NSString *)name withClass:(Class)clazz withPros:(NSDictionary *)pros
{
WXAssert(name && clazz, @"name or clazz must not be nil for registering component.");
WXComponentConfig *config = nil;
[_configLock lock];
config = [_componentConfigs objectForKey:name];
// 如果元件已經註冊過,會提示重複註冊,並且覆蓋原先的註冊行為
if(config){
WXLogInfo(@"Overrider component name:%@ class:%@, to name:%@ class:%@",
config.name, config.class, name, clazz);
}
config = [[WXComponentConfig alloc] initWithName:name class:NSStringFromClass(clazz) pros:pros];
[_componentConfigs setValue:config forKey:name];
// 註冊類方法
[config registerMethods];
[_configLock unlock];
}複製程式碼
在WXComponentFactory的_componentConfigs字典中會按照元件的名字作為key,WXComponentConfig作為value儲存各個元件的配置。
@interface WXComponentConfig : WXInvocationConfig
@property (nonatomic, strong) NSDictionary *properties;
@end
@interface WXInvocationConfig : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *clazz;
@property (nonatomic, strong) NSMutableDictionary *asyncMethods;
@property (nonatomic, strong) NSMutableDictionary *syncMethods;
@end複製程式碼
WXComponentConfig繼承自WXInvocationConfig,在WXInvocationConfig中儲存了元件名name,類名clazz,類裡面的同步方法字典syncMethods和非同步方法字典asyncMethods。
元件註冊這裡比較關鍵的一點是註冊類方法。
- (void)registerMethods
{
Class currentClass = NSClassFromString(_clazz);
if (!currentClass) {
WXLogWarning(@"The module class [%@] doesn't exit!", _clazz);
return;
}
while (currentClass != [NSObject class]) {
unsigned int methodCount = 0;
// 獲取類的方法列表
Method *methodList = class_copyMethodList(object_getClass(currentClass), &methodCount);
for (unsigned int i = 0; i < methodCount; i++) {
// 獲取SEL的字串名稱
NSString *selStr = [NSString stringWithCString:sel_getName(method_getName(methodList[i])) encoding:NSUTF8StringEncoding];
BOOL isSyncMethod = NO;
// 如果是SEL名字帶sync,就是同步方法
if ([selStr hasPrefix:@"wx_export_method_sync_"]) {
isSyncMethod = YES;
// 如果是SEL名字不帶sync,就是非同步方法
} else if ([selStr hasPrefix:@"wx_export_method_"]) {
isSyncMethod = NO;
} else {
// 如果名字裡面不帶wx_export_method_字首的方法,那麼都不算是暴露出來的方法,直接continue,進行下一輪的篩選
continue;
}
NSString *name = nil, *method = nil;
SEL selector = NSSelectorFromString(selStr);
if ([currentClass respondsToSelector:selector]) {
method = ((NSString* (*)(id, SEL))[currentClass methodForSelector:selector])(currentClass, selector);
}
if (method.length <= 0) {
WXLogWarning(@"The module class [%@] doesn't has any method!", _clazz);
continue;
}
// 去掉方法名裡面帶的:號
NSRange range = [method rangeOfString:@":"];
if (range.location != NSNotFound) {
name = [method substringToIndex:range.location];
} else {
name = method;
}
// 最終字典裡面會按照非同步方法和同步方法儲存到最終的方法字典裡
NSMutableDictionary *methods = isSyncMethod ? _syncMethods : _asyncMethods;
[methods setObject:method forKey:name];
}
free(methodList);
currentClass = class_getSuperclass(currentClass);
}
}複製程式碼
這裡的做法也比較常規,找到對應的類方法,判斷名字裡面是否帶有“sync”來判斷方法是同步還是非同步方法。這裡重點需要解析的是元件的方法是如何轉換成類方法的暴露出去的。
Weex是通過裡面通過WX_EXPORT_METHOD巨集做到對外暴露類方法的。
#define WX_EXPORT_METHOD(method) WX_EXPORT_METHOD_INTERNAL(method,wx_export_method_)
#define WX_EXPORT_METHOD_INTERNAL(method, token) \
+ (NSString *)WX_CONCAT_WRAPPER(token, __LINE__) { \
return NSStringFromSelector(method); \
}
#define WX_CONCAT_WRAPPER(a, b) WX_CONCAT(a, b)
#define WX_CONCAT(a, b) a ## b複製程式碼
WX_EXPORT_METHOD巨集會完全展開成下面這個樣子:
#define WX_EXPORT_METHOD(method)
+ (NSString *)wx_export_method_ __LINE__ { \
return NSStringFromSelector(method); \
}複製程式碼
舉個例子,在WXWebComponent的第52行裡面寫了下面這一行程式碼:
WX_EXPORT_METHOD(@selector(goBack))複製程式碼
那麼這個巨集在預編譯的時候就會被展開成下面這個樣子:
+ (NSString *)wx_export_method_52 {
return NSStringFromSelector(@selector(goBack));
}複製程式碼
於是乎在WXWebComponent的類方法裡面就多了一個wx_export_method_52的方法。由於在同一個檔案裡面,WX_EXPORT_METHOD巨集是不允許寫在同一行的,所以轉換出來的方法名字肯定不會相同。但是不同類裡面行數就沒有規定,行數是可能相同的,從而不同類裡面可能就有相同的方法名。
比如在WXScrollerComponent裡面的第58行
WX_EXPORT_METHOD(@selector(resetLoadmore))複製程式碼
WXTextAreaComponent裡面的第58行
WX_EXPORT_METHOD(@selector(focus))複製程式碼
這兩個是不同的元件,但是巨集展開之後的方法名是一樣的,這兩個不同的類的類方法,是有重名的,但是完全不會有什麼影響,因為獲取類方法的時候是通過class_copyMethodList,保證這個list裡面都是唯一的名字即可。
還有一點需要說明的是,雖然用class_copyMethodList會獲取所有的類方法(+號方法),但是可能有人疑問了,那不通過WX_EXPORT_METHOD巨集對外暴露的普通的+號方法,不是也會被篩選進來麼?
回答:是的,會被class_copyMethodList獲取到,但是這裡有一個判斷條件,會避開這些不通過WX_EXPORT_METHOD巨集對外暴露的普通的+號類方法。
如果不通過WX_EXPORT_METHOD巨集來申明對外暴露的普通的+號類方法,那麼名字裡面就不會帶wx_export_method_的字首的方法,那麼都不算是暴露出來的方法,上面篩選的程式碼裡面會直接continue,進行下一輪的篩選,所以不必擔心那些普通的+號類方法會進來干擾。
回到WXWebComponent註冊,通過上述方法獲取完類方法之後,字典裡面就儲存的如下資訊:
methods = {
goBack = goBack;
goForward = goForward;
reload = reload;
}複製程式碼
這就完成了元件註冊的第一步,完成了註冊配置WXComponentConfig。
元件註冊的第二步,遍歷所有的非同步方法。
- (NSMutableDictionary *)_componentMethodMapsWithName:(NSString *)name
{
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
NSMutableArray *methods = [NSMutableArray array];
[_configLock lock];
[dict setValue:methods forKey:@"methods"];
WXComponentConfig *config = _componentConfigs[name];
void (^mBlock)(id, id, BOOL *) = ^(id mKey, id mObj, BOOL * mStop) {
[methods addObject:mKey];
};
[config.asyncMethods enumerateKeysAndObjectsUsingBlock:mBlock];
[_configLock unlock];
return dict;
}複製程式碼
這裡依舊是呼叫了WXComponentFactory的方法_componentMethodMapsWithName:。這裡就是遍歷出非同步方法,並放入字典中,返回非同步方法的字典。
還是以最複雜的WXWebComponent為例,這裡就會返回如下的非同步方法字典:
{
methods = (
goForward,
goBack,
reload
);
}複製程式碼
註冊元件的最後一步會在JSFrame中註冊元件。
@interface WXSDKManager ()
@property (nonatomic, strong) WXBridgeManager *bridgeMgr;
@property (nonatomic, strong) WXThreadSafeMutableDictionary *instanceDict;
@end複製程式碼
在WXSDKManager裡面會強持有一個WXBridgeManager。這個WXBridgeManager就是用來和JS互動的Bridge。
@interface WXBridgeManager : NSObject
@property (nonatomic, weak, readonly) WXSDKInstance *topInstance;
@property (nonatomic, strong) WXBridgeContext *bridgeCtx;
@property (nonatomic, assign) BOOL stopRunning;
@property (nonatomic, strong) NSMutableArray *instanceIdStack;
@end複製程式碼
WXBridgeManager中會弱引用WXSDKInstance例項,是為了能呼叫WXSDKInstance的一些屬性和方法。WXBridgeManager裡面最重要的一個屬性就是WXBridgeContext。
@interface WXBridgeContext ()
@property (nonatomic, weak, readonly) WXSDKInstance *topInstance;
@property (nonatomic, strong) id<WXBridgeProtocol> jsBridge;
@property (nonatomic, strong) WXDebugLoggerBridge *devToolSocketBridge;
@property (nonatomic, assign) BOOL debugJS;
// 儲存native要即將呼叫js的一些方法
@property (nonatomic, strong) NSMutableDictionary *sendQueue;
// 例項的一些堆疊
@property (nonatomic, strong) WXThreadSafeMutableArray *insStack;
// 標識JSFramework是否已經載入完成
@property (nonatomic) BOOL frameworkLoadFinished;
// 在JSFramework載入完成之前,臨時儲存一些方法
@property (nonatomic, strong) NSMutableArray *methodQueue;
// 儲存js模板的service
@property (nonatomic, strong) NSMutableArray *jsServiceQueue;
@end複製程式碼
在WXBridgeContext中強持有了一個jsBridge。這個就是用來和js進行互動的Bridge。
三者的關係用圖表示出來如上圖。由於是弱引用,所以用虛的線框表示。
回到註冊的步驟中來,在WXSDKEngine中呼叫如下方法:
[[WXSDKManager bridgeMgr] registerComponents:@[dict]];複製程式碼
WXBridgeManager呼叫registerComponents方法。
- (void)registerComponents:(NSArray *)components
{
if (!components) return;
__weak typeof(self) weakSelf = self;
WXPerformBlockOnBridgeThread(^(){
[weakSelf.bridgeCtx registerComponents:components];
});
}複製程式碼
最終是WXBridgeManager裡面的WXBridgeContext 呼叫registerComponents,進行元件的註冊。但是註冊元件的這一步是在一個特殊的執行緒中執行的。
void WXPerformBlockOnBridgeThread(void (^block)())
{
[WXBridgeManager _performBlockOnBridgeThread:block];
}
+ (void)_performBlockOnBridgeThread:(void (^)())block
{
if ([NSThread currentThread] == [self jsThread]) {
block();
} else {
[self performSelector:@selector(_performBlockOnBridgeThread:)
onThread:[self jsThread]
withObject:[block copy]
waitUntilDone:NO];
}
}複製程式碼
這裡就可以看到,block閉包是在jsThread的執行緒中執行的,並非主執行緒。WXBridgeManager會新建一個名為@"com.taobao.weex.bridge"的jsThread執行緒,所有的元件註冊都在這個子執行緒中執行的。這個jsThread也是一個單例,全域性唯一。
+ (NSThread *)jsThread
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
WXBridgeThread = [[NSThread alloc] initWithTarget:[[self class]sharedManager] selector:@selector(_runLoopThread) object:nil];
[WXBridgeThread setName:WX_BRIDGE_THREAD_NAME];
if(WX_SYS_VERSION_GREATER_THAN_OR_EQUAL_TO(@"8.0")) {
[WXBridgeThread setQualityOfService:[[NSThread mainThread] qualityOfService]];
} else {
[WXBridgeThread setThreadPriority:[[NSThread mainThread] threadPriority]];
}
[WXBridgeThread start];
});
return WXBridgeThread;
}複製程式碼
這裡就是建立jsThread的程式碼,jsThread會把@selector(_runLoopThread)作為selector。
- (void)_runLoopThread
{
[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
while (!_stopRunning) {
@autoreleasepool {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
}
}複製程式碼
於是這裡就給jsThread開啟了一個runloop。這裡是用[NSMachPort port]的方式開啟的runloop,之後再也無法獲取到這個port了,而且這個runloop不是CFRunloop,所以用官方文件上的那3個方法已經不能停止這個runloop了,只能自己通過while的方式來停止。上述程式碼是一種寫法,當然StackOverFlow上面推薦的是下面的寫法,下面的寫法也是我常用的寫法。
BOOL shouldKeepRunning = YES; // global
NSRunLoop *theRL = [NSRunLoop currentRunLoop];
while (shouldKeepRunning && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);複製程式碼
- (void)registerComponents:(NSArray *)components
{
WXAssertBridgeThread();
if(!components) return;
[self callJSMethod:@"registerComponents" args:@[components]];
}複製程式碼
在WXBridgeContext中註冊元件,其實呼叫的是js的方法"registerComponents"。
這裡有一個需要注意的一點,由於是在子執行緒上註冊元件,那麼JSFramework如果沒有載入完成,native去呼叫js的方法,必定呼叫失敗。所以需要在JSFramework載入完成之前,把native呼叫JS的方法都快取起來,一旦JSFramework載入完成,把快取裡面的方法都丟給JSFramework去載入。
- (void)callJSMethod:(NSString *)method args:(NSArray *)args
{
if (self.frameworkLoadFinished) {
[self.jsBridge callJSMethod:method args:args];
} else {
[_methodQueue addObject:@{@"method":method, @"args":args}];
}
}複製程式碼
所以在WXBridgeContext中需要一個NSMutableArray,用來快取在JSFramework載入完成之前,呼叫JS的方法。這裡是儲存在_methodQueue裡面。如果JSFramework載入完成,那麼就會呼叫callJSMethod:args:方法。
- (JSValue *)callJSMethod:(NSString *)method args:(NSArray *)args
{
WXLogDebug(@"Calling JS... method:%@, args:%@", method, args);
return [[_jsContext globalObject] invokeMethod:method withArguments:args];
}複製程式碼
由於這些註冊的方法的定義是全域性函式,那麼很顯然應該在JSContext的globalObject物件上呼叫該方法。(目前流程進行到這裡還看不到定義的全域性函式,往後看就會看到)
還是用WXWebComponent來舉例,那麼這裡註冊元件的method就是@“registerComponents”,args引數如下:
(
{
append = tree;
methods = (
goForward,
goBack,
reload
);
type = web;
}
)複製程式碼
實際上程式執行到這裡,並不會去執行callJSMethod:args:,因為現在JSFramework還沒有載入完成。
註冊元件的全部流程如下:
再註冊Modules
註冊Modules的流程和上面註冊Components非常類似。
+ (void)_registerDefaultModules
{
[self registerModule:@"dom" withClass:NSClassFromString(@"WXDomModule")];
[self registerModule:@"navigator" withClass:NSClassFromString(@"WXNavigatorModule")];
[self registerModule:@"stream" withClass:NSClassFromString(@"WXStreamModule")];
[self registerModule:@"animation" withClass:NSClassFromString(@"WXAnimationModule")];
[self registerModule:@"modal" withClass:NSClassFromString(@"WXModalUIModule")];
[self registerModule:@"webview" withClass:NSClassFromString(@"WXWebViewModule")];
[self registerModule:@"instanceWrap" withClass:NSClassFromString(@"WXInstanceWrap")];
[self registerModule:@"timer" withClass:NSClassFromString(@"WXTimerModule")];
[self registerModule:@"storage" withClass:NSClassFromString(@"WXStorageModule")];
[self registerModule:@"clipboard" withClass:NSClassFromString(@"WXClipboardModule")];
[self registerModule:@"globalEvent" withClass:NSClassFromString(@"WXGlobalEventModule")];
[self registerModule:@"canvas" withClass:NSClassFromString(@"WXCanvasModule")];
[self registerModule:@"picker" withClass:NSClassFromString(@"WXPickerModule")];
[self registerModule:@"meta" withClass:NSClassFromString(@"WXMetaModule")];
[self registerModule:@"webSocket" withClass:NSClassFromString(@"WXWebSocketModule")];
}複製程式碼
WXSDKEngine會預設註冊這15種基礎模組。這裡就以比較複雜的模組WXWebSocketModule為例,來看看它是如何被註冊的。
+ (void)registerModule:(NSString *)name withClass:(Class)clazz
{
WXAssert(name && clazz, @"Fail to register the module, please check if the parameters are correct !");
// 1. WXModuleFactory註冊模組
NSString *moduleName = [WXModuleFactory registerModule:name withClass:clazz];
// 2.遍歷所有同步和非同步方法
NSDictionary *dict = [WXModuleFactory moduleMethodMapsWithName:moduleName];
// 3.把模組註冊到WXBridgeManager中
[[WXSDKManager bridgeMgr] registerModules:dict];
}複製程式碼
註冊模組也分3步,第一步是在WXModuleFactory中註冊。
@interface WXModuleFactory ()
@property (nonatomic, strong) NSMutableDictionary *moduleMap;
@property (nonatomic, strong) NSLock *moduleLock;
@end複製程式碼
在WXModuleFactory中,moduleMap會儲存所有的模組的配置資訊,註冊的過程也是生成moduleMap的過程。
- (NSString *)_registerModule:(NSString *)name withClass:(Class)clazz
{
WXAssert(name && clazz, @"Fail to register the module, please check if the parameters are correct !");
[_moduleLock lock];
// 這裡需要注意的是:註冊模組是允許同名模組的
WXModuleConfig *config = [[WXModuleConfig alloc] init];
config.name = name;
config.clazz = NSStringFromClass(clazz);
[config registerMethods];
[_moduleMap setValue:config forKey:name];
[_moduleLock unlock];
return name;
}複製程式碼
整個註冊的過程就是把WXModuleConfig為value,name為key,存入_moduleMap字典裡。
@interface WXModuleConfig : WXInvocationConfig
@end複製程式碼
WXModuleConfig僅僅只是繼承自WXInvocationConfig,所以它和WXInvocationConfig是完全一樣的。[config registerMethods]這個方法和註冊元件的方法是同一個方法,具體註冊流程這裡就不再贅述了。
在WXModuleFactory中會記錄下一個個的WXModuleConfig:
_moduleMap = {
animation = "<WXModuleConfig: 0x60000024a230>";
canvas = "<WXModuleConfig: 0x608000259ce0>";
clipboard = "<WXModuleConfig: 0x608000259b30>";
dom = "<WXModuleConfig: 0x608000259440>";
event = "<WXModuleConfig: 0x60800025a280>";
globalEvent = "<WXModuleConfig: 0x60000024a560>";
instanceWrap = "<WXModuleConfig: 0x608000259a70>";
meta = "<WXModuleConfig: 0x60000024a7a0>";
modal = "<WXModuleConfig: 0x6080002597d0>";
navigator = "<WXModuleConfig: 0x600000249fc0>";
picker = "<WXModuleConfig: 0x608000259e60>";
storage = "<WXModuleConfig: 0x60000024a4a0>";
stream = "<WXModuleConfig: 0x6080002596e0>";
syncTest = "<WXModuleConfig: 0x60800025a520>";
timer = "<WXModuleConfig: 0x60000024a380>";
webSocket = "<WXModuleConfig: 0x608000259fb0>";
webview = "<WXModuleConfig: 0x6080002598f0>";
}複製程式碼
每個WXModuleConfig中會記錄下所有的同步和非同步的方法。
config.name = dom,
config.clazz = WXDomModule,
config.asyncMethods = {
addElement = "addElement:element:atIndex:";
addEvent = "addEvent:event:";
addRule = "addRule:rule:";
createBody = "createBody:";
createFinish = createFinish;
getComponentRect = "getComponentRect:callback:";
moveElement = "moveElement:parentRef:index:";
refreshFinish = refreshFinish;
removeElement = "removeElement:";
removeEvent = "removeEvent:event:";
scrollToElement = "scrollToElement:options:";
updateAttrs = "updateAttrs:attrs:";
updateFinish = updateFinish;
updateStyle = "updateStyle:styles:";
},
config.syncMethods = {
}複製程式碼
第二步遍歷所有的方法列表。
- (NSMutableDictionary *)_moduleMethodMapsWithName:(NSString *)name
{
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
NSMutableArray *methods = [self _defaultModuleMethod];
[_moduleLock lock];
[dict setValue:methods forKey:name];
WXModuleConfig *config = _moduleMap[name];
void (^mBlock)(id, id, BOOL *) = ^(id mKey, id mObj, BOOL * mStop) {
[methods addObject:mKey];
};
[config.syncMethods enumerateKeysAndObjectsUsingBlock:mBlock];
[config.asyncMethods enumerateKeysAndObjectsUsingBlock:mBlock];
[_moduleLock unlock];
return dict;
}複製程式碼
這裡遍歷模組的方法列表和元件的有所不同。首先模組是有預設方法的。
- (NSMutableArray*)_defaultModuleMethod
{
return [NSMutableArray arrayWithObjects:@"addEventListener",@"removeAllEventListeners", nil];
}複製程式碼
所有的模組都有addEventListener和removeAllEventListeners方法。第二個不同就是模組會遍歷所有的同步和非同步方法,(元件只會遍歷非同步方法)。最終返回生成模組的所有方法的字典。
以dom模組為例,它返回的字典如下:
{
dom = (
addEventListener,
removeAllEventListeners,
addEvent,
removeElement,
updateFinish,
getComponentRect,
scrollToElement,
addRule,
updateAttrs,
addElement,
createFinish,
createBody,
updateStyle,
removeEvent,
refreshFinish,
moveElement
);
}複製程式碼
最後一步也是在WXBridgeManager註冊模組。
- (void)registerModules:(NSDictionary *)modules
{
if (!modules) return;
__weak typeof(self) weakSelf = self;
WXPerformBlockOnBridgeThread(^(){
[weakSelf.bridgeCtx registerModules:modules];
});
}複製程式碼
這裡註冊過程和元件是完全一樣的,也是在子執行緒@"com.taobao.weex.bridge"的jsThread中操作的。
- (void)registerModules:(NSDictionary *)modules
{
WXAssertBridgeThread();
if(!modules) return;
[self callJSMethod:@"registerModules" args:@[modules]];
}複製程式碼
這裡呼叫JS的方法名變為了@"registerModules",入參args就是第二步產生的方法字典。
args = (
{
dom = (
addEventListener,
removeAllEventListeners,
addEvent,
removeElement,
updateFinish,
getComponentRect,
scrollToElement,
addRule,
updateAttrs,
addElement,
createFinish,
createBody,
updateStyle,
removeEvent,
refreshFinish,
moveElement
);
}
)複製程式碼
同樣,此時模組並不會真正的被註冊上,因為JSFramework還沒有載入完成,這裡也會被新增進methodQueue快取起來。
註冊模組的全部流程如下:
最後是註冊Handlers。
+ (void)_registerDefaultHandlers
{
[self registerHandler:[WXResourceRequestHandlerDefaultImpl new] withProtocol:@protocol(WXResourceRequestHandler)];
[self registerHandler:[WXNavigationDefaultImpl new] withProtocol:@protocol(WXNavigationProtocol)];
[self registerHandler:[WXURLRewriteDefaultImpl new] withProtocol:@protocol(WXURLRewriteProtocol)];
[self registerHandler:[WXWebSocketDefaultImpl new] withProtocol:@protocol(WXWebSocketHandler)];
}複製程式碼
WXSDKEngine中預設註冊4個Handler。
+ (void)registerHandler:(id)handler withProtocol:(Protocol *)protocol
{
WXAssert(handler && protocol, @"Fail to register the handler, please check if the parameters are correct !");
[WXHandlerFactory registerHandler:handler withProtocol:protocol];
}複製程式碼
WXSDKEngine會繼續呼叫WXHandlerFactory的registerHandler:withProtocol:方法。
@interface WXHandlerFactory : NSObject
@property (nonatomic, strong) WXThreadSafeMutableDictionary *handlers;
+ (void)registerHandler:(id)handler withProtocol:(Protocol *)protocol;
+ (id)handlerForProtocol:(Protocol *)protocol;
+ (NSDictionary *)handlerConfigs;
@end複製程式碼
WXHandlerFactory也是一個單例,裡面有一個執行緒安全的字典handlers,用來儲存例項和Protocol名的對映表。
WXSDKEngine初始化的最後一步就是執行JSFramework。
[[WXSDKManager bridgeMgr] executeJsFramework:script];複製程式碼
WXSDKManager會呼叫WXBridgeManager去執行SDK裡面的main.js檔案。
- (void)executeJsFramework:(NSString *)script
{
if (!script) return;
__weak typeof(self) weakSelf = self;
WXPerformBlockOnBridgeThread(^(){
[weakSelf.bridgeCtx executeJsFramework:script];
});
}複製程式碼
WXBridgeManager通過WXBridgeContext呼叫executeJsFramework:方法。這裡方法呼叫也是在子執行緒中進行的。
- (void)executeJsFramework:(NSString *)script
{
WXAssertBridgeThread();
WXAssertParam(script);
WX_MONITOR_PERF_START(WXPTFrameworkExecute);
[self.jsBridge executeJSFramework:script];
WX_MONITOR_PERF_END(WXPTFrameworkExecute);
if ([self.jsBridge exception]) {
NSString *message = [NSString stringWithFormat:@"JSFramework executes error: %@", [self.jsBridge exception]];
WX_MONITOR_FAIL(WXMTJSFramework, WX_ERR_JSFRAMEWORK_EXECUTE, message);
} else {
WX_MONITOR_SUCCESS(WXMTJSFramework);
// 至此JSFramework算完全載入完成了
self.frameworkLoadFinished = YES;
// 執行所有註冊的JsService
[self executeAllJsService];
// 獲取JSFramework版本號
JSValue *frameworkVersion = [self.jsBridge callJSMethod:@"getJSFMVersion" args:nil];
if (frameworkVersion && [frameworkVersion isString]) {
// 把版本號存入WXAppConfiguration中
[WXAppConfiguration setJSFrameworkVersion:[frameworkVersion toString]];
}
// 執行之前快取在_methodQueue陣列裡面的所有方法
for (NSDictionary *method in _methodQueue) {
[self callJSMethod:method[@"method"] args:method[@"args"]];
}
[_methodQueue removeAllObjects];
// 至此,初始化工作算完成了。
WX_MONITOR_PERF_END(WXPTInitalize);
};
}複製程式碼
WX_MONITOR_PERF_START是在操作之前標記WXPTFrameworkExecute。執行完JSFramework以後,用WX_MONITOR_PERF_END標記執行完成。
- (void)executeJSFramework:(NSString *)frameworkScript
{
WXAssertParam(frameworkScript);
if (WX_SYS_VERSION_GREATER_THAN_OR_EQUAL_TO(@"8.0")) {
[_jsContext evaluateScript:frameworkScript withSourceURL:[NSURL URLWithString:@"main.js"]];
}else{
[_jsContext evaluateScript:frameworkScript];
}
}複製程式碼
載入JSFramework的核心程式碼在這裡,通過JSContext執行evaluateScript:來載入JSFramework。由於這裡並沒有返回值,所以載入的JSFramework的目的僅僅是宣告瞭裡面的所有方法,並沒有呼叫。這也符合OC載入其他Framework的過程,載入只是載入到記憶體中,Framework裡面的方法可以隨時被呼叫,而不是一載入就呼叫其所有的方法。
載入完成JSFramework以後,就要開始載入之前快取的JSService和JSMethod。JSService是在jsServiceQueue中快取的。JSMethod是在methodQueue中快取的。
- (void)executeAllJsService
{
for(NSDictionary *service in _jsServiceQueue) {
NSString *script = [service valueForKey:@"script"];
NSString *name = [service valueForKey:@"name"];
[self executeJsService:script withName:name];
}
[_jsServiceQueue removeAllObjects];
}複製程式碼
JSService由於是直接js轉成NSString,所以這裡直接執行executeJsService:withName即可。
for (NSDictionary *method in _methodQueue) {
[self callJSMethod:method[@"method"] args:method[@"args"]];
}
[_methodQueue removeAllObjects];
- (JSValue *)callJSMethod:(NSString *)method args:(NSArray *)args
{
WXLogDebug(@"Calling JS... method:%@, args:%@", method, args);
NSLog(@"WXJSCoreBridge jsContext 正要呼叫方法");
return [[_jsContext globalObject] invokeMethod:method withArguments:args];
}複製程式碼
由於_methodQueue裡面裝的都是全域性的js方法,所以需要呼叫invokeMethod: withArguments:去執行。
當這一切都載入完成,SDK的初始化工作就基本完成了,這裡就會標記上WXPTInitalize結束。
這裡還需要說明的是,jsBridge第一次是如何被載入進來的。
- (id<WXBridgeProtocol>)jsBridge
{
WXAssertBridgeThread();
_debugJS = [WXDebugTool isDevToolDebug];
Class bridgeClass = _debugJS ? NSClassFromString(@"WXDebugger") : [WXJSCoreBridge class];
if (_jsBridge && [_jsBridge isKindOfClass:bridgeClass]) {
return _jsBridge;
}
if (_jsBridge) {
[_methodQueue removeAllObjects];
_frameworkLoadFinished = NO;
}
_jsBridge = _debugJS ? [NSClassFromString(@"WXDebugger") alloc] : [[WXJSCoreBridge alloc] init];
[self registerGlobalFunctions];
return _jsBridge;
}複製程式碼
第一次進入這個函式沒有jsBridge例項的時候,會先生成WXJSCoreBridge的例項,然後緊接著註冊全域性的函式。等第二次再呼叫這個函式的時候,_jsBridge已經是WXJSCoreBridge型別了,就會直接return,下面的語句也不會再重複執行了。
typedef NSInteger(^WXJSCallNative)(NSString *instance, NSArray *tasks, NSString *callback);
typedef NSInteger(^WXJSCallAddElement)(NSString *instanceId, NSString *parentRef, NSDictionary *elementData, NSInteger index);
typedef NSInvocation *(^WXJSCallNativeModule)(NSString *instanceId, NSString *moduleName, NSString *methodName, NSArray *args, NSDictionary *options);
typedef void (^WXJSCallNativeComponent)(NSString *instanceId, NSString *componentRef, NSString *methodName, NSArray *args, NSDictionary *options);複製程式碼
這4個閉包就是OC封裝暴露給JS的4個全域性函式。
- (void)registerCallNative:(WXJSCallNative)callNative
{
JSValue* (^callNativeBlock)(JSValue *, JSValue *, JSValue *) = ^JSValue*(JSValue *instance, JSValue *tasks, JSValue *callback){
NSString *instanceId = [instance toString];
NSArray *tasksArray = [tasks toArray];
NSString *callbackId = [callback toString];
WXLogDebug(@"Calling native... instance:%@, tasks:%@, callback:%@", instanceId, tasksArray, callbackId);
return [JSValue valueWithInt32:(int32_t)callNative(instanceId, tasksArray, callbackId) inContext:[JSContext currentContext]];
};
_jsContext[@"callNative"] = callNativeBlock;
}複製程式碼
- (void)registerCallAddElement:(WXJSCallAddElement)callAddElement
{
id callAddElementBlock = ^(JSValue *instanceId, JSValue *ref, JSValue *element, JSValue *index, JSValue *ifCallback) {
NSString *instanceIdString = [instanceId toString];
NSDictionary *componentData = [element toDictionary];
NSString *parentRef = [ref toString];
NSInteger insertIndex = [[index toNumber] integerValue];
WXLogDebug(@"callAddElement...%@, %@, %@, %ld", instanceIdString, parentRef, componentData, (long)insertIndex);
return [JSValue valueWithInt32:(int32_t)callAddElement(instanceIdString, parentRef, componentData, insertIndex) inContext:[JSContext currentContext]];
};
_jsContext[@"callAddElement"] = callAddElementBlock;
}複製程式碼
- (void)registerCallNativeModule:(WXJSCallNativeModule)callNativeModuleBlock
{
_jsContext[@"callNativeModule"] = ^JSValue *(JSValue *instanceId, JSValue *moduleName, JSValue *methodName, JSValue *args, JSValue *options) {
NSString *instanceIdString = [instanceId toString];
NSString *moduleNameString = [moduleName toString];
NSString *methodNameString = [methodName toString];
NSArray *argsArray = [args toArray];
NSDictionary *optionsDic = [options toDictionary];
WXLogDebug(@"callNativeModule...%@,%@,%@,%@", instanceIdString, moduleNameString, methodNameString, argsArray);
NSInvocation *invocation = callNativeModuleBlock(instanceIdString, moduleNameString, methodNameString, argsArray, optionsDic);
JSValue *returnValue = [JSValue wx_valueWithReturnValueFromInvocation:invocation inContext:[JSContext currentContext]];
return returnValue;
};
}複製程式碼
- (void)registerCallNativeComponent:(WXJSCallNativeComponent)callNativeComponentBlock
{
_jsContext[@"callNativeComponent"] = ^void(JSValue *instanceId, JSValue *componentName, JSValue *methodName, JSValue *args, JSValue *options) {
NSString *instanceIdString = [instanceId toString];
NSString *componentNameString = [componentName toString];
NSString *methodNameString = [methodName toString];
NSArray *argsArray = [args toArray];
NSDictionary *optionsDic = [options toDictionary];
WXLogDebug(@"callNativeComponent...%@,%@,%@,%@", instanceIdString, componentNameString, methodNameString, argsArray);
callNativeComponentBlock(instanceIdString, componentNameString, methodNameString, argsArray, optionsDic);
};
}複製程式碼
由於JS的方法的寫法,多個引數是依次寫在小括號裡面的,和OC多個引數中間用:號隔開是不一樣的,所有在暴露給JS的時候,需要把Block再包裝一層。包裝的4個方法如上,最後把這4個方法注入到JSContext中。
如上圖,灰色的就是OC本地傳入的Block,外面在包一層,變成JS的方法,注入到JSContext中。
4. 模擬器WXSimulatorShortcutManager連線本地除錯工具
#if TARGET_OS_SIMULATOR
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[WXSimulatorShortcutManager registerSimulatorShortcutWithKey:@"i" modifierFlags:UIKeyModifierCommand | UIKeyModifierAlternate action:^{
NSURL *URL = [NSURL URLWithString:@"http://localhost:8687/launchDebugger"];
NSURLRequest *request = [NSURLRequest requestWithURL:URL];
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request
completionHandler:
^(NSData *data, NSURLResponse *response, NSError *error) {
// ...
}];
[task resume];
WXLogInfo(@"Launching browser...");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 連線websocket偵錯程式
[self connectDebugServer:@"ws://localhost:8687/debugger/0/renderer"];
});
}];
});
#endif複製程式碼
由於平時開發可能用到模擬器,那麼除錯的時候就會連線到本地的瀏覽器(Chrome,Safari)進行除錯介面。這裡就是在開啟模擬的時候,啟動瀏覽器,並且連線websocket偵錯程式。
WXSDKEngine初始化的全部流程可以大概描述如下圖:
(二). Weex 是如何讓JS調起OC原生UIView的?
上一章節我們分析了WXSDKEngine是如何初始化的,那麼初始化完成之後,iOS Native客戶端是如何接收到JS的頁面並呼叫OC生成UIView的呢?這一章節我們來分析分析。
在分析這個問題之前,先來看看AppStore上面Weex官方為我們提供的例項程式WeexPlayground的掃碼功能是怎麼實現掃描二維碼就可以進入到一個頁面的。
1.掃二維碼的原理
首先看一下掃碼介面的一些屬性:
@interface WXScannerVC : UIViewController <AVCaptureMetadataOutputObjectsDelegate>
@property (nonatomic, strong) AVCaptureSession * session;
@property (nonatomic, strong) AVCaptureVideoPreviewLayer *captureLayer;
@end複製程式碼
這個頁面沒有額外的配置,就是一些呼叫攝像頭的代理。
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection
{
[_captureLayer removeFromSuperlayer];
[_session stopRunning];
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
if (metadataObjects.count > 0) {
AVMetadataMachineReadableCodeObject * metadataObject = [metadataObjects objectAtIndex : 0 ];
[self openURL:metadataObject.stringValue];
}
}複製程式碼
當掃描到二維碼以後,代理會呼叫上面這個函式,掃描出來的URL就是metadataObject.stringValue。
- (void)openURL:(NSString*)URL
{
NSString *transformURL = URL;
NSArray* elts = [URL componentsSeparatedByString:@"?"];
if (elts.count >= 2) {
NSArray *urls = [elts.lastObject componentsSeparatedByString:@"="];
for (NSString *param in urls) {
if ([param isEqualToString:@"_wx_tpl"]) {
transformURL = [[urls lastObject] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
break;
}
}
}
NSURL *url = [NSURL URLWithString:transformURL];
if ([self remoteDebug:url]) {
return;
}
[self jsReplace:url];
WXDemoViewController * controller = [[WXDemoViewController alloc] init];
controller.url = url;
controller.source = @"scan";
NSMutableDictionary *queryDict = [NSMutableDictionary new];
if (WX_SYS_VERSION_GREATER_THAN_OR_EQUAL_TO(@"8.0")) {
NSURLComponents *components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO];
NSArray *queryItems = [components queryItems];
for (NSURLQueryItem *item in queryItems)
[queryDict setObject:item.value forKey:item.name];
}else {
queryDict = [self queryWithURL:url];
}
NSString *wsport = queryDict[@"wsport"] ?: @"8082";
NSURL *socketURL = [NSURL URLWithString:[NSString stringWithFormat:@"ws://%@:%@", url.host, wsport]];
controller.hotReloadSocket = [[SRWebSocket alloc] initWithURL:socketURL protocols:@[@"echo-protocol"]];
controller.hotReloadSocket.delegate = controller;
[controller.hotReloadSocket open];
[[self navigationController] pushViewController:controller animated:YES];
}複製程式碼
上面這段是完成的開啟二維碼頁面的程式碼,裡面包含判斷URL的query引數的一些處理。稍微簡化一下,簡化成下面的樣子:
- (void)openURL:(NSString*)URL
{
// 1.獲取URL
NSString *transformURL = URL;
NSURL *url = [NSURL URLWithString:transformURL];
// 2.配置新頁面的url
WXDemoViewController * controller = [[WXDemoViewController alloc] init];
controller.url = url;
controller.source = @"scan";
// 3.連線websocket
NSString *wsport = queryDict[@"wsport"] ?: @"8082";
NSURL *socketURL = [NSURL URLWithString:[NSString stringWithFormat:@"ws://%@:%@", url.host, wsport]];
controller.hotReloadSocket = [[SRWebSocket alloc] initWithURL:socketURL protocols:@[@"echo-protocol"]];
controller.hotReloadSocket.delegate = controller;
[controller.hotReloadSocket open];
// 4.頁面跳轉
[[self navigationController] pushViewController:controller animated:YES];
}複製程式碼
openURL:其實就幹了上面註釋說的4件事情。最重要的就是給新的介面配置了URL,至於連線websocket是為了更改.we檔案或者.vue檔案能及時的在手機上看見更改。最後一步就是頁面跳轉。所以掃描二維碼能開啟一個新的頁面,原因只是給這個新的頁面配置了一個URL,僅此而已。
2.JS是如何調起OC原生View的
再次回到我們的主題上來,JS究竟是如何調起OC原生View的?
所有的祕密都在WXSDKInstance這個類裡面。
@interface WXSDKInstance : NSObject
// 當前需要渲染的viewController
@property (nonatomic, weak) UIViewController *viewController;
// Native根容器的View是完全受WXSDKInstance控制,開發者無法更改
@property (nonatomic, strong) UIView *rootView;
// 如果元件想固定rootview的frame,可以把這個屬性設定為YES,當weex進行layout的時候,就不會改變rootview的frame了。反之設定為NO
@property (nonatomic, assign) BOOL isRootViewFrozen;
// weex bundle的scriptURL
@property (nonatomic, strong) NSURL *scriptURL;
// 父Instance
@property (nonatomic, weak) WXSDKInstance *parentInstance;
// 父Instance節點的引用
@property (nonatomic, weak) NSString *parentNodeRef;
// 用來標識當前weex instance獨一無二的ID
@property (nonatomic, strong) NSString *instanceId;
// 當前weex instance的狀態
@property (nonatomic, assign) WXState state;
// 當weex instance完成rootView的建立時的回撥block
@property (nonatomic, copy) void (^onCreate)(UIView *);
// 根容器的frame改變時候的回撥
@property (nonatomic, copy) void (^onLayoutChange)(UIView *);
// 當weex instance完成渲染時的回撥block
@property (nonatomic, copy) void (^renderFinish)(UIView *);
// 當weex instance重新整理完成時的回撥block
@property (nonatomic, copy) void (^refreshFinish)(UIView *);
// 當weex instance渲染失敗時的回撥block
@property (nonatomic, copy) void (^onFailed)(NSError *error);
// 當weex instance頁面滾動時的回撥block
@property (nonatomic, copy) void (^onScroll)(CGPoint contentOffset);
// 當weex instance渲染進行中的回撥block
@property (nonatomic, copy) void (^onRenderProgress)(CGRect renderRect);
// 當前weex instance的frame
@property (nonatomic, assign) CGRect frame;
// user儲存的一些Info資訊
@property (nonatomic, strong) NSMutableDictionary *userInfo;
// css單元和裝置畫素的換算比例因子
@property (nonatomic, assign, readonly) CGFloat pixelScaleFactor;
// 是否監測元件的渲染
@property (nonatomic, assign)BOOL trackComponent;
- (void)renderWithURL:(NSURL *)url;
- (void)renderWithURL:(NSURL *)url options:(NSDictionary *)options data:(id)data;
- (void)renderView:(NSString *)source options:(NSDictionary *)options data:(id)data;
// forcedReload為YES,每次載入都會從URL重新讀取,為NO,會從快取中讀取
- (void)reload:(BOOL)forcedReload;
- (void)refreshInstance:(id)data;
- (void)destroyInstance;
- (id)moduleForClass:(Class)moduleClass;
- (WXComponent *)componentForRef:(NSString *)ref;
- (NSUInteger)numberOfComponents;
- (BOOL)checkModuleEventRegistered:(NSString*)event moduleClassName:(NSString*)moduleClassName;
- (void)fireModuleEvent:(Class)module eventName:(NSString *)eventName params:(NSDictionary*)params;
- (void)fireGlobalEvent:(NSString *)eventName params:(NSDictionary *)params;
- (NSURL *)completeURL:(NSString *)url;
@end複製程式碼
一個WXSDKInstance就對應一個UIViewController,所以每個Weex的頁面都有一個與之對應的WXSDKInstance。
@property (nonatomic, strong) WXSDKInstance *instance;複製程式碼
WXSDKInstance主要用來渲染頁面,一般通過呼叫renderWithURL方法。
一個Weex介面的主動渲染的過程如下:
- (void)render
{
CGFloat width = self.view.frame.size.width;
[_instance destroyInstance];
_instance = [[WXSDKInstance alloc] init];
_instance.viewController = self;
_instance.frame = CGRectMake(self.view.frame.size.width-width, 0, width, _weexHeight);
__weak typeof(self) weakSelf = self;
_instance.onCreate = ^(UIView *view) {
[weakSelf.weexView removeFromSuperview];
weakSelf.weexView = view;
[weakSelf.view addSubview:weakSelf.weexView];
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, weakSelf.weexView);
};
_instance.onFailed = ^(NSError *error) {
};
_instance.renderFinish = ^(UIView *view) {
[weakSelf updateInstanceState:WeexInstanceAppear];
};
_instance.updateFinish = ^(UIView *view) {
};
if (!self.url) {
WXLogError(@"error: render url is nil");
return;
}
NSURL *URL = [self testURL: [self.url absoluteString]];
NSString *randomURL = [NSString stringWithFormat:@"%@%@random=%d",URL.absoluteString,URL.query?@"&":@"?",arc4random()];
[_instance renderWithURL:[NSURL URLWithString:randomURL] options:@{@"bundleUrl":URL.absoluteString} data:nil];
}複製程式碼
由於WXSDKInstance是支援實時重新整理,所以在建立的時候需要先銷燬掉原來的,再建立一個新的。
WXSDKInstance支援設定各種狀態時候的回撥callback函式,具體支援哪些狀態,可以看上面WXSDKInstance的定義。
Weex支援從本地載入JS,也支援從伺服器載入JS。如果從本地載入,那麼可以用下面的方法,從本地載入一個JSBundle。
- (void)loadLocalBundle:(NSURL *)url
{
NSURL * localPath = nil;
NSMutableArray * pathComponents = nil;
if (self.url) {
pathComponents =[NSMutableArray arrayWithArray:[url.absoluteString pathComponents]];
[pathComponents removeObjectsInRange:NSRangeFromString(@"0 3")];
[pathComponents replaceObjectAtIndex:0 withObject:@"bundlejs"];
NSString *filePath = [NSString stringWithFormat:@"%@/%@",[NSBundle mainBundle].bundlePath,[pathComponents componentsJoinedByString:@"/"]];
localPath = [NSURL fileURLWithPath:filePath];
}else {
NSString *filePath = [NSString stringWithFormat:@"%@/bundlejs/index.js",[NSBundle mainBundle].bundlePath];
localPath = [NSURL fileURLWithPath:filePath];
}
NSString *bundleUrl = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@/bundlejs/",[NSBundle mainBundle].bundlePath]].absoluteString;
[_instance renderWithURL:localPath options:@{@"bundleUrl":bundleUrl} data:nil];
}複製程式碼
最後渲染頁面就是通過呼叫renderWithURL:options:data:做到的。
- (void)renderWithURL:(NSURL *)url options:(NSDictionary *)options data:(id)data
{
if (!url) {
WXLogError(@"Url must be passed if you use renderWithURL");
return;
}
WXResourceRequest *request = [WXResourceRequest requestWithURL:url resourceType:WXResourceTypeMainBundle referrer:@"" cachePolicy:NSURLRequestUseProtocolCachePolicy];
[self _renderWithRequest:request options:options data:data];
}複製程式碼
在WXSDKInstance呼叫renderWithURL:options:data:方法的時候,會生成一個WXResourceRequest。NSMutableURLRequest定義如下:
@interface WXResourceRequest : NSMutableURLRequest
@property (nonatomic, strong) id taskIdentifier;
@property (nonatomic, assign) WXResourceType type;
@property (nonatomic, strong) NSString *referrer;
@property (nonatomic, strong) NSString *userAgent;
@end複製程式碼
WXResourceRequest其實也就是對NSMutableURLRequest的一層封裝。
下面來分析一下最核心的函式renderWithURL:options:data:(以下的程式碼實現在原始碼的基礎上略有刪減,原始碼太長,刪減以後並不影響閱讀)
- (void)_renderWithRequest:(WXResourceRequest *)request options:(NSDictionary *)options data:(id)data;
{
NSURL *url = request.URL;
_scriptURL = url;
_options = options;
_jsData = data;
NSMutableDictionary *newOptions = [options mutableCopy] ?: [NSMutableDictionary new];
WX_MONITOR_INSTANCE_PERF_START(WXPTJSDownload, self);
__weak typeof(self) weakSelf = self;
_mainBundleLoader = [[WXResourceLoader alloc] initWithRequest:request];
// 請求完成的回撥
_mainBundleLoader.onFinished = ^(WXResourceResponse *response, NSData *data) {
__strong typeof(weakSelf) strongSelf = weakSelf;
if ([response isKindOfClass:[NSHTTPURLResponse class]] && ((NSHTTPURLResponse *)response).statusCode != 200) {
NSError *error = [NSError errorWithDomain:WX_ERROR_DOMAIN
code:((NSHTTPURLResponse *)response).statusCode
userInfo:@{@"message":@"status code error."}];
if (strongSelf.onFailed) {
strongSelf.onFailed(error);
}
return ;
}
if (!data) {
if (strongSelf.onFailed) {
strongSelf.onFailed(error);
}
return;
}
NSString *jsBundleString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (!jsBundleString) {
return;
}
[strongSelf _renderWithMainBundleString:jsBundleString];
};
// 請求失敗的回撥
_mainBundleLoader.onFailed = ^(NSError *loadError) {
if (weakSelf.onFailed) {
weakSelf.onFailed(loadError);
}
};
[_mainBundleLoader start];
}複製程式碼
上面程式碼只要就是幹了2件事情,第一步,生成了WXResourceLoader,並設定了它的onFinished和onFailed回撥。第二步呼叫了start方法。
在WXSDKInstance中強持有了一個WXResourceLoader,WXResourceLoader的定義如下:
@interface WXResourceLoader : NSObject
@property (nonatomic, strong) WXResourceRequest *request;
@property (nonatomic, copy) void (^onDataSent)(unsigned long long /* bytesSent */, unsigned long long /* totalBytesToBeSent */);
@property (nonatomic, copy) void (^onResponseReceived)(const WXResourceResponse *);
@property (nonatomic, copy) void (^onDataReceived)(NSData *);
@property (nonatomic, copy) void (^onFinished)(const WXResourceResponse *, NSData *);
@property (nonatomic, copy) void (^onFailed)(NSError *);
- (instancetype)initWithRequest:(WXResourceRequest *)request;
- (void)start;
- (void)cancel:(NSError **)error;
@end複製程式碼
WXResourceLoader裡面含有一個WXResourceRequest,所以WXResourceRequest也可以看出對網路請求的封裝,並且提供了5種不同狀態的callback回撥函式。
- (void)start
{
if ([_request.URL isFileURL]) {
[self _handleFileURL:_request.URL];
return;
}
id requestHandler = [WXHandlerFactory handlerForProtocol:@protocol(WXResourceRequestHandler)];
if (requestHandler) {
[requestHandler sendRequest:_request withDelegate:self];
} else if ([WXHandlerFactory handlerForProtocol:NSProtocolFromString(@"WXNetworkProtocol")]){
// deprecated logic
[self _handleDEPRECATEDNetworkHandler];
} else {
WXLogError(@"No resource request handler found!");
}
} 複製程式碼
在呼叫了WXResourceLoader的start方法以後,會先判斷是不是本地的url,如果是本地的檔案,那麼就直接開始載入。
- (void)_handleFileURL:(NSURL *)url
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *fileData = [[NSFileManager defaultManager] contentsAtPath:[url path]];
if (self.onFinished) {
self.onFinished([WXResourceResponse new], fileData);
}
});
}複製程式碼
本地檔案就直接回撥onFinished函式。
如果不是本地的檔案,就開始發起網路請求,請求伺服器端的js檔案。
- (void)sendRequest:(WXResourceRequest *)request withDelegate:(id<WXResourceRequestDelegate>)delegate
{
if (!_session) {
NSURLSessionConfiguration *urlSessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
if ([WXAppConfiguration customizeProtocolClasses].count > 0) {
NSArray *defaultProtocols = urlSessionConfig.protocolClasses;
urlSessionConfig.protocolClasses = [[WXAppConfiguration customizeProtocolClasses] arrayByAddingObjectsFromArray:defaultProtocols];
}
_session = [NSURLSession sessionWithConfiguration:urlSessionConfig
delegate:self
delegateQueue:[NSOperationQueue mainQueue]];
_delegates = [WXThreadSafeMutableDictionary new];
}
NSURLSessionDataTask *task = [_session dataTaskWithRequest:request];
request.taskIdentifier = task;
[_delegates setObject:delegate forKey:task];
[task resume];
}複製程式碼
這裡的網路請求就是普通的正常的NSURLSession網路請求。
如果成功,最終都會執行onFinished的回撥函式。
_mainBundleLoader.onFinished = ^(WXResourceResponse *response, NSData *data) {
__strong typeof(weakSelf) strongSelf = weakSelf;
if ([response isKindOfClass:[NSHTTPURLResponse class]] && ((NSHTTPURLResponse *)response).statusCode != 200) {
NSError *error = [NSError errorWithDomain:WX_ERROR_DOMAIN
code:((NSHTTPURLResponse *)response).statusCode
userInfo:@{@"message":@"status code error."}];
if (strongSelf.onFailed) {
strongSelf.onFailed(error);
}
return ;
}
if (!data) {
NSString *errorMessage = [NSString stringWithFormat:@"Request to %@ With no data return", request.URL];
WX_MONITOR_FAIL_ON_PAGE(WXMTJSDownload, WX_ERR_JSBUNDLE_DOWNLOAD, errorMessage, strongSelf.pageName);
if (strongSelf.onFailed) {
strongSelf.onFailed(error);
}
return;
}
NSString *jsBundleString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"下載下來的 jsBundleString = %@",jsBundleString);
if (!jsBundleString) {
WX_MONITOR_FAIL_ON_PAGE(WXMTJSDownload, WX_ERR_JSBUNDLE_STRING_CONVERT, @"data converting to string failed.", strongSelf.pageName)
return;
}
WX_MONITOR_SUCCESS_ON_PAGE(WXMTJSDownload, strongSelf.pageName);
WX_MONITOR_INSTANCE_PERF_END(WXPTJSDownload, strongSelf);
[strongSelf _renderWithMainBundleString:jsBundleString];
};複製程式碼
在onFinished的回撥中,還會有3種錯誤判斷,status code error,no data return,data converting to string failed。
NSString *jsBundleString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
[strongSelf _renderWithMainBundleString:jsBundleString];複製程式碼
如果一切正常,那麼在onFinished的回撥中其實就是拿到jsBundleString,並執行渲染操作。
- (void)_renderWithMainBundleString:(NSString *)mainBundleString
{
//以下程式碼有刪減,去除了一些錯誤判斷,但是不影響閱讀
NSMutableDictionary *dictionary = [_options mutableCopy];
//生成WXRootView
WXPerformBlockOnMainThread(^{
_rootView = [[WXRootView alloc] initWithFrame:self.frame];
_rootView.instance = self;
if(self.onCreate) {
self.onCreate(_rootView);
}
});
// 再次註冊預設的模組modules、元件components、handlers,以確保在建立instance之前它們都被註冊了
[WXSDKEngine registerDefaults];
// 開始createInstance
[[WXSDKManager bridgeMgr] createInstance:self.instanceId template:mainBundleString options:dictionary data:_jsData];
}複製程式碼
這裡WXSDKEngine還會重新再次註冊一遍模組modules、元件components、handlers,以確保在建立instance之前它們都被註冊了。
- (void)createInstance:(NSString *)instance
template:(NSString *)temp
options:(NSDictionary *)options
data:(id)data
{
if (!instance || !temp) return;
if (![self.instanceIdStack containsObject:instance]) {
if ([options[@"RENDER_IN_ORDER"] boolValue]) {
[self.instanceIdStack addObject:instance];
} else {
[self.instanceIdStack insertObject:instance atIndex:0];
}
}
__weak typeof(self) weakSelf = self;
WXPerformBlockOnBridgeThread(^(){
[weakSelf.bridgeCtx createInstance:instance
template:temp
options:options
data:data];
});
}複製程式碼
WXSDKManager中會呼叫createInstance:template:options:data:方法,這個方法也必須在JSThread中執行。
- (void)createInstance:(NSString *)instance
template:(NSString *)temp
options:(NSDictionary *)options
data:(id)data
{
if (![self.insStack containsObject:instance]) {
if ([options[@"RENDER_IN_ORDER"] boolValue]) {
[self.insStack addObject:instance];
} else {
[self.insStack insertObject:instance atIndex:0];
}
}
//create a sendQueue bind to the current instance
NSMutableArray *sendQueue = [NSMutableArray array];
[self.sendQueue setValue:sendQueue forKey:instance];
NSArray *args = nil;
if (data){
args = @[instance, temp, options ?: @{}, data];
} else {
args = @[instance, temp, options ?: @{}];
}
[self callJSMethod:@"createInstance" args:args];
}複製程式碼
最終還是WXJSCoreBridge裡面的JSContext呼叫
[[_jsContext globalObject] invokeMethod:method withArguments:args];複製程式碼
呼叫JS的"createInstance"方法。從此處開始,就開始和JSFramework進行相互呼叫了。
在舉例之前,我們先把前面的流程畫圖總結一下:
下面舉例請見下篇。
接下來用一個例子來說明JS是如何呼叫起OC原生的View的。
先用JS寫一個頁面:
<template>
<div class="container">
<image src="http://9.pic.paopaoche.net/up/2016-7/201671315341.png" class="pic" onclick="picClick"></image>
<text class="text">{{title}}</text>
</div>
</template>
<style>
.container{
align-items: center;
}
.pic{
width: 200px;
height: 200px;
}
.text{
font-size: 40px;
color: black;
}
</style>
<script>
module.exports = {
data:{
title:'Hello World',
toggle:false,
},
ready:function(){
console.log('this.title == '+this.title);
this.title = 'hello Weex';
console.log('this.title == '+this.title);
},
methods:{
picClick: function () {
this.toggle = !this.toggle;
if(this.toggle){
this.title = '圖片被點選';
}else{
this.title = 'Hello Weex';
}
}
}
}
</script>複製程式碼
這個頁面跑起來長下面這個樣子:
上面是我的.we原始檔,經過Weex編譯以後,就變成了index.js,裡面的程式碼如下:
// { "framework": "Weex" }
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId])
/******/ return installedModules[moduleId].exports;
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ exports: {},
/******/ id: moduleId,
/******/ loaded: false
/******/ };
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/ // Flag the module as loaded
/******/ module.loaded = true;
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/ // Load entry module and return exports
/******/ return __webpack_require__(0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ function(module, exports, __webpack_require__) {
var __weex_template__ = __webpack_require__(1)
var __weex_style__ = __webpack_require__(2)
var __weex_script__ = __webpack_require__(3)
__weex_define__('@weex-component/916f9ecb075bbff1f4ea98389a4bb514', [], function(__weex_require__, __weex_exports__, __weex_module__) {
__weex_script__(__weex_module__, __weex_exports__, __weex_require__)
if (__weex_exports__.__esModule && __weex_exports__.default) {
__weex_module__.exports = __weex_exports__.default
}
__weex_module__.exports.template = __weex_template__
__weex_module__.exports.style = __weex_style__
})
__weex_bootstrap__('@weex-component/916f9ecb075bbff1f4ea98389a4bb514',undefined,undefined)
/***/ },
/* 1 */
/***/ function(module, exports) {
module.exports = {
"type": "div",
"classList": [
"container"
],
"children": [
{
"type": "image",
"attr": {
"src": "http://9.pic.paopaoche.net/up/2016-7/201671315341.png"
},
"classList": [
"pic"
],
"events": {
"click": "picClick"
}
},
{
"type": "text",
"classList": [
"text"
],
"attr": {
"value": function () {return this.title}
}
}
]
}
/***/ },
/* 2 */
/***/ function(module, exports) {
module.exports = {
"container": {
"alignItems": "center"
},
"pic": {
"width": 200,
"height": 200
},
"text": {
"fontSize": 40,
"color": "#000000"
}
}
/***/ },
/* 3 */
/***/ function(module, exports) {
module.exports = function(module, exports, __weex_require__){'use strict';
module.exports = {
data: function () {return {
title: 'Hello World',
toggle: false
}},
ready: function ready() {
console.log('this.title == ' + this.title);
this.title = 'hello Weex';
console.log('this.title == ' + this.title);
},
methods: {
picClick: function picClick() {
this.toggle = !this.toggle;
if (this.toggle) {
this.title = '圖片被點選';
} else {
this.title = 'Hello Weex';
}
}
}
};}
/* generated by weex-loader */
/***/ }
/******/ ]);複製程式碼
看上去一堆程式碼,實際上仔細看看,就能看出門道。
(function(modules) { // webpackBootstrap
…… ……
}複製程式碼
這段程式碼是自動加的,暫時不管。然後下面有4段程式碼,開頭都分別編了序號,0,1,2,3。1,2,3段程式碼就是分別對應<template>
,<style>
,<script>
。上述這段程式碼就是從伺服器請求下來的程式碼。
那伺服器拿到JS以後,OC會呼叫JS的方法createInstance(id, code, config, data)方法。
args:(
0,
“(這裡是網路上下載的JS,由於太長了,省略)”,
{
bundleUrl = "http://192.168.31.117:8081/HelloWeex.js";
debug = 1;
}
)複製程式碼
接著會在JSFramework裡面執行一些轉換的操作:
[JS Framework] create an Weex@undefined instance from undefined [;
[JS Framework] Intialize an instance with: undefined [;
[JS Framework] define a component @weex-component/916f9ecb075bbff1f4ea98389a4bb514 [;
[JS Framework] bootstrap for @weex-component/916f9ecb075bbff1f4ea98389a4bb514 [;
[JS Framework] "init" lifecycle in Vm(916f9ecb075bbff1f4ea98389a4bb514) [;
[JS Framework] "created" lifecycle in Vm(916f9ecb075bbff1f4ea98389a4bb514) [;
[JS Framework] compile native component by {"type":"div","classList":["container"],"children":[{"type":"image","attr":{"src":"http://9.pic.paopaoche.net/up/2016-7/201671315341.png"},"classList":["pic"],"events":{"click":"picClick"}},{"type":"text","classList":["text"],"attr":{}}]} [;
[JS Framework] compile to create body for div [;
[JS Framework] compile to append single node for {"ref":"_root","type":"div","attr":{},"style":{"alignItems":"center"}}複製程式碼
接下來JSFramework就會呼叫OC的callNative方法。呼叫dom模組的createBody方法,建立rootView。引數如下:
(
{
args = (
{
attr = {
};
ref = "_root";
style = {
alignItems = center;
};
type = div;
}
);
method = createBody;
module = dom;
}
)複製程式碼
建立好rootView以後,接著要繼續新增View了。
[JS Framework] compile native component by {"type":"image","attr":{"src":"http://9.pic.paopaoche.net/up/2016-7/201671315341.png"},"classList":["pic"],"events":{"click":"picClick"}} [;
[JS Framework] compile to create element for image [;
[JS Framework] compile to append single node for {"ref":"3","type":"image","attr":{"src":"http://9.pic.paopaoche.net/up/2016-7/201671315341.png"},"style":{"width":200,"height":200},"event":["click"]}複製程式碼
JSFramework繼續呼叫OC的callAddElement方法新增View。引數如下:
{
attr = {
src = "http://9.pic.paopaoche.net/up/2016-7/201671315341.png";
};
event = (
click
);
ref = 3;
style = {
height = 200;
width = 200;
};
type = image;
}複製程式碼
UIImage新增完成以後,再接著新增UIlabel。
[JS Framework] compile native component by {"type":"text","classList":["text"],"attr":{}} [;
[JS Framework] compile to create element for text [;
[JS Framework] compile to append single node for {"ref":"4","type":"text","attr":{"value":"Hello World"},"style":{"fontSize":40,"color":"#000000"}}複製程式碼
JSFramework繼續呼叫OC的callAddElement方法新增View。引數如下:
{
attr = {
value = "Hello World";
};
ref = 4;
style = {
color = "#000000";
fontSize = 40;
};
type = text;
}複製程式碼
當ready以後:
[JS Framework] "ready" lifecycle in Vm(916f9ecb075bbff1f4ea98389a4bb514)複製程式碼
JSFramework繼續呼叫OC的callNative方法,引數如下:
(
{
args = (
4,
{
value = "hello Weex";
}
);
method = updateAttrs;
module = dom;
}
)複製程式碼
至此,所有的佈局已經完成。JSFramework會繼續呼叫OC的callNative方法。
(
{
args = (
);
method = createFinish;
module = dom;
}
)複製程式碼
到此為止,所有的View都已經建立完成了。最終整個佈局如下:
{layout: {width: 414, height: 672, top: 0, left: 0}, flexDirection: 'column', alignItems: 'stretch', flex: 0, width: 414, height: 672, left: 0, top: 0, children: [
{_root:div layout: {width: 414, height: 672, top: 0, left: 0}, flexDirection: 'column', alignItems: 'center', flex: 0, width: 414, height: 672, children: [
{3:image layout: {width: 110.4, height: 110.4, top: 0, left: 151.8}, flexDirection: 'column', alignItems: 'stretch', flex: 0, width: 110.4, height: 110.4, },
{4:text layout: {width: 107.333, height: 26.6667, top: 110.4, left: 153.333}, flexDirection: 'column', alignItems: 'stretch', flex: 0, },
]},
]}複製程式碼
從最終的layout來看,我們可以看出,每一個module,component都有其對應的獨一無二的id。
接著下一步操作是WXImageComponent更新圖片。更新結束以後,整個Render就算徹底完成了。
JSFramework在整個過程中扮演的角色是根據輸入的JSBundle,不斷的輸出Json格式的Virtual DOM,然後通過JSCore呼叫OC原生方法,生成View。
上面這個例子中,JSFramework的工作原理基本就展現出來了。大體流程如下圖:
接下來詳細總結一下JSFramework在整個Native端是如何工作的。
首先JSFramework的初始化只會在App啟動時初始化一次,多個頁面都共享這一份JSFramework。這個設計也提高了Weex所有頁面的開啟速度, JS Framework 的啟動過程幾百毫秒,相當於每個頁面開啟的時候,這幾百毫秒都被節省下來了。
雖然JSFramework全域性只有一個,那麼Weex是如何避免多個Weex在同一個JS Runtime裡面相互互不影響?Weex採取了2方面的措施,一是要求每個Weex頁面都必須要建立一個全域性唯一的 instance ID,通過這個ID直接能對應一個Weex頁面。二是JS與Native進行相互呼叫的時候,每個方法都要求第一個引數是ID。比如createInstance(id, code, config, data),sendTasks(id, tasks),receiveTasks(id, tasks)。這樣不同頁面的狀態就被隔離到了不同的閉包中了,這樣就做到了相互不影響。
當Native需要渲染頁面的時候,會主動呼叫createInstance(id, code, config, data)方法,其中code引數就是JS Bundle轉換成的String。JSFramework接收到了這段入參以後,就會開始解析,並開始sendTasks(id, tasks)。
sendTasks(id, tasks)會通過JSBridge呼叫OC Native方法。tasks裡面會指定功能的模組名、方法名以及引數。比如:
sendTasks(id, [{ module: 'dom', method: 'removeElement', args: [elementRef]}])複製程式碼
這裡就會呼叫之前註冊到JSContext的OC方法。
客戶端也會呼叫receiveTasks(id, tasks)方法,呼叫JS的方法。receiveTasks 中有兩種方式,一種是fireEvent,對應的是客戶端在某個DOM元素上觸發的事件,比如fireEvent(titleElementRef, 'click', eventObject);另一種則是callback,即前面功能模組呼叫之後產生的回撥,比如我們通過fetch介面向Native端傳送一個 HTTP 請求,並設定了一個回撥函式,這個時候,先在JS端為這個回撥函式生成一個callbackID,比如字串 "123",這個是傳送給Native端的是這個callbackID,當請求結束之後,native需要把請求結果返還給JS Runtime,為了能夠前後對得上,這個回撥最終會成為類似 callback(callbackID, result) 的格式。
四.關於Weex,ReactNative,JSPatch
這一章本來是不在這個文章之中的,但是由於近期蘋果稽核,帶來了一些稽核風波,於是打算在這裡稍微提提。
在各位讀者看到這篇文章的時候,純的ReactNative和純的Weex的專案已經可以完美通過稽核了,JSPatch依舊處於被封殺的狀態。
既然本篇文章分析了Weex的工作原理,那麼就稍微談談RN,Weex和JSpatch的區別。
首先他們三者都是基於JS來進行熱修復的,但是RN,Weex和JSPatch有一個最大的不同是,如果Native沒有提供可以供JS呼叫的方法介面的話,那麼在RN和Weex介面怎麼也無法實現Native的一些方法的。
但是JSPatch不同,雖然它也是一套基於JSCore的bridge,但是它是基於Runtime的,基於OC的Runtime,可以實現各種需求,即使預先Native沒有暴露出來的介面,都可以新增方法實現需求,也可以更改已經實現的方法。
從熱更新的能力來看,RN和Weex的能力僅僅只是中等能力,而JSPatch是幾乎無所不能,Runtime都實現的,它都能實現。
所以從熱更新的能力上看,RN和Weex都不能改變Native原生程式碼,也無法動態呼叫Native系統私有API。所以蘋果稽核允許RN和Weex通過。
最後
本篇文章只講述了Weex是如何在iOS Native端跑起來的原理,但是關於Weex其實還有很多沒有解釋,比如說在Vue.js頁面更改了一個頁面元素,是怎麼能讓Native頁面及時的變更?Weex的頁面是怎麼通過FlexBox演算法進行渲染的?前端頁面是如何打包成JS bundle的?.we和.vue檔案是怎麼通過DSL被翻譯的?如何利用JS的Runtime寫一些強大的JSService?webpackBootstrap和weex-loader是如何生成最終的JS程式碼的,中間有哪些優化?……
以上的這些問題都會在接下來一系列的Weex文章裡面一一詳解,希望大家多多指點!