2016年4月21日,阿里巴巴在Qcon大會上宣佈跨平臺移動開發工具Weex開放內測邀請。Weex可以完美兼顧性能與動態性,讓移動開發者經過簡捷的前端語法寫出Native級別的性能體驗,並支持iOS、安卓、YunOS及Web等多端部署。javascript
近一年來,ReactNative 和 Weex 這些跨平臺技術對Native開發者來講,衝擊是巨大的。Native在開發App的時候存在一些弊端,好比客戶端須要頻繁更新,iOS更新時間還要受到審覈的牽制;iOS、Android和前端同時開發同一個需求,在人員成本上消耗大;Hybrid的性能和Native相比又差了一點。css
ReactNative 和 Weex的出現,就是爲了解決這些痛點的。html
從4月21號宣佈內測之後,短短兩週就有超過5000名開發者申請。前端
2016年6月30日阿里巴巴正式宣佈Weex開源。號稱能夠用Web方式,開發Native級性能體驗的億級應用匠心打造跨平臺移動開發工具Weex在開源首日就登上Github趨勢榜首位,截止目前爲止,Weex在GitHub上的Star數已經到達了13393了。成爲中國2016年在Github上最熱門的開源項目之一。vue
Weex從出生那天起,彷彿就是和ReactNative是「一對」。html5
ReactNative宣稱「Learn once, write anywhere」,而Weex宣稱「Write Once, Run Everywhere」。Weex從出生那天起,就被給予了一統三端的厚望。ReactNative能夠支持iOS、Android,而Weex能夠支持iOS、Android、HTML5。一統三端就解決了前言裏面說的第二個痛點,同時開發浪費人員成本的問題。java
Native移動開發者只須要在本地導入Weex的SDK,就能夠經過HTML/CSS/JavaScript網頁的這套編程語言來開發Native級別的Weex界面。這意味着能夠直接用現有Web開發的編輯器和IDE的代碼補全、提示、檢查等功能。從而也給前端人員開發Native端,較低的開發成本和學習成本。node
Weex是一種輕量級、可擴展、高性能框架。集成也很方便,能夠直接在HTML5頁面嵌入,也可嵌在原生UI中。因爲和ReactNative同樣,都會調用Native端的原生控件,因此在性能上比Hybrid高出一個層次。這就解決了前言裏面所說的第三個痛點,性能問題。react
Weex很是輕量,體積小巧,語法簡單,方便接入和上手。ReactNative官方只容許將ReactNative基礎js庫和業務JS一塊兒打成一個JS bundle,沒有提供分包的功能,因此若是想節約流量就必須製做分包打包工具。而Weex默認打的JS bundle只包含業務JS代碼,體積小不少,基礎JS庫包含在Weex SDK中,這一點Weex與Facebook的React Native和微軟的Cordova相比,Weex更加輕量,體積小巧。把Weex生成的JS bundle輕鬆部署到服務器端,而後Push到客戶端,或者客戶端請求新的資源便可完成發佈。如此快速的迭代就解決了前言裏面說的第一個痛點,發佈沒法控制時間,webpack
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是如何把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的總體流程,因爲筆者前端知識匱乏,因此從.we或者.vue文件到JS bundle前端這部分的源碼分析本文暫時不涉及,等筆者熟悉前端之後,這塊還會再補上來。
分析以前先說明一點,Weex的全部源碼其實已經開源了,至於SDK的Demo裏面還依賴了一個ATSDK.framework,這個是沒有開源的。ATSDK.framework這個實際上是Weex性能監控的插件。
就是上圖中的那個灰色的框框的插件。這個插件有些大廠有本身的APM,阿里暫時沒有開源這塊,可是對Weex全部功能是不影響的。
那麼接下來就詳細分析一下在iOS Native端,Weex是如何跑起來的。直接上源碼分析。
這是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。接下來一步步的分析。
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;複製代碼
這些函數暫時這裏沒有用到,暫時先不解析了。
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。
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中。
#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初始化的所有流程能夠大概描述以下圖:
上一章節咱們分析了WXSDKEngine是如何初始化的,那麼初始化完成以後,iOS Native客戶端是如何接收到JS的頁面並調用OC生成UIView的呢?這一章節咱們來分析分析。
在分析這個問題以前,先來看看AppStore上面Weex官方爲咱們提供的實例程序WeexPlayground的掃碼功能是怎麼實現掃描二維碼就能夠進入到一個頁面的。
首先看一下掃碼界面的一些屬性:
@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,僅此而已。
再次回到咱們的主題上來,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; } idrequestHandler = [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) 的格式。
這一章原本是不在這個文章之中的,可是因爲近期蘋果審覈,帶來了一些審覈風波,因而打算在這裏稍微提提。
在各位讀者看到這篇文章的時候,純的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文章裏面一一詳解,但願你們多多指點!