Skip to content

iOS Architecture

zhangyanqiang edited this page Apr 26, 2018 · 1 revision

框架

小程序iOS 主要是基于Webview来运行的,核心框架由四个部分组成 Webview(基于 WKWebview), Executor(基于 JavascriptCore) WebCache (基于 NSProtocal )Container (基于 UIView)

Webview

wkwebview

在iOS8之后,ios新加入wkwebview 用来代替UIWebview,在性能方面做了很多优化(具体变化请参考官方文档)。本项目基于WKWebview来实现。

在正常的webview使用的基础上,本项目主要做了如下变化。

url获取方式

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];
     }
MessageHandler

通过实现代理 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

Executor 是异步执行 js 的解释器,通过该解释器能实现异步执行 js 代码,为实现该功能需要在两个移动端自定义 线程、队列,timer等组件,这一优化解决了页面 JS 执行时滑动操作卡顿的问题,并且使得 Native 开发者可以直接向 Javascript 执行器添加 Bridge 方法或对象,而不需要考虑 Web View 平台差异性,保证了 IOS 和 Android 原生调用接口统一。

QIYIExecutorTimer

创建弱引用的timer,使timer能在JS与Native通信的过程内存正确管理

QIYIExecutorQueue

创建可控制的串行队列

QIYIExecutor

主要是对 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];
            }
        };
QIYIThreadExecutor

主要创建相应的线程,将 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];
}
QIYIThreadExecutorLocal

管理本地创建的QIYIExecutorQueue和QIYIExecutor,将线程与对应的执行器绑定

QIYIExecutorExport

通过实现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];
    }
}
QIYINetworkExport

通过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];
        }];
    }];
}

Webcache

通过拦截标识的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];
    }
}

Container

将 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];
        }];
    }
}
Clone this wiki locally