Skip to content

How JSPatch works2 Detail

easingboy edited this page Sep 12, 2016 · 6 revisions

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