Skip to content

JSPatch New Features Review

Gavin Zhou edited this page Aug 7, 2016 · 1 revision

JSPatch New Features Review

JSPatch is improving with the help of the community. This article will review the JSPatch new features added in past few months, and their realization. They are performSelectorInOC interface without lock, supporting variable argument, adding type to defineProtocol interface, supporting overriding dealloc method, and two extensions, JPCleaner and JPLoader.

performSelectorInOC

JavaScript is a single-thread language; the Objective-C adds a lock to JS part when running it by JavaScriptCore Engine. It promises the JS code running serialized under the JSContext. This makes calling JSPatch substitution method, and calling Objective-C method from JSPatch will run in this lock. It has three issues:

  1. The JSPatch substitutes method cannot run paralleled. If the main thread and the child thread are both running substituting methods, methods are running serialized. The main thread waits child thread to finish the execution. If child thread speeds long time, it will just block the main thread.
  2. Under some circumstances, the mixed locks of JavaScriptCore and Objective-C leads to the deadlock.
  3. UIWebView initialization has conflict with JavaScriptCore. If the first UIWebView happens in the JS lock, it will result UIWebView loading failure.

In order to solve the problems above, we introduce a new interface .performSelectorInOC(selector, arguments, callback). It can execute Objective-C method without JavaScriptCore lock, it promises serialized execution as well.

For example:

defineClass('JPClassA', {
  methodA: function() {
    //run in mainThread
  },
  methodB: function() {
      //run in childThread
      var limit = 20;
      var data = self.readData(limit);
      var count = data.count();
      return {data: data, count: count};
  }
})

If the main thread and child thread call -method A and method B at the same time, while the function call self.readData(limit) take long time to finish, it will block the main thread. If so, you can use performSelectorInOC() instead. It executes after the JavaScriptCore lock released, and it keeps serialized execution.

defineClass('JPClassA', {
  methodA: function() {
    //run in mainThread
  },
  methodB: function() {
      //run in childThread
      var limit = 20;
      return self.performSelectorInOC('readData', [limit], function(ret) {
          var count = ret.count();
          return {data: ret, count: count};
      });
  }
})

The calling difference are shown in the below: Difference The first piece of code is corresponding to the left image. The -methodB has been substituted. When the Objective-C calls the function -methodB, it enters the JSPatch core, JPFordInvocation and calls the JS function -methodB . The JSCore lock is locked, then it calls -reloadData in Objective-C. The -reloadData method is exectued in the JavaScriptCore lock. The lock releases after JS method returns.

The second piece of code is corresponding to the right flow chart. The first part is the same, JavaScriptCore add a lock when -methodB is called. However, the -methodB will not call any Objective-C method directly, instead it returns an object. The returning object has the following structure:

{
    __isPerformInOC:1,
    obj:self.__obj,
    clsName:self.__clsName,
    sel: args[0],
    args: args[1],
    cb: args[2]
}

JS execution stops after it returns this object. The JavaScriptCore lock is released as well. The Objective-C is able to get this object, then it checks _isPerformInOC=1. If it's true, it calls the Objective-C method from the selector. Since the execution happens outside the JavaScriptCore, our goal has been achieved.

After running OC method, it calls the parameter in the cb function. It passes the returning value to the cb function and goes back to JS part. This part runs repeatedly to check the value of _isPerformInOC=1. If yes, it repeats the procedure above, stops otherwise.

This is all concepts, you can find related code here and here. The realization is relative simple, and has no impact to other parts. It will take a while to understand it.

Variable Arguments Calling Supports

JSPatch didn't support function calls like NSString method +(instancetype)stringWithFormat:(NSString *) format, ... in the past. When JSPatch calls a Objective-C function, it assembles NSInvocation based on method name and parameters from JS, however, NSInvocation does not support variable agreements.

Later @wjacker uses another approach, he uses obj_msgSend to support variable argument support. We didn't use objc_msgSend before, since it is suitable for dynamic calling. The method defination and calling is fixed:

    1. Definition

It need to define the type and number of parameters in advance. For example, use objc_msgSend to call the following:

- (int)methodWithFloat:(float)num1 withObj:(id)obj withBool:(BOOL)flag

You have to define a C function

int (*new_msgSend)(id, SEL, float, id, BOOL) = (int (*)(id, SEL, float, id, BOOL)) objc_msgSend;

Then, use new_msgSend to call this method. While this process cannot be accomplished dynamically, it can only be decided at compile time. Since various number of parameters and return type, it cannot finish exhaustively at compile time. So, it cannot be used in all function calls.

For the variable argument, supporting id type is almost sufficient for most situations. It makes the following possible

id (*new_msgSend1)(id, SEL, id,...) = (id (*)(id, SEL, id,...)) objc_msgSend;

In this way, we can use new_msgSend1 to call a fixed argument with following variable arguments. In the simulator, it suppport N number of id, which satisfies our original needs. After @wjacker and @Awhisper test, it doesn't work out on devices. You have to define different method for different number of arguments. Here is some explanation from [official document] (https://developer.apple.com/library/ios/documentation/General/Conceptual/CocoaTouch64BitGuide/ConvertingYourAppto64-Bit/ConvertingYourAppto64-Bit.html#//apple_ref/doc/uid/TP40013501-CH3-SW26). Eventually, we use a lot of definition to deal with one to ten variables.

id (*new_msgSend2)(id, SEL, id,id,...) = (id (*)(id, SEL, id,id,...)) objc_msgSend;
id (*new_msgSend3)(id, SEL, id,id,id,...) = (id (*)(id, SEL, id,id,id,...)) objc_msgSend;
id (*new_msgSend4)(id, SEL, id,id,id,id,...) = (id (*)(id, SEL, id,id,id,id,...)) objc_msgSend;
...

    1. Calling the method

After resolving issues mentioned above, there is one remaining. Unlike NSInvocation, objc_msgSend cannot dynamically assemble and pass number of arguments at runtime. objc_msgSend has to decide the number of parameters at the compile time. You need to use logic like if-else to write calling function. Here is the gist. It looks better after applying macro definition.

defineProtocol

When JSPatch adds a non-existing method from Objective-C class, all arguments will be defined as id types. Since new JS method are rarely called from Objective-C, instead these methods are using on JS part. In general, all parameters can be think as objects, so those methods mentioned above are defined as id.

In fact, there is a situation. In an Objective-C header file, there is a defined method; however, this method is not implemented in the .m file. The project will crash when this method is called from elsewhere. If you want to fix this bug by using JSPatch, you will use JSPatch to dynamic assign argument type.

In fact, by using defineClass(), there are solution by passing parameters and return parameter. It just will lead to a complex defineClass interface. Eventually, @Awhisper came up with a good solution, which is adding dynamic protocol.

First, defineClass() supports using protocol:

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

It adds different types arguments to unimplemented method, instead of id types.

If you want to add certain type of argument, you just need to add new protocol dynamically. It will not make defineClass() ugly, and solve the problem cleanly.

defineProtocol('JPDemoProtocol',{
   stringWithRect_withNum_withArray: {
       paramsType:"CGRect, float, NSArray*",
       returnType:"id",
   },
}

defineClass('JPTestObject : NSObject <JPDemoProtocol>', {
    stringWithRect_withNum_withArray:function(rect, num, arr){
        //use rect/num/arr params here
        return @"success";
    },
}

Here is the implementation details;

Support Overriding dealloc Method

JSPatch didn't support overriding -dealloc method for following reasons:

    1. With previous procedure, after JS overriding -dealloc method, calling -dealloc method will pack self to be a weakObject. The following crash occurred:
Cannot form weak reference to instance (0x7fb74ac26270) of class JPTestObject. It is possible that this object was over-released, or is in the process of deallocation.
    1. If we don't pack the current object, or don't pass any object to JS, then the overriding will success. However, without calling native -dealloc method, this object cannot release properly, which leads to memory leak.

After overriding -dealloc method, the original selector has changed to ORIGdealloc. It you run native Objective-C -dealloc method after JS -dealloc execution, it crashes. The possible reason is the ARC did protection to -dealloc method. The possible reason is the selector name has to be -dealloc when executing IMP. In this way, at run time [super dealloc] can be called.

I run out of ideas here, until @ipinka come up an idea to cheat ARC implementation.

    1. To resolve the first issue, instead of packing self as weakObject when calling -dealloc, pack it as assignObject and pass it to JS.
    1. To resolve the second question, calling ORIGdealloc will change selectorName. ARC cannot recognize this -dealloc method, we use the following approach:
Class instClass = object_getClass(assignSlf);
Method deallocMethod = class_getInstanceMethod(instClass, NSSelectorFromString(@"ORIGdealloc"));
void (*originalDealloc)(__unsafe_unretained id, SEL) = (__typeof__(originalDealloc))method_getImplementation(deallocMethod);
        originalDealloc(assignSlf, NSSelectorFromString(@"dealloc"));

In this way, we get IMP of ORIGdealloc, which is the original implementation in Objective C. Then we pass the selectorName to dealloc, the ARC recognize it as -dealloc .

Extensions

JPCleaner

Sometimes, JSPatch users might have requirements that is reverted to unsubstituted version after executing scripts. I suggested developers not to run the script for the next launching, but it doesn't help the situation that requires instant reverting. While, this requirement is not a core functionality, we extract it and put it into extensions.

Adding JPCleaner.h , then call +cleanAll interface. It reverts all substituted method from JSPatch to previous version. Additionally, +cleanCleass: supports revert a certain class. These methods have OC and JS versions.

[JPCleaner cleanAll]
[JPCleaner cleanClass:@“JPViewController”];

The implementation is pretty straight-forward. The substituted methods are saved in a static variable in _JSOverideMethods . It has the following structure _JSOverideMethods[cls][selectorName] = jsFunction. I added an interface to JPExtension, and give access to this static variable. Looping through saved class and selectorName, and point IMP to the original IMP. Check the original code.

##JPLoader ## JSPatch scripts are need to push from the server side, so the client has to has downloading and executing functions. It has to consider security issues. JPloader will do everything for you.

Downloading script is easy, its task is to guarantee the security of transmission. JPLoader contain a packaging tool packer.php. This tool pack scripts and get a MD5 value to the packed file. The MD5 value will by encrypted by RSA private key. In the end, the encrypted MD5 is sent with the script. JPLoad unencrypted the MD5 and then compares it with the MD5 value for received file. The identical MD5 values will prove the script has not been changed from the third party. Check another article JSPatch security strategies for deployment. For using JPloader, here it reference to [Wiki document] (https://github.com/bang590/JSPatch/wiki/JSPatch-Loader-%E4%BD%BF%E7%94%A8%E6%96%87%E6%A1%A3)

Clone this wiki locally