JavaScriptCore that JS and native OC call each other in iOS

Recently, due to work and personal idleness, I have not updated the blog in a timely manner. I apologize to all students for this, so I will not talk about nonsense. Today we will talk about the mutual calls between JS and native OC in iOS, and directly upload the code information~

#pragma mark - This article is excerpted from: https://hjgitbook.gitbooks.io/ios/content/04-technical-research/04-javascriptcore-note.html

A Preliminary Study of JavaScriptCore

Note: JavaScriptCore API can also be called with Swift, this article uses Objective-C to introduce.

Before iOS7, it was difficult to communicate between native apps and web apps. If you want to render HTML or run JavaScript on an iOS device, you have to use UIWebView. Introduced in iOS7 JavaScriptCore, it is more powerful and easier to use.

Introduction to JavaScriptCore

JavaScriptCoreIt is an Objective-C API that encapsulates the bridge between JavaScript and Objective-C. With very little code, JavaScript can call Objective-C, or Objective-C can call JavaScript.

stringByEvaluatingJavaScriptFromString:In previous iOS versions, you could only execute a JavaScript script by sending a message to UIWebView . And if you want to call Objective-C from JavaScript, you must open a custom URL (eg: foo://), and then webView:shouldStartLoadWithRequest:navigationTypehandle it in UIWebView's delegate method.

However, it is now possible to take advantage of the advanced capabilities of JavaScriptCore, which can:

  • Run JavaScript without relying on UIWebView 
  • Use modern Objective-C syntax (such as Blocks and subscripts)
  • Pass values ​​or objects seamlessly between Objective-C and JavaScript
  • Create hybrid objects (native objects can have a JavaScript value or function as a property)

Benefits of developing with Objective-C and JavaScript:

  • Rapid development and prototyping :
    If the business requirements of an area change very frequently, JavaScript can be used to develop and prototype, which is more efficient than Objective-C. 
  • 团队职责划分
    这部分参考原文吧
    Since JavaScript is much easier to learn and use than Objective-C (especially if you develop a nice JavaScript sandbox), it can be handy to have one team of developers responsible for the Objective-C “engine/framework”, and another team of developers write the JavaScript that uses the “engine/framework”. Even non-developers can write JavaScript, so it’s great if you want to get designers or other folks on the team involved in certain areas of the app.
  • JavaScript is an interpreted language :
    JavaScript is interpreted, you can modify JavaScript code in real time and see the results immediately.
  • Write logic once and run on multiple platforms :
    The logic can be implemented in JavaScript, and can be called on both the iOS and Android sides

Overview of JavaScriptCore

JSValue : Represents a JavaScript entity. A JSValue can represent many JavaScript primitive types such as boolean, integers, doubles, and even objects and functions.
JSManagedValue : Essentially a JSValue, but can handle some special cases in memory management, it can help the correct conversion between the two memory management mechanisms of reference technology and garbage collection.
JSContext : represents the runtime environment of JavaScript, you need to use JSContext to execute JavaScript code. All JSValues ​​are bundled on a JSContext.
JSExport : This is a protocol that can be used to export native objects to JavaScript, so that the properties or methods of the native objects become the properties or methods of JavaScript, which is very magical.
JSVirtualMachine : Represents an object space with its own heap structure and garbage collection mechanism. In most cases you don't need to interact with it directly, unless you have to deal with some special multithreading or memory management issues.

JSContext / JSValue

JSVirtualMachineProvides the underlying resources for the execution JSContextof JavaScript, and provides a running environment for JavaScript, through

- (JSValue *)evaluateScript:(NSString *)script;

A method can execute a JavaScript script, and if there are methods, variables and other information in it, it will be stored in it for use when needed. The creation of JSContext is based on JSVirtualMachine:

- (id)initWithVirtualMachine:(JSVirtualMachine *)virtualMachine;

If it is used - (id)init;for initialization, a new JSVirtualMachine object will be automatically created inside it and then the previous initialization method will be called.

After creating a JSContext, it's easy to run JavaScript code to create variables, perform calculations, and even define methods:

JSContext *context = [[JSContext alloc] init];
[context evaluateScript:@"var num = 5 + 5"];
[context evaluateScript:@"var names = ['Grace', 'Ada', 'Margaret']"];
[context evaluateScript:@"var triple = function(value) { return value * 3 }"];
JSValue *tripleNum = [context evaluateScript:@"triple(num)"];

Any value from a JSContext can be wrapped in a JSValue object, which wraps every possible JavaScript value: strings and numbers; arrays, objects, and methods; even errors and special JavaScript values ​​such as null and undefined.

toStringYou can call the , toBool, toDouble, etc. methods on JSValue toArrayto convert it to a suitable Objective-C value or object.

Objective-C calls JavaScript

For example, there is a "Hello.js" file with the following content:

function printHello() {

}

Call the printHello method in Objective-C:

NSString *scriptPath = [[NSBundle mainBundle] pathForResource:@"hello" ofType:@"js"];
NSString *scriptString = [NSString stringWithContentsOfFile:scriptPath encoding:NSUTF8StringEncoding error:nil];

JSContext *context = [[JSContext alloc] init];
[context evaluateScript:scriptString];

JSValue *function = self.context[@"printHello"];
[function callWithArguments:@[]];

Analyze the above code:

First, a JSContext is initialized, and the JavaScript script is executed. At this time, the printHello function is not called, but is read into this context.

Then take the reference to the printHello function from the context and save it to a JSValue.

Note here that the syntax for taking a JavaScript entity (value, function, object) from JSContext and saving an entity to JSContext is similar to that of NSDictionary, which is very simple.

Finally, if JSValue is a JavaScript function, it can be called with callWithArguments, the parameter is an array, and if there is no parameter, an empty array @[] is passed in.

JavaScript calls Objective-C

Or the above example, change the content of "hello.js" to:

function printHello() {
    print("Hello, World!");
}

The print function here is implemented in Objective-C code

NSString *scriptPath = [[NSBundle mainBundle] pathForResource:@"hello" ofType:@"js"];
NSString *scriptString = [NSString stringWithContentsOfFile:scriptPath encoding:NSUTF8StringEncoding error:nil];

JSContext *context = [[JSContext alloc] init];
[context evaluateScript:scriptString];

self.context[@"print"] = ^(NSString *text) {
    NSLog(@"%@", text");
};

JSValue *function = self.context[@"printHello"];
[function callWithArguments:@[]];

Here, a Block is passed to the JavaScript context in the name of "print", and the Objective-C Block can be executed by calling the print function in JavaScript.

Note that the string in JavaScript can be seamlessly bridged to NSString, and the actual parameter "Hello, World!" is passed to the text parameter of type NSString.

exception handling

When an exception occurs when JavaScript is running, it will call back the Block set in the exceptionHandler of the JSContext

context.exceptionHandler = ^(JSContext *context, JSValue *exception) {
      NSLog(@"JS Error: %@", exception);
};

[context evaluateScript:@"function multiply(value1, value2) { return value1 * value2 "];
// 此时会打印Log "JS Error: SyntaxError: Unexpected end of script"

JSExport

JSExport is a protocol that allows properties or methods of native classes to be called JavaScript properties or methods.

See the example below:

@protocol ItemExport <JSExport>
@property (strong, nonatomic) NSString *name;
@property (strong, nonatomic) NSString *description;
@end

@interface Item : NSObject <ItemExport>
@property (strong, nonatomic) NSString *name;
@property (strong, nonatomic) NSString *description;
@end

Note that the Item class does not directly conform to JSExport, but conforms to its own protocol, which inherits the JSExport protocol.

For example, the following JavaScript code

function Item(name, description) {
    this.name = name;
    this.description = description;
}

var items = [];

function addItem(item) {
    items.push(item);
}

You can pass an Item object to the addItem function in Objective-C

Item *item = [[Item alloc] init];
item.name = @"itemName";
item.description = @"itemDescription";

JSValue *function = context[@"addItem"];
[function callWithArguments:@[item]];

Or export the Item class to the JavaScript environment and wait for it to be used later

[self.context setObject:Item.self forKeyedSubscript:@"Item"];

memory management pitfalls

The memory management mechanism of Objective-C is reference counting, and the memory management mechanism of JavaScript is garbage collection. In most cases, JavaScriptCore can seamlessly transition between these two memory management mechanisms, but there are a few cases that require special attention.

Capture JSContext inside block

Block will create a strong reference to all objects captured by it by default. A JSContext also holds a strong reference for all JSValues ​​it manages. Also, JSValue maintains a strong reference to both the value it holds and the Context it is in. In this way, JSContext and JSValue appear to be circular references, but no, the garbage collection mechanism will break this circular reference.

See the example below:

self.context[@"getVersion"] = ^{
    NSString *versionString = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"];

    versionString = [@"version " stringByAppendingString:versionString];

    JSContext *context = [JSContext currentContext]; // 这里不要用self.context
    JSValue *version = [JSValue valueWithObject:versionString inContext:context];

    return version;
};

Use [JSContext currentContext] instead of self.context to use JSContext in blocks to prevent circular references.

JSManagedValue

When saving a JavaScript value to a local instance variable, special attention needs to be paid to memory management pitfalls. Storing a JSValue in an instance variable is very easy to cause circular references.

Consider the following example, customizing a UIAlertView that calls a JavaScript function when the button is clicked:

#import <UIKit/UIKit.h>
#import <JavaScriptCore/JavaScriptCore.h>

@interface MyAlertView : UIAlertView

- (id)initWithTitle:(NSString *)title
            message:(NSString *)message
            success:(JSValue *)successHandler
            failure:(JSValue *)failureHandler
            context:(JSContext *)context;

@end

According to the implementation method of general custom AlertView, MyAlertView needs to hold two JSValue objects, successHandler and failureHandler

Inject a function into the JavaScript environment

self.context[@"presentNativeAlert"] = ^(NSString *title,
                                        NSString *message,
                                        JSValue *success,
                                        JSValue *failure) {
   JSContext *context = [JSContext currentContext];
   MyAlertView *alertView = [[MyAlertView alloc] initWithTitle:title 
                                                       message:message
                                                       success:success
                                                       failure:failure
                                                       context:context];
   [alertView show];
};

Because there are "strong references" in the JavaScript environment (relative to the concept of Objective-C), at this time, JSContext strongly references a presentNativeAlert function, which in turn strongly references MyAlertView, which means that JSContext strongly references MyAlertView, and MyAlertView strongly references MyAlertView. In order to hold the two callbacks, the two JSValues, successHandler and failureHandler, are strongly referenced, so that MyAlertView and the JavaScript environment refer to each other.

So Apple provides a JSMagagedValue class to solve this problem.

See the correct implementation of MyAlertView.m:

#import "MyAlertView.h"

@interface XorkAlertView() <UIAlertViewDelegate>
@property (strong, nonatomic) JSContext *ctxt;
@property (strong, nonatomic) JSMagagedValue *successHandler;
@property (strong, nonatomic) JSMagagedValue *failureHandler;
@end

@implementation MyAlertView

- (id)initWithTitle:(NSString *)title
            message:(NSString *)message
            success:(JSValue *)successHandler
            failure:(JSValue *)failureHandler
            context:(JSContext *)context {

    self = [super initWithTitle:title
                    message:message
                   delegate:self
          cancelButtonTitle:@"No"
          otherButtonTitles:@"Yes", nil];

    if (self) {
        _ctxt = context;

        _successHandler = [JSManagedValue managedValueWithValue:successHandler];
        // A JSManagedValue by itself is a weak reference. You convert it into a conditionally retained
        // reference, by inserting it to the JSVirtualMachine using addManagedReference:withOwner:
        [context.virtualMachine addManagedReference:_successHandler withOwner:self];

        _failureHandler = [JSManagedValue managedValueWithValue:failureHandler];
        [context.virtualMachine addManagedReference:_failureHandler withOwner:self];
    }
    return self;
}

- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
    if (buttonIndex == self.cancelButtonIndex) {
        JSValue *function = [self.failureHandler value];
        [function callWithArguments:@[]];
    } else {
        JSValue *function = [self.successHandler value];
        [function callWithArguments:@[]];
    }

    [self.ctxt.virtualMachine removeManagedReference:_failureHandler withOwner:self];
    [self.ctxt.virtualMachine removeManagedReference:_successHandler withOwner:self];
}    

@end

Analyzing the above example, the JSValue object passed in from the outside is saved using JSManagedValue inside the class.

JSManagedValue itself is a weak reference object, you need to call JSVirtualMachine to addManagedReference:withOwner:add it to the JSVirtualMachine object to ensure that JSValue will not be released during use

When the user clicks the button on the AlertView, the corresponding processing function is executed according to which button the user clicks, and the AlertView is also destroyed immediately. In this case, a manual call removeManagedReference:withOwner:is required to remove JSManagedValue.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325810853&siteId=291194637