-
Notifications
You must be signed in to change notification settings - Fork 2.2k
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.
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:
- 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.
- Under some circumstances, the mixed locks of JavaScriptCore and Objective-C leads to the deadlock.
- 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:
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.
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:
-
- 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;
...
-
- 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.
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;
JSPatch didn't support overriding -dealloc
method for following reasons:
-
- With previous procedure, after JS overriding
-dealloc
method, calling-dealloc
method will packself
to be a weakObject. The following crash occurred:
- With previous procedure, after JS overriding
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.
-
- 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.
- 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
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.
-
- To resolve the first issue, instead of packing self as weakObject when calling
-dealloc
, pack it as assignObject and pass it to JS.
- To resolve the first issue, instead of packing self as weakObject when calling
-
- To resolve the second question, calling
ORIGdealloc
will change selectorName. ARC cannot recognize this-dealloc
method, we use the following approach:
- To resolve the second question, calling
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
.
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)