Skip to content
easingboy edited this page Sep 12, 2016 · 6 revisions

JSPatch is a hot fix framework on iOS platform. You can use JavaScript to call any native Objective-C method just by importing a tiny engine. Now, you can enjoy the advantages of scripting language, such as integrating modules into project dynamically, as well as replacing original code to fix bugs.

In this article, I will show you the implementation details of JSPatch to help you understand and use JSPatch more easily.

Outline

Basic Theory
Method invocation
    1. require
    2. JS Interface
        i. Encapsulation of JS Object
        ii. `__c()` Metafunction
    3. message forwarding
    4. object retaining and converting
    5. type converting

Method replacement
    1. Basic theory
    2. Use of va_list(32-bit)
    3. Use of ForwardInvocation(64-bit)
    4. Add new methods
        i. Plan
        ii. Protocol
    5. Implementation of property
    6. Keyword: self
    7. Keyword: super

Extension
    1. Support Struct
    2. Support C function
    
Detail
    1. Special Struct
    2. Memory Issue
        i.  Double Release
        ii. Memery Leak
    3. About '_'
    4. JPBoxing
    5. About nil
        i.  Distinguish NSNull/nil
        ii. Chained invocation
        
Summary

Basic Theory

The fundamental cause of calling and changing Objective-C method with JavaScript code from the fact that Obejctive-C is a dynamic language. All invocations of methods and generation of classes are handled at runtime. So, we can use reflection to get corresponding classes and methodes from their names:

Class class = NSClassFromString("UIViewController");
id viewController = [[class alloc] init];
SEL selector = NSSelectorFromString("viewDidLoad");
[viewController performSelector:selector];

We can also replace the implementation of a method with a new one:

static void newViewDidLoad(id slf, SEL sel) {}
class_replaceMethod(class, selector, newViewDidLoad, @"");

We can even create a new class and add some methods for it:

Class cls = objc_allocateClassPair(superCls, "JPObject", 0);
objc_registerClassPair(cls);
class_addMethod(cls, selector, implement, typedesc);

There are a lot of execellent blogs talking about object model and dynamic message sending in Objective-C, so I won't explain them here. Theoretically, you can call any method at runtime with the class name and method name. You can also replace the implementation of any class and add new classes. In a word, the basic of JSPatch is the transmission of string from JavaScript to Objective-C. Then the Objective-C side can use runtime to call and replace methods.

This is the basic theory, however, to put it into practice, we still have to solve a lot of problems. Now, let's take a look at each of them.

Method Invocation

require('UIView')
var view = UIView.alloc().init()
view.setBackgroundColor(require('UIColor').grayColor())
view.setAlpha(0.5)

With JSPatch, you can create an instance of UIView with JavaScript code, you can also set the background color and the value of alpha.

The code above covers the following five topics:

  1. using 'require' keyword to import a class
  2. using JavaScript to call Objective-C method
  3. message passing
  4. object retaining and converting
  5. type converting

Now, let's talk about them one by one.

1. require

With require('UIview'), you can call class methods of UIView now. What the require keyword does is very simple, it just creates a global variable with the same name. The variable is an object, whose __isCls is set to 1, which means this is a class, and __claName is the name of this class. These two properties will be used during method invocation.

var _require = function(clsName) {
  if (!global[clsName]) {
    global[clsName] = {
      __isCls: 1,
      __clsName: clsName
    }
  }
  return global[clsName]
}

Therefore, when you call require('UIview'), what you are actually doing is creating a global object looks like this:

{
  __isCls: 1,
  __clsName: "UIView"
}

2. JS Bridge

Now, let's take a look at how UIView.alloc() is called.

i Encapsulation of JS Object

At the very beginning, in order to comply with JavaScript syntax, I tried to add a method called alloc to the object UIView, otherwise, you will get an exception when calling the method. This is the difference from Objective-C runtime since you won't have any chance to pass the method invocation. Based on the analysis above, on calling require method, I passed the class name to Objective-C, gave all methods of this class back to JavaScript and then create corresponding JavaScript methods. In these JavaScript methods, I used the method name to call corresponding Obejective-C method.

So the UIView object now looks like this:

{
    __isCls: 1,
    __clsName: "UIView",
    alloc: function() {},
    beginAnimations_context: function() {},
    setAnimationsEnabled: function(){},
    ...
}

In fact, I have to get methods from not only the current class itself, but also its superclass. All methods in the inheritance chain will be added into JavaScript. However, I got a serious problem about memory usage because a class may have several hundred methods. To reduce memory usage, I made some optimization such as using inheritance chain in JavaScript to avoid adding methods of superclass repeatedly. However, there is still too much memory consumption.

ii. __c() Metafunction

This is the solution which complies with JavaScript syntax. But it doesn't mean that I have to comply with JavsScript syntax strictly. Think about CoffieScript and JSX, they have a parser to translate them into stand JavaScript. This technology is absolutely feasible in my case, and I only need to call a particular method (MetaFunction) when an unknown method is called. Therefore, as the final solution, before evaluating JavaScript in Objective-C, I translate all the method invocation to __c() function with the help of RegEx and then evaluate the translated script. This looks like the message forwarding in OC/Lua/Ruby.

UIView.alloc().init()
->
UIView.__c('alloc')().__c('init')()

I add a __c property to the prototype of base Object so that any object can access it:

Object.prototype.__c = function(methodName) {
  if (!this.__obj && !this.__clsName) return this[methodName].bind(this);
  var self = this
  return function(){
    var args = Array.prototype.slice.call(arguments)
    return _methodFunc(self.__obj, self.__clsName, methodName, args, self.__isSuper)
  }
}

In method _methodFunc, the relative information will be passed to Objective-C, call corresponding method at runtime and give back the result of this function.

In this way, I don't need to iterate over every method of a class and save them in JavaScript object. With this improvement, I reduced 99% memory usage.

Message forwarding

Now, we are going to talk about the communication between JavaScript and Objective-C. In fact, I used an interface declared in JavaScriptCore. On starting JSPatch engine, we will create an instance of JSContext which is used to execute JavaScript code. We can register an Obejctive-C method to JSContext and call it in JavaScript later:

JSContext *context = [[JSContext alloc] init];
context[@"hello"] = ^(NSString *msg) {
    NSLog(@"hello %@", msg);
};
[_context evaluateScript:@"hello('word')"];     //output hello word

JavaScript talks to Objective-C with methods registered in JSContext and get information of Objective-C from the result of method call. In this manner, JavaScriptCore will convert the type of parameters and results automatically. This means NSArray, NSDictionary, NSString, NSNumber, NSBlock will be converted to array/object/string/number/function. This is how _methodFunc method passes class name and method name to Objective-C.

4. Object retaining and converting

Now, you may have known how UIView.alloc() is executed:

  • When you call require('UIView'), you will create a global object called UIView
  • When you call alloc() method of the object UIView, what you are actually calling is __c() method, in which class name and method will be passed to Objective-C to complete the method invocation.

This is the detailed process of invocation of instance method and what about class method? We will receive an instance of UIView after calling UIView.alloc() but how can we represent this instance in JavaScript? How can we call its instance method UIView.alloc().init()?

For an object of type id, JavaScriptCore will pass its pointer to JS. Although it can't be used in JS, it can be given back to Obejctivce-C later. As for the lifecycle of this object, in my opinion, its reference count will increase by 1 when a variable is retained in JavaScript and decrease by 1 when released in JavaScript. If there is no Objective-C object refers to it, its lifecycle depends on JavaScript context and will be released when garbage collection takes place.

As we mentioned before, object passed to JS can be given back to OC when calling __call() method. If you want to call a method in a Objective-C object, you can use the object pointer and method name as parameters of __call() method. Now, there is only one question left: How can we know whether the caller is an Objective-C pointer or not.

I have no idea about this and my solution is wrapping the object into a dictionary before passing it to JS:

static NSDictionary *_wrapObj(id obj) {
    return @{@"__obj": obj};
}

The object now is a value inside the dictionary, it is represented as below in JS:

{__obj: [OC Object Pointer]}

In this way, you can know whether an object is Objective-C object easily by checking its __obj property. In __c() method, if this property is not undefined, we can access it and pass to OC, this is how we call instance method.

5. Type converting

After sending class name, method name and method caller to Objective-C, we will use NSInvocation to call corresponding OC method. In this process, there are two thinks to do:

  • Get types of parameters of the OC method you want to call and convert the JS value.
  • Get the result of method invocation, wrap it into an object and send back to JS

For example, think about the code above view.setAlpha. The parameter on JS side is of type NSNumber, however, with calling OC method NSMethodSignature, we know the parameter should be a float. So we should call OC method after converting NSNumber to float. In JSPatch, I mainly handle the convertion of number type such as int/float/bool. Besides, I took care of some special type like CGRect and CGRange.

Method replacement

In JSPatch, you can use defineClass to replace any method of any class. To support this, I made a lot of effort. At the beginning, I used va_list to get parameters, which turned out to be infeasible in arm64. It also took some time to add new methods to a class, implement property and add support to self/super keyword. Here I will introduce them one by one.

1. Basic theory

In OC, every class is actually a struct below:

struct objc_class {
  struct objc_class * isa;
  const char *name;
  ….
  struct objc_method_list **methodLists;
};

The type of elements in methodLists is a method:

typedef struct objc_method *Method;
typedef struct objc_ method {
  SEL method_name;
  char *method_types;
  IMP method_imp;
};

A method object contains all information of a method, including the name of SEL, types of parameters and returning value, the IMP pointer to its real implementation.

When calling a method via its selector, you actually look for a method in the methodList. It is a linked list whose elements can be replaced dynamically. You can replace the function pointer(IMP) of a selector with a new IMP, you can link one IMP with another selector as well. OC runtime provides some APIs to do this, as an example, let's replace viewDidLoad of UIViewController:

static void viewDidLoadIMP (id slf, SEL sel) {
   JSValue *jsFunction = …;
   [jsFunction callWithArguments:nil];
}

Class cls = NSClassFromString(@"UIViewController");
SEL selector = @selector(viewDidLoad);
Method method = class_getInstanceMethod(cls, selector);

//get function pointer to viewDidLoad
IMP imp = method_getImplementation(method)

// get type of parameters of viewDidLoad
char *typeDescription = (char *)method_getTypeEncoding(method);

// add a new method called ORIGViewDidLoad and points to the original viewDidLoad
class_addMethod(cls, @selector(ORIGViewDidLoad), imp, typeDescription);

//viewDidLoad IMP now points to the new IMP
class_replaceMethod(cls, selector, viewDidLoadIMP, typeDescription);

With the code above, we can replace viewDidLoad with a new customized method. Now, if you call viewDidLoad in your app, you will call viewDidLoadIMP, in which you will call a method from JS. This is how we can call method which is written in JS code. Meanwhile, we add a new method called ORIGViewDidLoad which points to the original viewDidLoad, it can be called in JS.

If a method doesn't have parameter, this is all we need to do to replace a method. However, what if a method has parameters, how can we pass the value of parameter to the new IMP? For example, to call viewDidAppear of UIViewController, the caller will specify a BOOL value and we have to get this value in our customized IMP. If we only need to write a new IMP for a single method, it's quite easy:

static void viewDidAppear (id slf, SEL sel, BOOL animated) {
   [function callWithArguments:@(animated)];
}

However, we want a general IMP which can be used as an interchange station for any method with any parameters. In this IMP, we need to get all parameters and pass to JS.

2. Use of va_list(32-bit)

At the beginning, I use a mutable type va_list:

static void commonIMP(id slf, ...)
  va_list args;
  va_start(args, slf);
  NSMutableArray *list = [[NSMutableArray alloc] init];
  NSMethodSignature *methodSignature = [cls instanceMethodSignatureForSelector:selector];
  NSUInteger numberOfArguments = methodSignature.numberOfArguments;
  id obj;
  for (NSUInteger i = 2; i < numberOfArguments; i++) {
      const char *argumentType = [methodSignature getArgumentTypeAtIndex:i];
      switch(argumentType[0]) {
          case 'i':
              obj = @(va_arg(args, int));
              break;
          case 'B':
              obj = @(va_arg(args, BOOL));
              break;
          case 'f':
          case 'd':
              obj = @(va_arg(args, double));
              break;
          …… //Other types
          default: {
              obj = va_arg(args, id);
              break;
          }
      }
      [list addObject:obj];
  }
  va_end(args);
  [function callWithArguments:list];
}

In this case, whatever the number and type of parameters are, I can use methods of va_list to get them and put into a NSArray object, which will be passed to JS. It works very well until I run these code and get crashes in my arm64 device. After looking up for some information, it turns out that the architecture of va_list will change in arm64 so that I can't get parameters like this. For more details, please look at this article

3. Use of ForwardInvocatoin(64-bit)

Finally I played a trick and solve this problem. I used the message forward in OC.

When calling a non-exist method, you won't get an exception immediately, but get several chances instead. These methods will be called in order: resolveInstanceMethod, forwardingTargetForSelector, methodSignatureForSelector, forwardInvocation. In the last method forwardInvocation, you will create a NSInnovation object which contains all information about the method call, including the name of selector, parameters and return value. The most important thing is that you can get the value of parameters in NSInvocation. The problem can be solved if we can call forwardInvocation when a method is replaced in JS.

As an example, let's try to replace viewWillAppear of UIViewController to show the details:

  1. Use class_replaceMethod to point viewWillAppear to _objc_msgForward. This is a global IMP which will be called when a non-exist method is called. With this replacement, you will actually call forwardInvocation when you call viewWillAppear.
  2. Add two methods ORIGviewWillAppear and _JPviewWillAppear for UIViewController. The first one is the original implementation and the last one is the new implementation in which we will execute JS code.
  3. Replace forwardInvocation of UIViewController with our customized implementation. When the viewWillAppear method is called, forwardInvocation will be called and we can get a NSInvocatoin object which contains the value of parameters. Then you can call the new method JPviewWillAppear with these parameters and call the implementation in JS.

The whole process is illustrated as the flow-chart below:

There is one problem left, as we replaced the -forwardInvocation: of UIViewController, what if a method really needs it? First of all, before replacing -forwardInvocation:, we will create a new method called -ORIGforwardInvocation: to save the original IMP. There will be a judgement in the new -forwardInvocation:: begin method forward if the method is asked to be replaced, otherwise call -ORIGforwardInvocation: and work normally.

4. Add new methods

i. Plan

When JSPatch becomes open-source, you can't add methods to a class because I think the ability of replacing existing method is all what we need. You can add new methods to JS object and run in JS context. Also, the types of parameters and returning value should be figured out if we want to add new methods to an OC class, because these information is necessary in JS. This is a troublesome but wildly-concerned problem since we can't use target-action pattern without adding new method and I started to find a good way to add methods. Finally, my solution is that all value is of type id because the methods added are used only in JS(except Protocol) and we won't need to worry about the type if all values are of type id.

Now, defineClass is wrapped in JS, it will create an array with number of parameters and methods themselves, then pass this array to OC. If the method exists in OC, it will be replaced, otherwise class_addMethod() is called to add this method. We can create a new Method object with knowing the number of parameters and set the type to id. If the new method is called in JS, you will finally call forwardInvocation.

ii. Protocol

If a class conforms to some protocol which has an optional method and not all the types of parameters are id, for example, the method below in UITableViewDataSource:

- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index;

If this method is not declared in OC but added in JS, all the parameters are of type id, which doesn't match the declaration in protocol and rusults an error.

In this case, you have to specify the protocol that are implementing in JS, so that types of parameters can be known based on the protocol. The syntax looks like OC:

defineClass("JPViewController: UIViewController <UIAlertViewDelegate>", {
  alertView_clickedButtonAtIndex: function(alertView, buttonIndex) {
    console.log('clicked index ' + buttonIndex)
  }
})

It's easy to parse this. First of all, we can get the protocol name then call objc_getProtocol and protocol_copyMethodDescriptionList to get the method from protocol if the method doesn't exist in the class. Otherwise, we can replace the original method.

5. Implementation of property

To use a property defined in OC, just like calling normal methods in OC, you can call its set/get method:

//OC
@property (nonatomic) NSString *data;
@property (nonatomic) BOOL *succ;
//JS
self.setSucc(1);
var str = self.data();

You have to blaze a new trail if you want to add a new property to an OC object:

defineClass('JPTableViewController : UITableViewController', {
  dataSource: function() {
    var data = self.getProp('data')
    if (data) return data;
    data = [1,2,3]
    self.setProp_forKey(data, 'data')
    return data;
  }
}

In JSPatch, you can use these two methods -getProp: and -setProp:forKey: to add properties dynamically. Basically, you are calling objc_getAssociatedObject and objc_setAssociatedObject to simulate a property since you can associate an object with current object self and get this object later from self. It works as a property except that its type is id.

Although we can use class-addIvar() to add new properties, but it must be called before the class is registered. It means that this method can be used in JS to add properties but can't used in existing OC class.

6. Keyword: self

defineClass("JPViewController: UIViewController", {
  viewDidLoad: function() {
    var view = self.view()
    ...
  },
}

You can use keyword self in defineClass, just like in OC, it means current object. Wondering how this is possible? Actually, self is a global variable and will be set to current object before calling instance method then back to nil after calling the method. With this little trick, you can use self in instance method.

7. Keyword: super

defineClass("JPViewController: UIViewController", {
  viewDidLoad: function() {
    self.super().viewDidLoad()
  },
}

Super is a keyword in OC which can't be accessed dynamically, so how can we support this keyword in JSPatch? As we all know, in OC, when calling method of super, you are actually calling method of super class and take current object as self. All we need to do is to simulate this process.

First of all, we have to tell OC that we want to call the method of super class, so when calling self.super(), we will create a new object in __c which holds a reference to OC object and has a property __isSuper set to 1:

...
if (methodName == 'super') {
  return function() {
    return {__obj: self.__obj, __clsName: self.__clsName, __isSuper: 1}
  }
}
...

When you call method of this returned object, __c will pass this __isSuper to OC and tell OC to call method of super class. In OC, we will find IMP in super class and create a new method in current class which points to the IMP in superclass. Now, calling the new method means calling method of super class. Finally, we should replace the method called with the new method.

static id callSelector(NSString *className, NSString *selectorName, NSArray *arguments, id instance, BOOL isSuper) {
    ...
    if (isSuper) {
        NSString *superSelectorName = [NSString stringWithFormat:@"SUPER_%@", selectorName];
        SEL superSelector = NSSelectorFromString(superSelectorName);

        Class superCls = [cls superclass];
        Method superMethod = class_getInstanceMethod(superCls, selector);
        IMP superIMP = method_getImplementation(superMethod);

        class_addMethod(cls, superSelector, superIMP, method_getTypeEncoding(superMethod));
        selector = superSelector;
    }
    ...
}

Extension

Support Struct

Struct should be converted when passed between OC and JS. At the beginning, JSPatach can only handle four native structs: NSRange/CGRect/CGSize/CGPoint and other structs can't be passed. Users have to use extension to convert customized structs. It works but can't be added dynamically because these codes must be written in OC in advanced, also it's quite complicated to write these codes. Now, I choose another approach:

/*
struct JPDemoStruct {
  CGFloat a;
  long b;
  double c;
  BOOL d;
}
*/
require('JPEngine').defineStruct({
  "name": "JPDemoStruct",
  "types": "FldB",
  "keys": ["a", "b", "c", "d"]
})

You can declare a new struct in JS and give a name to it. You also need to specify the name and type of every member. Now this struct can be passed between JS and OC:

//OC
@implementation JPObject
+ (void)passStruct:(JPDemoStruct)s;
+ (JPDemoStruct)returnStruct;
@end
//JS
require('JPObject').passStruct({a:1, b:2, c:4.2, d:1})
var s = require('JPObject').returnStruct();

To support this syntax, I take the value inside the struct in order and wrap it into a NSDictionary with its key. To read each member in order, we can get the length of each member according to its type and copy each value:

for (int i = 0; i < types.count; i ++) {
  size_t size = sizeof(types[i]);  //types[i] is of type float double int etc.
  void *val = malloc(size);
  memcpy(val, structData + position, size);
  position += size;
}

To pass struct from JS to OC works almost the same as above. We only need to allocate the memory(accumulate length of each member) and copy value from JS to this memory section.

This solution works well since we can add a new struct dynamically without declaring it in OC in advance. However, this relies strictly on the arrangement of each member in the memory space, and won't work as expected if there is byte alignment in some specific device. Fortunately, I have not encountered such a problem.

Support C function

Functions in C can't be called with reflection so we have to call them manually in JS. In detail, you can create a new method in the JavaScriptCore context whose name is the same as C function. In this method, you can call C function. Take memcpy() as an example:

context[@"memcpy"] = ^(JSValue *des, JSValue *src, size_t n) {
    memcpy(des, src, n);
};

Now you can call memcpy() in JS. In fact, here we get a problem about the conversion of parameters between JS and OC and we just ignore it temporarily.

We have another two problems:

  1. There will be too many source codes if all C functions are written in JSPatch in advance
  2. Too many C functions will affect the performance.

Therefore, I chose to use extension to solve these problems. JSPatch will only provide a context and methods to convert parameters, the interface looks like this:

@interface JPExtension : NSObject
+ (void)main:(JSContext *)context;

+ (void *)formatPointerJSToOC:(JSValue *)val;
+ (id)formatPointerOCToJS:(void *)pointer;
+ (id)formatJSToOC:(JSValue *)val;
+ (id)formatOCToJS:(id)obj;

@end

The +main method exposes a context to the external so that you are free to add functions to this context. The other four methods formatXXX are used to convert the parameters. Now, the extension of memcpy() looks like this:

@implementation JPMemory
+ (void)main:(JSContext *)context
{
    context[@"memcpy"] = ^id(JSValue *des, JSValue *src, size_t n) {
        void *ret = memcpy([self formatPointerJSToOC:des], [self formatPointerJSToOC:src], n);
        return [self formatPointerOCToJS:ret];
    };
}
@end

Also, with +addExtensions: method, you can add some extension dynamically when necessary:

require('JPEngine').addExtensions(['JPMemory'])

In fact, you can choose another way to add support to C function: you can wrap it in a OC method:

@implementation JPCFunctions
+ (void)memcpy:(void *)des src:(void *)src n:(size_t)n {
  memcpy(des, src, n);
}
@end

And then in JS:

require('JPFunctions').memcpy_src_n(des, src, n);

In this case, you don't need extension or parameter conversion, but you will use runtime mechanism which is only half as fast as using extension. So, for better performance, I decided to provide an extension.

Detail

Above is the rough explanation for how JSPatch works, next will be some issue or details encountered when implemented.

1. Special Struct

About replacing methods with _objc_msgForward mentioned above (Part Method replacement) ,which forward obj_msg, there will be a problem: if a method replaced return a "struct", using _objc_msgForward (or @selector(__JPNONImplementSelector) mentioned above) to replace this method will cause crash.

The reason and solution was found after several attemps:

you must use function _objc_msgForward_stret instead of _objc_msgForward for some CPU architectures or some struct.The differences between objc_msgSend_stret and objc_msgSend is explained in this articles which also explains why it is necessary to use this function. The main reason is the underlying mechanisms of C language, simply repeat here:

On most processors, the first few parameters to a function are passed in CPU registers, and return values are handed back in CPU registers. Objective-C methods (such as obj_msgSend) do the same, but with id self and SEL _cmd as the first two parameters.

-(int) method:(id)arg;
    r3 = self
    r4 = _cmd, @selector(method:)
    r5 = arg
    (on exit) r3 = returned int

CPU registers work fine for small return values like ints and pointers, but structure values can be too big to fit. For structs, the caller allocates stack space for the returned struct, passes the address of that storage to the function, and the function writes its return value into that space. The address of the struct is an implicit first parameter just like self and _cmd:

 -(struct st) method:(id)arg;
    r3 = &struct_var (in caller's stack frame)
    r4 = self
    r5 = _cmd, @selector(method:)
    r6 = arg
    (on exit) return value written into struct_var

Now consider objc_msgSend's task. It uses _cmd and self->isa to choose the destination. But self and _cmd are in different registers if the method will return a struct, and objc_msgSend can't tell that in advance. Thus objc_msgSend_stret: just like objc_msgSend, but reading its values from different registers.

What is said "some CPU architectures or some struct" mentioned above?Non-arm64 in ios architectures. And what struct need to go above process with xxx_stret instead of the original methods are no clear rules , OC does not provide an interface , only on a wonderful interface revealed the secret , so there is such a magical judgment :

if ([methodSignature.debugDescription rangeOfString:@"is special struct return? YES"].location != NSNotFound)

By the method debugDescription of class NSMethodSignature , we can judged, only by this string @"is special struct return? YES". Finally ,using _objc_msgForward_stret if it is special struct, or using _objc_msgForward.

2. Memory Issue

i.Double Release

Some memory issues were encountered when Implementation , starting with Double Release problems. When we read a parameter value From method -forwardInvocation: of NSInvocation object, if the parameter value is of type id , so we read like that :

id arg;
[invocation getArgument:&arg atIndex:i];

But it will cause crash because id arg equivalent __strong id arg at ARC, in this case if the code assign the variable "arg" , according to the mechanism of ARC , it will automatically insert retain statement when assignment, and insert release statement when quit the role domain.

- (void)method {
id arg = [SomeClass getSomething];
// [arg retain]
...
// [arg release]  release Out of scope
}

However, we did not assign "arg",but pass "arg" to method -getArgument: atIndex:, so it will not insert retain statement when assignment, but insert release statement when quit the role domain.This will cause double release and then crash.The solution is set "arg" to "__unsafe_unretained"or "__weak".

__unsafe_unretained id arg;
[invocation getReturnValue:&arg];

And you can also set local variable (returnValue) to hold this return object by __bridge transfer, like this:

id returnValue;
void *result;
[invocation getReturnValue:&result];
returnValue = (__bridge id)result;

ii. Memery Leak

After Double Release issue , we encountered a memory leak problem: Github issue page mentioned that the object does not release after alloc. After investigation, we locate the problem: when the method NSInvocation call is alloc , the returned object will not be released, causing a memory leak . It must transfer the object memory management rights out , and let the external object release it.

id returnValue;
void *result;
[invocation getReturnValue:&result];
if ([selectorName isEqualToString:@"alloc"] || [selectorName isEqualToString:@"new"]) {
    returnValue = (__bridge_transfer id)result;
} else {
    returnValue = (__bridge id)result;
}

This is because the ARC have agreed on the method name, when at the beginning of the method name is alloc / new / copy / mutableCopy, the object is returned with retainCount = 1, or return the object with autorelease, according to previous section view, normal method return value, ARC will automatically insert retain strong statement when assigned to a variable, but for methods such as alloc, are no longer automatically inserted retain statement:

id obj = [SomeObject alloc];
//alloc return object with retainCount +1,don't need retain

id obj2 = [SomeObj someMethod];
//return object with autorelease,ARC will insert [obj2 retain]

But the ARC does not deal with situations not explicitly invoked, when dynamic call these methods, ARC are not automatically inserted retain, in this case, the retainCount of object which alloc / new and other such methods return , is one more than other methods, so these methods require special handling.

3. About '_'

JSPatch use underscores '_' to connect multiple parameters of OC Method:

- (void)setObject:(id)anObject forKey:(id)aKey;
<==>
setObject_forKey()

But if OC method name contains '_' , it appears ambiguous:

- (void)set_object:(id)anObject forKey:(id)aKey;
<==>
set_object_forKey()

We Can not distinguish set_object_forKey corresponding selector is set_object:forKey: or set:object:forKey:.

In this regard we need to set a rule: in the JS use other characters instead of _ in the name of the oc method . JS naming rule leaving only the $ and _ when removed letters and numbers. So we have to use $ instead, but that is ugly:

- (void)set_object:(id)anObject forKey:(id)aKey;
- (void)_privateMethod();
<==>
set$object_forKey()
$privateMethod()

So we try another method, instead of using two underscores __:

set__object_forKey()
__privateMethod()

But there will be a problem, the method name end with '_' will not match:

- (void)setObject_:(id)anObject forKey:(id)aKey;
<==>
setObject___forKey()

So setObject___forKey() is matched to the corresponding selector setObject:_forKey:. Because rarely seen such a wonderful way of naming, and Use $ can also have the same problem, finally for look good, use the double-underlined __ replace _.

4.JPBoxing

We found JS cann't modify NSMutableArray / NSMutableDictionary / NSMutableString object by calling their methods in JSPatch. Because these were turned into JS Array / Object / String by JavaScriptCore when return from OC to JS. This conversion is mandatory in JavaScriptCore, can not be cancelled.

If we want object return to the OC can also call the method of this object, we must prevent JavaScriptCore conversion, the only way is to not return the object, but this object encapsulation, JPBoxing is doing that:

@interface JPBoxing : NSObject
@property (nonatomic) id obj;
@end

@implementation JPBoxing
+ (instancetype)boxObj:(id)obj
{
   JPBoxing *boxing = [[JPBoxing alloc] init];
    boxing.obj = obj;
    return boxing;
}

We save NSMutableArray / NSMutableDictionary / NSMutableString objects as as members of JPBoxing instance, then return to JS, After JS get a pointer JPBoxing object, send it back to the OC, OC can get to the original object members NSMutableArray / NSMutableDictionary / NSMutableString objects, similar to boxing / unboxing operations, thus avoiding these objects are JavaScriptCore converted.

Actually only variable NSMutableArray / NSMutableDictionary / NSMutableString three classes necessary to call JSBoxing to modify objects, immutable NSArray / NSDictionary / NSString is not necessary to do so, but for simple rules, JSPatch let NSArray / NSDictionary / NSString also return JSBoxing objects, to avoid distinction. Finally, the entire rule is quite clear: NSArray / NSDictionary / NSString and its subclass were as same as NSObject objects, also can call their OC methods, but if you want to convert then to JS type, you should use .toJS () interface to convert.

For the parameter and return value is a C pointer or Class type, we can also use the same JPBoxing way, save C pointer or Class as as members of JPBoxing instance to send to JS, unbox them to C pointer or Class when return to OC,So JSPatch supports all data types OC <-> JS cross pass.

5. About nil

i. distinguish NSNull/nil

For the "empty", JS has null / undefined, OC has nil / NSNull, JavaScriptCore process these parameters as follows:

  • From JS to OC, directly transfer null / undefined, OC will be converted to nil, if we transfer Array contain null / undefined to OC, it will be converted into NSNull.
  • From OC to JS, nil will be converted into null, NSNull return pointer as same as NSObject.

JSPatch pass parameters by array from JS to OC , so all null / undefined will become become NSNull in OC, while the real NSNull passed in is also NSNull, we can not tell which came from JS, so we have to distinguish them.

Considered that manually transfer variable NSNull is rare, and null / undefined and nil(OC) is very common, So, use a special variable nsnull in JS expressed NSNull, other null / undefined represents nil, so the incoming OC can distinguish nil and NSNull, show code below:

@implementation JPObject
+ (void)testNil:(id)obj
{
     NSLog(@"%@", obj);
}
@end

require("JPObject").testNil(null)      //output: nil
require("JPObject").testNil(nsnull)      //output: NSNull

This will have a little problem, if we explicit use NSNull.null() as a parameter,it will becomes nil in OC:

require("JPObject").testNil(require("NSNull").null())     //output: nil

we should note that you should use nsnull to replace NSNull, so OC can get NSNull from JS.

ii. Chained calling

The second question, if nil in the JS was null / undefined, it can not call the method of nil, and also can not guarantee the security of chain call:

@implementation JPObject
+ (void)returnNil
{
     return nil;
}
@end

[[JPObject returnNil] hash]     //it's OK

require("JPObject").returnNil().hash()     //crash

The reason is that null / undefined is not an object in JS, unable to call any method, even if __c() method add to add to any object. At the very beginning the only way is using a special object represents nil, in order to solve this problem. But if using a special object represents nil, it will be very complex to determine whether it is nil in JS:

//if use a _nil object represents nil in OC
var obj = require("JPObject").returnNil()
obj.hash()
if (!obj || obj == _nil) {
     //when determine whether the object is nil,it need determine whether equal _nil in additional
}

Such solution is difficult to accept, continue to look for solutions.Then we found true / false is in JS can call method, if use false representation nil, it will work, it also can directly if (!obj) to determine whether it is nil, so continue, solved other problems by false representation nil, and finally it is almost perfect solution, except there is a pit, pass false to the OC method which parameter is NSNumber * type, OC will be nil instead NSNumber objects:

@implementation JPObject
+ (void)passNSNumber:(NSNumber *)num {
     NSLog(@"%@", num);
}
@end

require("JPObject").passNSNumber(false) //output: nil

If the argument type of OC method is BOOL, or pass in true / 0, it work fine. So it is not a big problem.

Digression, the this of false in JS is no longer the original false object, but another Boolean object, it is too magic:

Object.prototype.c = function(){console.log(this === false)};
false.c() //output false

Someone try to explain #351

Summary

Above is the rough explanation and issue about how JSPatch works, I hope this will be helpful for you to understand and use JSPatch.

Clone this wiki locally