Summary: Dynamic hot fixes are serious bugs that can crash your app and cause it to be too slow to submit a new version to the App Store for review. In this case, you can use JSPatch to write native iOS apps in JavaScript.

Dynamic hot fixes are serious bugs that can cause an app to crash, and slow submission of a new version to the App Store for approval

JSPatch lets you write native iOS apps in JavaScript. Simply by introducing a very small engine into your project, you can use JavaScript to call any native interface of Objective-C and gain the advantages of the scripting language: dynamically adding modules to your project, or dynamically fixing bugs by replacing your project’s native code.

Here is not to repeat the advantages and disadvantages focus on the implementation!

Daily updates:

www.jianshu.com

(a) First at the terminal

pod search JSPatch
Copy the code

Then you can pod yourself into the project you need to introduce, or download to the local and add yourself to the project search results below

-> JSPatch (0.1.4) JSPatch bridge Objective-C and JavaScript. You can call any Objective C class and method in JavaScript by just including a small engine. Pod 'JSPatch', '~> 0.1.4' -homepage: https://github.com/bang590/JSPatch - Source: https://github.com/bang590/JSPatch.git - Versions: 0.1.4 0.1.3, 0.1.2,, while 0.1, 0.0.3, hundreds of 0.0.1 [master repo] - 0.1.4 0.1.3, 0.1.2,, while 0.1, 0.0.3, hundreds, 0.0.1 [master-1 repo] -subspecs: -Jspatch /Core (0.1.4) -Jspatch /Extensions (0.1.4) macxy:~ xy$Copy the code

Normally we will execute the “.js” file on the server side and every time the program starts, we can perform a comparison between the local JS file and the server-side JS file to see if any changes have been made and I won’t go into the details here

- (BOOL) application: (UIApplication *) application didFinishLaunchingWithOptions: (launchOptions NSDictionary *) {/ / load the engine [JPEngine startEngine]; NSString *sourcePath = [[NSBundle mainBundle] pathForResource:@"demo" ofType:@" JS "]; NSString *script = [NSString stringWithContentsOfFile:sourcePath encoding:NSUTF8StringEncoding error:nil]; [JPEngine evaluateScript:script]; Get update JS [NSURLConnection sendAsynchronousRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://cnbang.net/test.js"]] queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) { NSString *script = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; // Execute js [JPEngine evaluateScript:script]; }Copy the code

This is the general process to describe the application of js substitution;

How to access the OC API when oc interacts with JS

/ / button event defineClass (' YFJSPatchMainViewController, {/ / in which class method buttonTouch: Function (button) { Function (parameters). / / jump to the tableView var tableViewCtrl = YFJSPatchTsetViewController alloc (). The init () self.navigationController().pushViewController_animated(tableViewCtrl, YES) } })Copy the code

I’m sure you all understand that so how do we use it like what I’ve written up here is to find it

YFJSPatchMainViewController in this classCopy the code

- (void) buttonTouch (UIbutton) BTN {} this method starts with nothing written. After executing the js file, this method will execute js and push; The principle is to want oneself to use ability so good wish you happyCopy the code

1. require

Before using an Objective-C class, require(‘className ‘) needs to be called:

require('UIView')
var view = UIView.alloc().init()
Copy the code

You can import multiple classes at once, separated by commas:

require('UIView, UIColor')
var view = UIView.alloc().init()
var red = UIColor.redColor()
Copy the code

Or call require() when it’s in use:

require('UIView').alloc().init()
Copy the code

2. Call the OC method

Calling a class method

var redColor = UIColor.redColor();
Copy the code

Call instance method

var view = UIView.alloc().init();
view.setNeedsLayout();
Copy the code

Parameter passing

Pass parameters as in OC:

var view = UIView.alloc().init();
var superView = UIView.alloc().init()
superView.addSubview(view)
Copy the code

Property

Getting/modifying a Property is equal to calling the getter/setter method of the Property.

view.setBackgroundColor(redColor);
var bgColor = view.backgroundColor();
Copy the code

Method name conversion

Multi-parameter method names are separated by _ :

var indexPath = require('NSIndexPath').indexPathForRow_inSection(0, 1);
Copy the code

Select * from ‘OC’ where ‘OC’ contains’ _ ‘; select * from ‘OC’ where ‘OC’ contains’ _ ‘;

// Obj-C: [JPObject _privateMethod];
JPObject.__privateMethod()
Copy the code

3. defineClass

API

defineClass(classDeclaration, [properties,] instanceMethods, classMethods)

@param classDeclaration: string, class name/parent class name and Protocol @param Properties: New property, string array, can be omitted @param instanceMethods: @param classMethods: classMethods to be added or overridden

Override method

1. Define an existing OC method in defineClass to override it. The method name rule is the same as the call rule, separated by _ :

// OC
@implementation JPTestObject
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
}
@end
Copy the code

// JS defineClass("JPTableViewController", { tableView_didSelectRowAtIndexPath: function(tableView, indexPath) { ... }})Copy the code

2. Use the double underscore __ to represent the underscore _ in the original OC method name:

// OC
@implementation JPTableViewController
- (NSArray *) _dataSource {
}
@end
Copy the code

// JS
defineClass("JPTableViewController", {
  __dataSource: function() {
  },
})
Copy the code

Select * from OC where ORIG precedes method name;

// OC
@implementation JPTableViewController
- (void)viewDidLoad {
}
@end
Copy the code

// JS defineClass("JPTableViewController", { viewDidLoad: function() { self.ORIGviewDidLoad(); }})Copy the code

Override class method

The third argument of defineClass() is the class method to be added or overridden, and the rules are the same as above for overridden instance methods:

// OC
@implementation JPTestObject
+ (void)shareInstance
{
}
@end
Copy the code

// JS defineClass("JPTableViewController", {// instance method}, {// class method shareInstance: function() {... }})Copy the code

Override Category method

Overriding a Category method is the same as overriding a normal method:

@implementation UIView (custom)
- (void)methodA {
}
+ (void)clsMethodB {
}
@end
Copy the code

defineClass('UIView', {
  methodA: function() {
  }
}, {
  clsMethodB: function() {
  }
});
Copy the code

Super

Using the self.super() interface to represent the super keyword, call the super method:

// JS defineClass("JPTableViewController", { viewDidLoad: function() { self.super().viewDidLoad(); }})Copy the code

Property

Obtains/modifies the Property defined by the OC

Get/modify a Property defined in OC by calling a getter/setter:

// OC
@interface JPTableViewController
@property (nonatomic) NSArray *data;
@property (nonatomic) NSString *shareURL;
@property (nonatomic) NSString *shareTitle;
@end
@implementation JPTableViewController
@end
Copy the code

// JS
defineClass("JPTableViewController", {
  viewDidLoad: function() {
     var data = self.data();     //get property value
     self.setData(data.toJS().push("JSPatch"));     //set property value
     var sel = self;
     self.bridge().registerHandler_handler('h5ToNativeShareDialog', block('NSDictionary *',function(data,responseCallback) {         
         sel.setShareURL(data.objectForKey('url'));
         sel.setShareTitle(data.objectForKey('title'));
     }));
})
Copy the code

Dynamic New Property

We can add a property to the class in the second argument of defineClass(), in the format of an array of strings, as used in the OC Property interface:

defineClass("JPTableViewController", ['data', 'totalCount'], { init: function() { self = self.super().init() self.setData(["a", "B "]) // Add new Property (id data) self.setTotalCount(2) return self}, viewDidLoad: Function () {var data = self.data(); var totalCount = self.data();Copy the code

Private member variables

Use valueForKey() and setValue_forKey() to get/modify private member variables:

// OC
@implementation JPTableViewController {
     NSArray *_data;
}
@end
Copy the code

// JS
defineClass("JPTableViewController", {
  viewDidLoad: function() {
     var data = self.valueForKey("_data")     //get member variables
     self.setValue_forKey(["JSPatch"], "_data")     //set member variables
  },
})
Copy the code

Adding new methods

We can optionally add methods to a class that are undefined by OC, but all arguments are of type ID:

// OC
@implementation JPTableViewController
- (void)viewDidLoad
{
     NSString* data = [self dataAtIndex:@(1)];
     NSLog(@"%@", data);      //output: Patch
}
@end
Copy the code

// JS
var data = ["JS", "Patch"]
defineClass("JPTableViewController", {
  dataAtIndex: function(idx) {
     return idx < data.length ? data[idx]: ""
  }
})
Copy the code

If the new method belongs to the interface of the Protocol, specify the implemented Protocol in the class declaration parameter of defineClass, as described below.

Protocol

It is possible to define a class that implements certain Protocol interfaces, written as OC:

defineClass("JPViewController: UIViewController<UIScrollViewDelegate, UITextViewDelegate>", {

})
Copy the code

When you add a method defined in Protocol and the class does not implement a method, the parameter type is automatically changed to the type defined in Protocol instead of id:

@protocol UIAlertViewDelegate <NSObject> ... - (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex; . @endCopy the code

defineClass("JPViewController: UIViewController <UIAlertViewDelegate>", {
  viewDidAppear: function(animated) {
    var alertView = require('UIAlertView')
      .alloc()
      .initWithTitle_message_delegate_cancelButtonTitle_otherButtonTitles(
        "Alert",
        self.dataSource().objectAtIndex(indexPath.row()), 
        self, 
        "OK", 
        null
      )
     alertView.show()
  }
  alertView_clickedButtonAtIndex: function(alertView, buttonIndex) {
    console.log('clicked index ' + buttonIndex)
  }
})
Copy the code

Special types

Struct

JSPatch supports CGRect/CGPoint/CGSize/NSRange struct types, which are represented by JS objects:

// Obj-C UIView *view = [[UIView alloc] initWithFrame:CGRectMake(20, 20, 100, 100)]; [view setCenter: CGPointMake (10, 10)]; [view sizeThatFits:CGSizeMake(100, 100)]; CGFloat x = view.frame.origin.x; NSRange range = NSMakeRange(0, 1);Copy the code

// JS
var view = UIView.alloc().initWithFrame({x:20, y:20, width:100, height:100})
view.setCenter({x: 10, y: 10})
view.sizeThatFits({width: 100, height:100})

var x = view.frame().x
var range = {location: 0, length: 1}
Copy the code

For details about other Struct types, see Adding Struct Type Support

Selector

Using strings to represent selectors in JS:

//Obj-C
[self performSelector:@selector(viewWillAppear:) withObject:@(YES)];
Copy the code

//JS
self.performSelector_withObject("viewWillAppear:", 1)
Copy the code

nil

Null and undefined in JS represent OC nil. If NSNull is null, use NSNull instead. If null is null, use null.

//Obj-C
@implemention JPTestObject
+ (BOOL)testNull(NSNull *null) {
    return [null isKindOfClass:[NSNull class]]
}
@end
Copy the code

//JS
require('JPTestObject').testNull(nsnull) //return 1
require('JPTestObject').testNull(null) //return 0
Copy the code

False = null; false = null;

var url = ""; var rawData = NSData.dataWithContentsOfURL(NSURL.URLWithString(url)); if (rawData ! = null) {} The _formatOCToJS method converts undefined,null,isNil to false in the jspatch.js source code.Copy the code

5. NSArray / NSString / NSDictionary

NSArray/NSString/NSDictionary does not automatically convert to the corresponding JS type, use them just like normal NSObject:

//Obj-C
@implementation JPObject
+ (NSArray *)data
{
  return @[[NSMutableString stringWithString:@"JS"]]
}
+ (NSMutableDictionary *)dict
{
    return [[NSMutableDictionary alloc] init];
}
@end
Copy the code

// JS
require('JPObject')
var ocStr = JPObject.data().objectAtIndex(0)
ocStr.appendString("Patch")

var dict = JPObject.dict()
dict.setObject_forKey(ocStr, 'name')
console.log(dict.objectForKey('name')) //output: JSPatch
Copy the code

To convert NSArray/NSString/NSDictionary to the corresponding JS type, use the.tojs () interface:

// JS
var data = require('JPObject').data().toJS()
//data instanceof Array === true
data.push("Patch")

var dict = JPObject.dict()
dict.setObject_forKey(data.join(''), 'name')
dict = dict.toJS()
console.log(dict['name'])    //output: JSPatch
Copy the code

6. Block

Block transfer

Block (paramTypes, function); / / use block(paramTypes, function); / / use block(paramTypes, function);

// Obj-C
@implementation JPObject
+ (void)request:(void(^)(NSString *content, BOOL success))callback
{
  callback(@"I'm content", YES);
}
@end
Copy the code

// JS
require('JPObject').request(block("NSString *, BOOL", function(ctn, succ) {
  if (succ) log(ctn)  //output: I'm content
}))
Copy the code

The types of arguments in the block are strings. Write the types of the arguments in the block, separated by commas. An NSObject like AN NSString star, an NSArray star and so on can be represented by an ID, but a block object should be represented by an NSBlock star.

}}}}}}}}}}}}}}}}}}}}}}}}}}}}}

// Obj-C
@implementation JPObject
typedef void (^JSBlock)(NSDictionary *dict);
+ (JSBlock)genBlock
{
  NSString *ctn = @"JSPatch";
  JSBlock block = ^(NSDictionary *dict) {
    NSLog(@"I'm %@, version: %@", ctn, dict[@"v"])
  };
  return block;
}
+ (void)execBlock:(JSBlock)blk
{
}
@end
Copy the code

// JS var blk = require('JPObject').genBlock(); Series ({v: "0.0.1}"); // Output: I'm JSPatch, version: 0.0.1Copy the code

To return this block from OC to OC, we need to wrap it with block() again, because BLK is already a normal JS function, just like the JS function we defined above:

// JS var blk = require('JPObject').genBlock(); Series ({v: "0.0.1}"); Output: I'm JSPatch, version: 0.0.1 require('JPObject'). ExecBlock (block("id", BLK)); //output: I'm JSPatch, version: 0.0.1 require('JPObject').Copy the code

If you pass a block object from JS to OC, it will become a JS function. If you pass a block object from JS to OC, you need to wrap it with the block() interface.

Use the self variable in the block

You can’t use the self variable inside a block. You need to use a temporary variable to hold it before entering the block:

defineClass("JPViewController", { viewDidLoad: function() { var slf = self; require("JPTestObject").callBlock(block(function(){ //`self` is not available here, use `slf` instead. slf.doSomething(); }); }}Copy the code

limit

When passing a block from JS to OC, there are two restrictions:

A. A maximum of six block parameters are supported. B. The block parameter type cannot be double/NSBlock/struct.

There is also no support for JS wrapped blocks being passed to OC and then passed back to JS to be called (see issue #155 for this reason) :

- (void)callBlock:(void(^)(NSString *str))block {
}
Copy the code

defineClass('JPTestObject', { run: function() { self.callBlock(block('NSString*', function(str) { console.log(str); })); }, callBlock: function(BLK) {// this block is passed from JS to OC and cannot be called. blk("test block"); }});Copy the code

7. __weak / __strong

You can declare a weak variable in JS with __weak(), mainly to avoid circular references.

For example, to avoid circular references caused by blocks in OC, we often write:

- (void)test {
    __weak id weakSelf = self;
    [self setCompleteBlock:^(){
        [weakSelf blabla];
    }]
}
Copy the code

In JS we can write:

var weakSelf = __weak(self)
self.setCompleteBlock(block(function(){
    weakSelf.blabla();
}))
Copy the code

To change weakSelf into a strong variable when using it, use the __strong() interface:

var weakSelf = __weak(self)
self.setCompleteBlock(block(function(){
    var strongSelf = __strong(weakSelf)
    strongSelf.blabla();
}))
Copy the code

8. GCD

Dispatch_async_main () dispatch_sync_main() dispatch_async_global_queue()

// Obj -c dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), ^{ // do something }); dispatch_async(dispatch_get_main_queue(), ^{ // do something });Copy the code

/ / JS dispatch_after (1.0, function(){ // do something }) dispatch_async_main(function(){ // do something }) dispatch_sync_main(function(){ // do something }) dispatch_async_global_queue(function(){ // do something })Copy the code

9. Pass the id* parameter

If you need to pass an ID * argument like NSError ** in this interface in NSURLConnection:

+ (NSData *)sendSynchronousRequest:(NSURLRequest *)request returningResponse:(NSURLResponse **)response error:(NSError **)error;
Copy the code

In this case, we pass a pointer to an NSObject. We can change the object to which the pointer points in the method, and we can call the object to which the new object points. For this argument, we first need to introduce the JPMemory extension, and then pass and retrieve it as follows:

  1. Use malloc(sizeof(id)) to create a pointer

  2. Pass the pointer as an argument to the method

  3. Method call, using pval() to get the pointer to the new object

  4. Call releaseTmpObj() to release the object

  5. Use free() to free the pointer

Here’s an example:

//OC
- (void)testPointer:(NSError **)error {
    NSError *err = [[NSError alloc]initWithDomain:@"com.jspatch" code:42 userInfo:nil];
    *error = err;
}
Copy the code

//JS //malloc() pval() free() is provided by JPMemory extension require('JPEngine').addExtensions(['JPMemory']) var pError = malloc(sizeof("id")) self.testPointer(pError) var error = pval(pError) if (! error) { console.log("success") } else { console.log(error) } releaseTmpObj(pError) free(pError)Copy the code

If, on the other hand, you wanted to replace the -testpointer: method above in JS and build an NSError object to assign to the pointer passed in, you could write:

defineClass('JPClassName', {
    testPointer: function(error){
        var  tmp = require('NSError').errorWithDomain_code_userInfo("test", 1, null);
        var newErrorPointer = getPointer(tmp)
        memcpy(error, newErrorPointer, sizeof('id'))
    }
);
Copy the code

10. Constants, enumerations, macros, global variables

Constants/enumerations

Constants/enumerations in Objective-C cannot be used directly in JS, but can be used directly in JS with concrete values:

//OC
[btn addTarget:self action:@selector(handleBtn) forControlEvents:UIControlEventTouchUpInside];
Copy the code

/ / UIControlEventTouchUpInside values is 1 < < 6 BTN. AddTarget_action_forControlEvents (self, "handleBtn", 1 < < 6);Copy the code

Or redefine a global variable with the same name in JS:

//js
var UIControlEventTouchUpInside  = 1 << 6;
btn.addTarget_action_forControlEvents(self, "handleBtn", UIControlEventTouchUpInside);
Copy the code

There are some constant strings that need to be typed in OC using NSLog to see what its value is:

//OC
[[NSAttributedString alloc].initWithString:@"str" attributes:@{NSForegroundColorAttributeName: [UIColor redColor]];
Copy the code

NSForegroundColorAttributeName in the code above is a static string constants, don’t see the value in the source code, can use first NSLog hit its value to write directly on the JS:

//OC
NSLog(@"%@", NSForegroundColorAttributeName) //output 'NSColor'
Copy the code

Nsattributedstring.alloc ().initWithstring_attributes (" invalid ", {'NSColor': uicolor.redcolor ()});Copy the code

The macro

For macro value

Macros in Objective-C also cannot be used directly in JS. If the macro is a value, you can define the same global variable in JS instead. If the macro is a program, you can expand the macro in JS instead:

#define TABBAR_HEIGHT 40
#define SCREEN_WIDTH [[UIScreen mainScreen] bounds].size.height
[view setWidth:SCREEN_WIDTH height:TABBAR_HEIGHT];
Copy the code

//JS
view.setWidth_height(UIScreen.mainScreen().bounds().height, 40);
Copy the code

If the value of a macro is some underlying value, such as CGFLOAT_MIN, it can be supported by returning it in a class or instance method or by adding an extension:

@implementation JPMacroSupport
+ (void)main:(JSContext *)context
{
  context[@"CGFLOAT_MIN"] = ^CGFloat() {
    return CGFLOAT_MIN;
  }
}
@end
Copy the code

require('JPEngine').addExtensions(['JPMacroSupport'])
var floatMin = CGFLOAT_MIN();
Copy the code

Modify the macro value

JSPatch does not support changing the value of a macro. To do so, you need to replace all methods that use the macro. Such as:

#define VIEW_HEIGHT 40 @implementation JPMethodDemo + (void)func { UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, VIEW_HEIGHT)]; . } @endCopy the code

//JS var VIEW_HEIGHT_NEW = 20; defineClass('JPMethodDemo', { func: function() { var view = UIView.alloc().initWithFrame({x:0, y:0, width:100, height:VIEW_HEIGHT_NEW}); . }});Copy the code

The global variable

A static global variable defined in a class cannot be retrieved from JS. To retrieve this variable in JS, you need to have a class method or instance method return it in OC:

static NSString *name;
@implementation JPTestObject
+ (NSString *)name {
  return name;
}
@end
Copy the code

Var name = jpTestObject.name (Copy the code

11. Swift

When you overwrite the Swift class with defineClass(), the class name should be the project name. If you want to override the ViewController method in JS, you need to write:

defineClass('demo.ViewController', {})
Copy the code

The same is true for invoking classes already defined in SWIFT:

require('demo.ViewController')
Copy the code

Some points to note:

  1. Only calls to Swift classes that inherit from NSObject are supported

  2. The Swift class inherits from NSObject, and its methods and properties that inherit from its parent class can be called in JS. Other custom methods and properties also need the dynamic keyword.

  3. If the parameter/attribute type of the method is Swift specific (such as Character/Tuple), the method and attribute cannot be called through JS.

  4. The Swift project has added a new class to JSPatch, just like OC, which can be used normally.

12. Load the dynamic library

For the built-in dynamic library of iOS, if the original APP has not been loaded, you can use the following methods to load the dynamic library. Take SafariServices. Framework as an example:

var bundle = NSBundle.bundleWithPath("/System/Library/Frameworks/SafariServices.framework");
bundle.load();
Copy the code

Once loaded, you can use the SafariServices. Framework.

13. Debugging

You can use console.log() to print an object, which acts like NSLog() and will be typed directly into the XCode console.

Console.log () supports any arguments, but does not support concatenation of NSLog(@”num:%f”, 1.0) like NSLog:

var view = UIView.alloc().init(); var str = "test"; var num = 1; console.log(view, str, num) console.log(str + num); // Concatenate the string directly in JSCopy the code

You can also debug JS breakpoints using Safari’s debugging tools.

Recommend a 👇 :

If you want to advance together and keep up with the latest interview news, you can add networking group 642363427