-
Notifications
You must be signed in to change notification settings - Fork 100
iOS Architecture
小程序iOS 主要是基于Webview来运行的,核心框架由四个部分组成 Webview(基于 WKWebview), Executor(基于 JavascriptCore) WebCache (基于 NSProtocal )Container (基于 UIView)
在iOS8之后,ios新加入wkwebview 用来代替UIWebview,在性能方面做了很多优化(具体变化请参考官方文档)。本项目基于WKWebview来实现。
在正常的webview使用的基础上,本项目主要做了如下变化。
webview所需加载的url统一通过asset接口的形式通过外部实现提供,通过该接口将放置html位置的本地路径,传送进来。代码如下
NSString *html = [asset obtainHtmlPath];
if (html != nil) {
NSURL* fileURL = [NSURL fileURLWithPath:html];
NSData *data = [NSData dataWithContentsOfFile:html];
NSString* str = [[NSString alloc] initWithData:data
encoding:kCFStringEncodingUTF8];
[self loadHTMLString:str baseURL:fileURL];
}
通过实现代理 WKScriptMessageHandler 将四个native方法注入到 HTML 中 。代码如下
WKUserContentController* userContentController = [[WKUserContentController alloc] init];
[userContentController addScriptMessageHandler:_delegate name:@"message"];
[userContentController addScriptMessageHandler:_delegate name:@"finish_construct"];
[userContentController addScriptMessageHandler:_delegate name:@"native_call"];
[userContentController addScriptMessageHandler:_delegate name:@"emit"];
WKWebViewConfiguration* configuration = [[WKWebViewConfiguration alloc] init];
configuration.userContentController = userContentController;
-(void) userContentController:(WKUserContentController*)userContentController
didReceiveScriptMessage:(WKScriptMessage *)message {
if ([@"message" isEqualToString:message.name]) {
} else if ([@"finish_construct" isEqualToString:message.name]) {
[self onDidConstructed];
} else if ([@"emit" isEqualToString:message.name]) {
if (self.onRecvScriptEmit) {
self.onRecvScriptEmit(message.body);
}
} else if ([@"native_call" isEqualToString:message.name]) {
if (self.onRecvNativeCall) {
self.onRecvNativeCall(message.body);
}
}
}
Executor 是异步执行 js 的解释器,通过该解释器能实现异步执行 js 代码,为实现该功能需要在两个移动端自定义 线程、队列,timer等组件,这一优化解决了页面 JS 执行时滑动操作卡顿的问题,并且使得 Native 开发者可以直接向 Javascript 执行器添加 Bridge 方法或对象,而不需要考虑 Web View 平台差异性,保证了 IOS 和 Android 原生调用接口统一。
创建弱引用的timer,使timer能在JS与Native通信的过程内存正确管理
创建可控制的串行队列
主要是对 JavascriptCore中 JSContext、 JSExport、Timer 等组件的封装,以及异常的处理
self.timer = [[QIYIExecutorTimer alloc] init];
_context = [[JSContext alloc] init];
__weak QIYIExecutor* ws = self;
_context.exceptionHandler = ^(JSContext* context, JSValue* exception) {
if (ws) {
[ws handleException:context exception:exception];
}
};
self.export = [[QIYIExecutorExportImpl alloc] init];
_context[@"__base__"] = self.export;
self.network = [[QIYINetworkExport alloc] init];
_context[@"network"] = self.network;
QIYIExecutorTimer* timer = self.timer;
_context[@"global"] = _context.globalObject;
_context[@"setTimeout"] = ^(JSValue* callback, NSNumber* timeout) {
if (nil == callback || nil == timeout) {
return @(0);
}
return @([timer addNode:timeout.integerValue withBlock:callback loop:NO]);
};
_context[@"setInterval"] = ^(JSValue* callback, NSNumber* timeout) {
if (nil == callback || nil == timeout) {
return @(0);
}
return @([timer addNode:timeout.integerValue withBlock:callback loop:YES]);
};
_context[@"clearTimeout"] = ^(NSNumber* handle) {
if (nil != handle) {
[timer removeNodeForHandle:handle.integerValue];
}
};
_context[@"clearInterval"] = ^(NSNumber* handle) {
if (nil != handle) {
[timer removeNodeForHandle:handle.integerValue];
}
};
主要创建相应的线程,将 QIYIExecutor 、NSThread、QIYIExecutorQueue、QIYIExecutorTimer等部分封装为一个整体,将线程中的任务都按正确的顺序添加到线程中让Executor执行。
-(void) threadMain {
NSArray* arr = nil;
Boolean shouldExit = NO;
QIYIExecutorTimer* timer = self.timer;
QIYIExecutor* executor = self.executor;
[QIYIThreadExecutorLocal attachExecutorQueue:self.threadQueue];
while (1) {
if (nil != timer) {
arr = [self.threadQueue obtian:[timer nextTimeout]];
[timer tick];
}
for (id object in arr) {
if ([NSNull null] == object) {
shouldExit = YES;
} else {
typedef void(^bThreadBlock)(QIYIExecutor *);
((bThreadBlock) object)(executor);
}
}
if (shouldExit) {
break;
}
}
[QIYIThreadExecutorLocal detachExecutorQueue];
}
管理本地创建的QIYIExecutorQueue和QIYIExecutor,将线程与对应的执行器绑定
通过实现JSExpot协议,实现 js 可以直接调用native的方法 ,js 的 diff 操作或者需要 native 执行的功能可以通过这些接口去调用。代码如下
@protocol QIYIExecutorExport<JSExport>
-(void) postPatch:(NSString*)patch;
JSExportAs(triggerEvent, -(void) triggerEvent:(NSString*)type arguments:(JSValue *)arguments);
-(BOOL) finish:(id)arguments;
-(BOOL) share:(id)argument;
@end
-(void) postPatch:(NSString*)patch {
if (self.delegate && patch) {
[self.delegate postPatch:patch];
}
}
-(void) triggerEvent:(NSString*)type arguments:(JSValue*)arguments{
NSDictionary* dic = nil;
if (nil != arguments) {
dic = __safe_convert([arguments toObject], NSDictionary);
}
if (self.delegate) {
[self.delegate triggerEvent:type withArguments:dic];
}
}
通过JSExport 实现网络实现,JS 需要的网络请求可以在native去执行,将获取的返回 通过 JSValue 回传给 js 端
@protocol QIYINetworkExport <JSExport>
JSExportAs(get, -(void)getUri:(NSString*)uri
args:(id)arguments success:(JSValue*)success fail:(JSValue*)fail);
@end
-(void)getUri:(NSString*)uri
args:(id)arguments success:(JSValue*)success fail:(JSValue*)fail {
QIYIExecutorQueue* queue = [QIYIThreadExecutorLocal currentExecutorQueue];
if (nil == queue || nil == uri) {
return;
}
NSDictionary* dic = __safe_convert(arguments, NSDictionary);
[QIYINetDownload get:uri arguments:nil success:^(id object) {
NSString* str = [[NSString alloc] initWithData:object
encoding:NSUTF8StringEncoding];
[queue post:^(QIYIExecutor* executor) {
[success callWithArguments:@[str]];
}];
} failure:^{
[queue post:^(QIYIExecutor* executor) {
[fail callWithArguments:nil];
}];
}];
}
通过拦截标识的uri来将一些网络资源替换为本地资源,加速页面渲染速度,减少网络请求,优化性能。
- (void)startLoading{
NSMutableURLRequest *mutableReqeust = [[self request] mutableCopy];
[NSURLProtocol setProperty:@YES forKey:QIYIURLProtocolKey inRequest:mutableReqeust];
if ([mutableReqeust.URL.absoluteString hasPrefix:@"file:///"]){
NSString *filePath = mutableReqeust.URL.absoluteString;
NSData *imageData = nil;
if ([mutableReqeust.URL.absoluteString hasPrefix:@"file:///res"]) {
NSArray *data = [filePath componentsSeparatedByString:@"?"];
if (data.count != 2) { return;}
filePath = data[0];
filePath = [filePath stringByReplacingOccurrencesOfString:@"file://" withString:@""];
if ([QIYIAssetManager shareInstance].asset != nil) {
imageData = [[QIYIAssetManager shareInstance].asset obtainFile:filePath];
}
}else{
filePath = [filePath stringByReplacingOccurrencesOfString:@"file://" withString:@""];
imageData = [NSData dataWithContentsOfFile:filePath];
}
if (imageData == nil) {return;}
NSURLResponse* response = [[NSURLResponse alloc] initWithURL:self.request.URL MIMEType:@"image/" expectedContentLength:imageData.length textEncodingName:nil];
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];
[self.client URLProtocol:self didLoadData:imageData];
[self.client URLProtocolDidFinishLoading:self];
}
else {
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:nil];
self.task = [session dataTaskWithRequest:self.request];
[self.task resume];
}
}
将 Webview 和 Executor 包装在一起,展示业务所需的页面。 通过这个 view 来管理该 Webview 的所有事情,并且将 Webview 与 js 交互的所有逻辑通过这个类来转发。
执行MessageHandler
self.webview.onRecvScriptEmit = ^(NSString* script) {
};
self.webview.onRecvNativeCall = ^(NSString* buffer) {
};
加载base package 中的 css 文件
NSString* businessPath = [self.manifest obtainPage:self.name];
NSString* cssPath = [self.asset obtainBundleCss:businessPath];
NSData* dada = [[NSFileManager defaultManager] contentsAtPath:cssPath];
if (nil != dada) {
NSString* script = @"addCssNative('";
script = [script stringByAppendingString:cssPath];
script = [script stringByAppendingString:@"');"];
[self postPatch:script];
}
开始异步js线程,注入base js 和 bundle js
-(BOOL) load {
if (!self.asset || !self.name || !self.manifest || !self.executor) {
return NO;
}
// start inner thread
[self.executor start];
// executor base script
NSData* buffer = [self.asset obtainBaseScript];
if (nil != buffer) {
[self.executor post:^(QIYIExecutor* executor) {
[executor evaluateScript:buffer];
}];
}
// executor business script
NSString* businessPath = [self.manifest obtainPage:self.name];
NSData* businessBuffer = [self.asset obtainBundleScript:businessPath];
if (nil != businessBuffer) {
[self.executor post:^(QIYIExecutor* executor) {
[executor evaluateScript:businessBuffer];
}];
}
return YES;
}
异步执行 js
-(void) evaluateScript:(NSData*)buffer {
if (buffer && self.executor) {
[self.executor post:^(QIYIExecutor* executor) {
[executor evaluateScript:buffer];
}];
}
}