iOS插件化研究之一——JavaScriptCore

原文:http://chentoo.com/?p=191

一、前言

一样的开篇问题,为什么要研究这个?iOS为什么要插件化?为什么要借助其他语言比如html5 js甚至脚本lua等来实现原本OC/Swift应该实现的东西?

原因可以归结为两点:

1. iOS平台 appstore 审核速度不可控,而很多活动页面需要频繁更新,如果每次更新都走appstore审核流程,那活动也就不要做了。

2. 可多平台复用代码,节省开发成本。比如同一个活动的页面,用html5+js完成,就可以通用的在iOS Android平台上,而只需要维护一份html5+js代码。

现如今国内各大互联网公司的iOS端产品,绝大多是都有使用这种技术,特别是html5+js。而使用脚本语言来做动态更新的app也不在少数。

本文先讨论使用html5 + js来插件化的技术。

请浏览一篇文章来脑补一下,我们要做啥 分析支付宝客户端的插件机制

当然,现在的支付宝版本已经告别了这种显性插件化的机制。后面会具体说。

另外还要补充一个前提,我们绝对不做纯html5+js的app,因为稍复杂的app,使用纯html5的方式,只会给自己挖坑,现阶段,native+部分简单逻辑的html5才是真正切合实际的方案。这个问题不展开讨论了。


二、应该准备点什么?

首先我们得准备点东西,当然你要熟悉OC语言(swift亦可),然后你要了解html语言,能写几句js。

然后我们绝不用历史上”著名”的PhoneGap来做,因为它真的很弱。也不能简单的使用UIWebViewDelegate的一个方法来做简单的js 和 OC的通信,因为那是远远不够的。

我们要使用的是很早就出现并广泛运用在mac平台,但直到iOS7才进入移动平台的JavaScriptCore。这真的是iOS7开始原生提供的,真的不是私有的,真的你随便用


三、JavaScriptCore基础知识

3.1 JavaScriptCore是什么?

JavaScriptCore框架是基于webkit中以C/C++实现的JavaScriptCore的一个包装,之前广泛应用于mac平台,从iOS7开始,apple主动将其加入到iOS SDK中。JavaScriptCore让Objective-C和JavaScript代码的交互变得更加简单和直接。

JavaScriptCore中有几个重要的东西:

#import "JSContext.h"
#import "JSValue.h"
#import "JSManagedValue.h"
#import "JSVirtualMachine.h"
#import "JSExport.h"

他们都是日常使用中经常用到的东西。后面会结合实例,介绍他们都是干嘛的。

3.2 iOS如何使用JavaScriptCore?

在需要的地方,引入:

#import JavaScriptCore/JavaScriptCore.h

3.3 JavaScriptCore能用来做什么?

3.3.1 通过OC执行js方法或调取js属性。

比如下面的一个例子:

    JSContext *context = [[JSContext alloc] init];
    [context evaluateScript:@"var arr = [1, 2, ‘This is js string‘];var sum = function(a, b) { return a+b;}"];
    JSValue *jsArray = context[@"arr"];
    JSValue *jsSum = context[@"sum"];

    JSValue *jsSumResult = [jsSum callWithArguments:[NSArray arrayWithObjects:@12, @33, nil]];

JSContext对象是JS的运行环境,通过 -evaluateScript 方法可以运行一段javaScript。javaScript的所有变量方法都会在JSContext对象中妥善的保存。通过对JSContext对象的一些操作,可以调用javaScript的方法,或者存取javaScript的对象。

之前见到的JSVirtualMachine顾名思义,是javaScript的虚拟机,是为JSContext提供运行资源。JSVirtualMachine的具体使用在后面也会讲到。

但是我们的JSContext *context 是通过 init方法生成的啊,看似并没有搀和到JSVirtualMachine啊?但是其实,通过init方法生成的JSContext对象,在init方法内部,仍然会自动线生成一个JSVirtualMachine,然后调用JSContext 对象的 -initWithVirtualMachine 方法。故,一个JSContext对象,必定要对应着一个JSVirtualMachine对象。

同一个JSVirtualMachine中的若干个JSContext可以互相交换方法对象等等,但是不同的JSVirtualMachine不能互相交换任何JSContext的资源。

下图来自苹果官方,很形象的描述了这一点。

2015-03-11 :(不知为何,苹果删除了官方文档中关于JavaScriptCore的部分。。擦咧。。。)

JSValueJavaScriptCore中一个重要的类,前面说到,我们使用JSContextJSVirtualMachine 开拓了一个运行和保留javaScript的空间,而JSContextjavaScript的各个方法和属性对应着JSValue

JSValue也是OCjavaScript 互相访问和修改的中间体,所有OCjavaScript的跨语言操作都要通过JSValue一些方法进行。

比如我们示例代码里的,JSValue *jsArray,jsArray对应着javaScript中的一个 array对象:arr。所以我们可以对jsArray进行一些操作,从而操作javaScript 中的 arr。

例如:

jsArray[0];//1
jsArray[2];//This is js string
jsArray[1] = 49;//修改arr 的第二个元素。
jsArray[@"length"];//结果是3,调用js arr对象的方法。

又比如我们示例代码里的,jsSum,对应着sum function,因此我们可以通过操作jsSum从而调取javaScript中的 sum function。

    JSValue *jsSumResult = [jsSum callWithArguments:[NSArray arrayWithObjects:@12, @33, nil]];
//jsSumResult = 45;

可见,我们可以方便的通过JSValue对象的 callWithArguments:方法来直接调取 js 的 function。js function的多参数,在OC中,由NSArray组装而成。

3.3.2 通过js执行OC方法或调取OC属性。

苹果介绍,有两种方式可以方便的通过js 调用 OC:

  1. Block 用来调用方法。
  2. JSExport protocol 用来调用对象。

示例:

//我们有一个OC方法,提供给js调用
- (NSInteger)sumWithA:(NSInteger)a B:(NSInteger)b C:(NSInteger)c
{
    return a + b + c;
}

- (void)jsToOcFunction
{
    JSContext *context = [[JSContext alloc] init];
    context[@"sumNums"] = ^(NSInteger a, NSInteger b, NSInteger c) {
        return [self sumWithA:a B:b C:c];
    };

    JSValue *sum = [context evaluateScript:@"sumNums(7, 56, 22)"];
    NSLog(@"sum  %@", sum);//sum  85
}

在例子中,我们定义了一个OC方法:sumWithA:(NSInteger)a B:(NSInteger)b C:(NSInteger)c,提供给js调取使用。

在调取时,我们首先声明一个JSContext,然后对context 的sumNums 赋予了一个OC的Block,Block内执行了我们之前提供的OC方法。

然后我们通过context的 -evaluateScript方法执行了一个js方法,尝试调用OC函数。最终看到了调取成功的结果。

我们再举一个例子,来说明JSContext 在js 调用 OC方法中的重要性。

//尝试用OC来实现一个JavaScriptCore所不具有的Log
JSContext *context = [[JSContext alloc] init];
context[@"log"] = ^() {
    NSLog(@"+++++++Begin Log+++++++");

    NSArray *args = [JSContext currentArguments];
    for (JSValue *jsVal in args) {
        NSLog(@"%@", jsVal);
    }

    JSValue *this = [JSContext currentThis];
    NSLog(@"this: %@",this);
    NSLog(@"-------End Log-------");
};

[context evaluateScript:@"log(‘ider‘, [7, 21], { hello:‘world‘, js:100 });"];

//Output:
//  +++++++Begin Log+++++++
//  ider
//  7,21
//  [object Object]
//  this: [object GlobalObject]
//  -------End Log-------

这个经典的例子来自于 Ider的blog

例子中有两个关键点:

  1. [JSContext currentArguments] 类方法可以拿到js函数中的所有参数列表。每个参数在OC中也都用JSValue 描述。
  2. [JSContext currentThis] 类方法可以拿到调用该方法的对象。

需要特别注意的是:

1. 不论在任何情况下,不要在Block中直接使用外面的JSValue对象, 而应该把JSValue当做参数来传进Block中。

2. 不论在任何情况下,不要在Block中直接使用外面的JSContext对象, 而应该使用 [JSContext currentContext]获取。

上面我们看到了js调取OC方法的例子,下面我们看js通过JSExport protocol调取OC属性的例子:

//使用rumtime 为一个系统控件UIButton增加JSExport protocol
@protocol UIButtonExport <JSExport>
- (void)setTitle:(NSString *)title forState:(UIControlState)state;
@end

- (void)changeTitle
{
    class_addProtocol([UIButton class], @protocol(UIButtonExport));

    UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
    [button setTitle:@"你好 OC" forState:UIControlStateNormal];
    button.frame = CGRectMake(100, 100, 100, 100);
    [self.view addSubview:button];

    JSContext *context = [[JSContext alloc] init];
    context[@"button"] = button;
    [context evaluateScript:@"button.setTitleForState(‘你好 js‘, 0)"];
}

而除了上述通过runtime的方式增加JSExport protocol之外,还可以通过category的方式,比如:

//UIButton+js.h
#import <UIKit/UIKit.h>
#import <JavaScriptCore/JavaScriptCore.h>

@protocol UIButtonExport <JSExport>
- (void)setTitle:(NSString *)title forState:(UIControlState)state;
@end

@interface UIButton (js) <UIButtonExport>
@end

可以看到,如果想要在js中调用OC 的类或者对象的方法,需要将方法在JSExport protocol中声明。

下面再举一个更复杂的例子。

//book.js
//列出书的title
var bookTitleList = function(book1, book2) {
    var book1Title = book1.title;
    var book2Title = book2.title;
    return ‘list:‘+ book1Title + book2Title;
};
//创建两本书的合订本
var makeBookFromTwoBooks = function(book1, book2) {
    var title = book1.title + book2.title;
    var page = book1.page + book2.page;
    return Book.makeBookWithTitlePage(title, page);
};
//
//  Book.h
#import <Foundation/Foundation.h>
#import <JavaScriptCore/JavaScriptCore.h>
@class Book;

@protocol BookExports <JSExport>
@property NSString *title;
@property NSInteger page;
+ (Book *)makeBookWithTitle:(NSString *)title page:(NSInteger)page;
@end

@interface Book : NSObject <BookExports>
@property NSString *title;
@property NSInteger page;
+ (Book *)makeBookWithTitle:(NSString *)title page:(NSInteger)page;
@end

//
//  Book.m
#import "Book.h"
@implementation Book
+ (Book *)makeBookWithTitle:(NSString *)title page:(NSInteger)page
{
    Book *book = [[Book alloc] init];
    book.title = title;
    book.page = page;
    return book;
}
@end
- (void)moreExportsTest
{
    JSContext *context = [[JSContext alloc] init];

    NSString* path = [[NSBundle mainBundle] pathForResource:@"book" ofType:@"js"];
    NSString *jsString = [NSString stringWithContentsOfFile:path encoding:NSStringEncodingConversionAllowLossy error:nil];

    [context evaluateScript:jsString];

    Book *book1 = [Book makeBookWithTitle:@"资治通鉴第一部" page:330];
    Book *book2 = [Book makeBookWithTitle:@"资治通鉴第二部" page:520];

    JSValue *fucntion = context[@"bookTitleList"];
    JSValue *result = [fucntion callWithArguments:@[book1, book2]];
    NSLog(@"result  %@",result.toString);
//result  list:资治通鉴第一部资治通鉴第二部

    context[@"Book"] = [Book class];

    JSValue *function = context[@"makeBookFromTwoBooks"];
    JSValue *jsResult = [function callWithArguments:@[book1, book2]];

    Book *newBook = [jsResult toObject];

    NSLog(@"newBook  title %@ page: %ld",newBook.title, (long)newBook.page);
//newBook  title 资治通鉴第一部资治通鉴第二部 page: 850
}

第一段js,是我们的javascript文件。我们在其中定义了两个方法。

第二段,是我们定义了一个Book类,可以看到Book对象有两个属性:title 和 page,有一个类方法:+ (Book )makeBookWithTitle:(NSString )title page:(NSInteger)page,用来生成新的Book。这个方法将在js中被调用。

然后我们声明了,@protocol BookExports ,将需要在js中直接访问的属性和方法放置进去。

最后,让我们的Book类遵循BookExports protocol(@interface Book : NSObject )。

这样一个可在js中直接访问的类就生成完毕了。

最后一段是我们的运行代码。

首先将一段js代码加载斤JSContext中并执行。之后声明两个Book对象,book1 book2。将其作为参数传入js函数bookTitleList中。可以看到执行结果与预期相同。在js中,我们直接book1.title取出了OC对象的属性。

紧接着,我们执行了第二个js函数,在其中,用js调用了OC的Book的类方法,创建出了一个新的OC 的Book对象并返回。

到这里,例子就讲完了。可以看到通过神奇的JavaScriptCore,OC语言和javascript语言几乎畅通无阻。而这将是我们以后构建可插件化的iOS架构的基础。

时间: 2024-10-07 00:31:15

iOS插件化研究之一——JavaScriptCore的相关文章

Android应用程序插件化研究之AssertManager

最近在研究Android应用的插件化开发,看了好几个相关的开源项目.插件化都是在解决以下几个问题: 如何把插件apk中的代码和资源加载到当前虚拟机. 如何把插件apk中的四大组件注册到进程中. 如何防止插件apk中的资源和宿主apk中的资源引用冲突. 就这几个问题,我开始研究插件化开发实现的相关技术. 在上篇文章中我讲了如何把插件apk中的class加载到当前进程 的问题,本篇文章主要讲第一点的第二点:如何加载另一个apk中的资源到当前应用中. AssetManager介绍 当我们在组件中获取资

android插件化研究

使用 android:sharedUserId="com.wallj.skin" 1.总共三个工程,包名分别为com.wallj.main/com.wallj.spring/com.wallj.summer 其中com.wallj.main为主工程,其他两个为插件工程,均只为主工程提供资源. 三个工程需要使用同一个 android:sharedUserId=" com.wallj.skin"以达到资源共享的目的. 设置了该属性后,三个工程之间的ClassLoader

iOS开发------Widget(Today Extension)插件化开发

iOS10.0发布啦(貌似过去有点时间了吧 - -),在宏观带给我们使用体验的提升之外,更多的是带给iOS开发者一定的欣喜. 因为我们又要学习新东西来适配10啦. 博文所说的Widget(以下称之为拓展应用)并不是iOS10系统新推出的插件化应用(其实早在iOS8上就已经出现啦,只不过楼主是在iOS10发布之后才算真正的关注它,实在是惭愧呀).iOS10之前它仅仅是存在于通知那一栏中,至于多隐蔽我就不说了吧.但在iOS10之后获得重生,地位获得了巨大的提升,从这点也不难看出苹果增加了对它的重视.

RN学习1——前奏,app插件化和热更新的探索

react_native_banner-min.png React Native(以下简称RN)有大量前端开发者的追捧.前端开发是一个活跃的社区,一直尝试着一统前后端,做一个全栈开发,RN就是他们在客户端领域的尝试. 说是从零开始,但其实我还是懂一点点JS代码的,而且算是一个有经验的iOS.Android开发,对很多js和native交互的细节和特性还算了解,在QDaily里面也做过好多hybird的尝试,还经常用JSPatch做hotfix,总的来说,就是对hot update.插件化以及hy

iOS 组件化架构漫谈

组件化架构漫谈 前段时间公司项目打算重构,准确来说应该是按之前的产品逻辑重写一个项目.在重构项目之前涉及到架构选型的问题,我和组里小伙伴一起研究了一下组件化架构,打算将项目重构为组件化架构.当然不是直接拿来照搬,还是要根据公司具体的业务需求设计架构. 在学习组件化架构的过程中,从很多高质量的博客中学到不少东西,例如蘑菇街李忠.casatwy.bang的博客.在学习过程中也遇到一些问题,在微博和QQ上和一些做iOS的朋友进行了交流,非常感谢这些朋友的帮助. 本篇文章主要针对于之前蘑菇街提出的组件化

360手机卫士插件化RePlugin今日开源

写在前面 "RePlugin将在6月底开源,这将是我们献给安卓世界最好的礼物."当我们宣布这一消息时,心中的激动,无以言表.是的,三年的"厚积",如今的"薄发",看似平凡的话,实际上却饱含了我们太多的激动.辛酸与泪. 那么今天,我们就来详细的和您聊一聊,这个从2014年中旬,正式在手机卫士上启用,并即将开源的360 RePlugin,究竟能为我们,更为您能带来什么. GitHub地址:https://github.com/Qihoo360/ReP

插件化技术:宿主访问插件资源

本文同步自wing的地方酒馆 最近在搞插件化,16年很火的东西,我又拖了1年才来研究,哈哈哈,正确下一个热门技术能提前一些吧. 今天想跟大家讨论一下我在研究插件化过程中,遇到的一个容易混淆的点,那就是资源访问. 首先感谢下在插件化道路上的老司机,无私的奉献资料. 看过很多插件化的文章,都提到了一大痛点是资源访问的问题.解决方法很通用,都是通过反射使用 AssetManager 的 addAssetPath 方法,把插件apk路径添加进去.再把系统的resource替换掉,就可以访问到资源了. 这

DroidPlugin插件化开发

360手机助手使用的 DroidPlugin,它是360手机助手团队在Android系统上实现了一种插件机制.它可以在无需安装.修改的情况下运行APK文件,此机制对改进大型APP的架构,实现多团队协作开发具有一定的好处. 它是一种新的插件机制,一种免安装的运行机制 github地址: https://github.com/DroidPluginTeam/DroidPlugin 参考博客:http://blog.csdn.net/hejjunlin/article/details/52124397

android插件化-apkplug框架基本结构-01

由于框架开发更新频繁的原因一直都没有时间写出框架的基本架构让大家云里雾里的,现在框架已基本稳定和完善,我就抽出时间写写关于apkplug框架的基本架构和原理,同时也跟大家一起研究利用apkplug框架玩出更新的功能. 一 apkplug的基本架构 apkplug简单的说是一个容器它将apk文件解析并映射成Bundle ,下文我们简称Bundle为插件.如下图 每一个apk插件被映射到apkplug框架中就是一个Bundle对象,完整路径为 org.osgi.framework.Bundle .通