Original article reproduced please indicate the source, thank you

I believe HotFix is familiar to all of you. Today I will summarize some of the recent research projects. HotFix solutions in iOS can be roughly divided into four categories:

  • WaxPatch(Alibaba)
  • Dynamic Framework(Apple)
  • React Native(Facebook)
  • JSPatch(Tencent)

WaxPatch

WaxPatch is an iOS framework written in Lua language. It not only allows users to use Lua to call the iOS SDK and API inside the application, but also uses OC Runtime to call the class methods written by OC inside the application to achieve the purpose of HotFix.

The advantage of WaxPatch is that it supports iOS6.0 and has excellent performance. However, its disadvantages are also very obvious. It does not comply with the audit rules of Apple3.2.2, that is, it cannot dynamically deliver executable codes. Code executed through Apple javascriptCore. framework or WebKit is an exception. Wax has not been maintained for a long time. As a result, many OC methods cannot be implemented using Lua. For example, Wax does not support block. Finally, Lua scripts must be embedded in the execution engine to run Lua scripts; Wax does not support the ARM64 framework.

Dynamic Framework

Dynamic Framework, in fact, is a dynamic library; First, I’ll describe some of the features and differences between dynamic and static libraries.

Libraries, whether static or dynamic, are essentially an executable binary format that can be loaded into memory for execution. Static libraries on iOS can be divided into.a files and.framework, and dynamic libraries can be divided into.dylib(which became.tdb after Xcode7) and.framework.

Static library: complete copy to executable file when linking, multiple use of multiple redundant copies.

Dynamic library: link does not copy, program run by the system dynamically loaded into memory, for program call, the system only loaded once, multiple programs shared, saving memory.

Static libraries and dynamic libraries are relative to compile time and run time: static libraries are linked to the object code when the program is compiled, so the program does not need to change the static library when it is run. Dynamic libraries are not linked to the object code when the program is compiled, but are only loaded when the program is running, because dynamic libraries are needed during the program's runtime.

Summary: When the same static library is used in different programs, each program must be imported once, and packaged into a package to form a program. However, dynamic libraries in different programs are not packaged into the package, and are only linked to load when the program is running (such as system frameworks such as UIKit, Foundation, etc.), so the program size will be much smaller.

Ok, so the Dynamic Framework is basically a way to HotFix bugs by updating the Framework that the App relies on, but the disadvantage of this is obvious it doesn’t comply with the audit rules of Apple3.2.2, This method is not available on the Apple Store, it can only be used in some jailbreak markets or some internal projects, and this solution is not really suitable for BugFix, more suitable for the big update of the App online. Therefore, the third-party frameworks introduced in our project are static libraries. We can use the file command to check whether our Framework is static or dynamic.

React Native

React Native supports JavaScript development, so you can change the JS file to implement the HotFix of the App. However, the obvious disadvantage of this solution is that it is only suitable for applications that use the React Native solution.

JSPatch

JSPatch is a scripting language that can be used to dynamically update iOS apps, replace project native code, and quickly fix bugs by simply introducing a minimal JSPatch engine into a project and calling objective-C native interfaces using JavaScript. But JSPatch also has its own shortcomings, mainly in because it relies on javascriptcore, framework, and it is the framework in iOS7.0 after introducing in, so it is not support iOS6.0 JSPatch, is due to the use of the script for the JS technology at the same time, So it is lower than Wax in terms of memory and performance.

Therefore, of course, JSPatch was adopted in the end, but there were still some problems in the actual process, so it was very helpful for us to grasp the core principles of JSPatch to solve the problems.

Explain the core principles of JSPatch

Preloaded part

About the interpretation of core principles, there are many, but are almost the same, many are still quoted author bang his writing the content of the document, so I use an example to explain JSPatch main operation process, actually, of course, also can refer to some authors briefly, you can consult the process I wrote about, In conjunction with the source code or official documentation, you should know about JSPatch.

 [JPEngine startEngine];
    NSString *sourcePath = [[NSBundle mainBundle] pathForResource:@"demo" ofType:@"js"];
    NSString *script = [NSString stringWithContentsOfFile:sourcePath encoding:NSUTF8StringEncoding error:nil];
    [JPEngine evaluateScript:script];Copy the code

The first step is to start JSPatch by running JPEngine startEngine. The startup process is divided into two parts:

  • Using JSContext, we declare a number of JS methods to memory, so that we can call these methods in JS later. The main methods used include the following methods, while listening for a memory alarm notification.

      context[@"_OC_defineClass"] = ^(NSString *classDeclaration, JSValue *instanceMethods, JSValue *classMethods) {
         return defineClass(classDeclaration, instanceMethods, classMethods);
     };
     context[@"_OC_callI"] = ^id(JSValue *obj, NSString *selectorName, JSValue *arguments, BOOL isSuper) {
         return callSelector(nil, selectorName, arguments, obj, isSuper);
     };
     context[@"_OC_callC"] = ^id(NSString *className, NSString *selectorName, JSValue *arguments) {
         return callSelector(className, selectorName, arguments, nil, NO);
     };
     context[@"_OC_formatJSToOC"] = ^id(JSValue *obj) {
         return formatJSToOC(obj);
     };
    
     context[@"_OC_formatOCToJS"] = ^id(JSValue *obj) {
         return formatOCToJS([obj toObject]);
     };
    
     context[@"_OC_getCustomProps"] = ^id(JSValue *obj) {
         id realObj = formatJSToOC(obj);
         return objc_getAssociatedObject(realObj, kPropAssociatedObjectKey);
        };
    
     context[@"_OC_setCustomProps"] = ^(JSValue *obj, JSValue *val) {
         id realObj = formatJSToOC(obj);
         objc_setAssociatedObject(realObj, kPropAssociatedObjectKey, val,
         OBJC_ASSOCIATION_RETAIN_NONATOMIC);
     };Copy the code
  • Load the JSPatch. Js file. The main content of the JSPatch file is to define some JS functions, data structures and variables that we will use later.

The script is running

We define the following script:

require('UIAlertView') defineClass('AppDelegate',['name', 'age', 'temperatureDatas'], { testFuncationOne: Function (index) {self.setname ('wuyike') self.setage (21) self.settemperaturedatas (new Array(37.10, 36.78) Alloc ().initWithTITLE_message_DelegATE_CANCelButtonTITLE_otherButtonTitles ("title",  self.name(), self, "OK", null) alertView.show() } }, { testFuncationTwo: function(datas) { var alertView = UIAlertView.alloc().initWithTitle_message_delegate_cancelButtonTitle_otherButtonTitles( "title", "wwww", self, "OK", null) alertView.show() } });Copy the code

Then it’s time to execute JPEngine evaluateScript: Script. The program starts executing our script, but before that, JSpatch does some processing on our script, which also has two aspects:

  • We need to add part of the try catch code to our program. The main purpose is to catch the error message when our JS script has an error
  • Change all functions to be called as __c primients.

In the end, the script we call looks like this:

; (function(){try{require('UIAlertView') defineClass('AppDelegate',['name', 'age', 'temperatureDatas'], { testFuncationOne: Function (index) {self.__c("setName")('wuyike') self.__c("setAge")(21) self.__c("setTemperatureDatas")(new Array(37.10, 36.78, Var alertView = 36.56)) UIAlertView.__c("alloc")().__c("initWithTitle_message_delegate_cancelButtonTitle_otherButtonTitles")( "title", self.__c("name")(), self, "OK", null) alertView.__c("show")() } }, { testFuncationTwo: function(datas) { var alertView = UIAlertView.__c("alloc")().__c("initWithTitle_message_delegate_cancelButtonTitle_otherButtonTitles")( "title", "wwww", self, "OK", null) alertView.__c("show")() } });Copy the code

It is illegal to call uiAlertView.alloc () because it does not take the form of message forwarding. All functions of a class are defined in JS.

{__clsName: "UIAlertView", alloc: function() {... }, beginAnimations_context: function () {... }, setAnimationsEnabled: function () {... },... }Copy the code

But this form will have to traverse the current class of all methods, but also loop to find until the top of the parent class method, this method is a direct result of the problem is the boom in memory, so it is not feasible, so finally the author used the forward thinking, defines a _c function, all function by _c forwarding, thus solved our problems.

It is worth mentioning that our __c function is declared in the Object method of js when we execute jspatch.js. This is the function below. _customMethods declares many functions that need to be appended to Object.

  for (var method in _customMethods) {
    if (_customMethods.hasOwnProperty(method)) {
      Object.defineProperty(Object.prototype, method, {value: _customMethods[method], configurable:false, enumerable: false})
    }
  }Copy the code
1. require

After calling require(‘UIAlertView’), you can directly use UIAlertView to call the corresponding class method. What require does is simply create a variable of the same name in the JS global scope that points to an object. The object attribute __clsName holds the Class name and indicates that the object is an OC Class.

var _require = function(clsName) { if (! global[clsName]) { global[clsName] = { __clsName: clsName } } return global[clsName] }Copy the code

This way we will not get an error when we call uiAlertView.__c (), because it is already a global Object in JS.

{
  __clsName: "UIAlertView"
}Copy the code
2.defineClass

Next we need to execute the defineClass function

 global.defineClass = function(declaration, properties, instMethods, clsMethods)Copy the code

The defineClass function can take four arguments: string: “Class name to replace or add: inherited parent class name < implemented protocol 1, implemented protocol 2>” [attributes] {instance method} {class method}

When I call this function, I do three things:

  • The _formatDefineMethods method is executed to change the format of the passed function function and to append the argument resolution back from the OC callback to the original implementation.
  • Then execute the _OC_defineClass method, which is the method that calls OC, parse the attributes of the incoming class, instance method, class method, and call the overrideMethod method to swizzing, which is the method redirection.
  • Finally, _setupJSMethod is executed, and _ocCls records class instance methods and class methods in JS.

The argument that the _formatDefineMethods method receives is a method list JS object, plus a new JS empty object

var _formatDefineMethods = function(methods, newMethods, realClsName) { for (var methodName in methods) { if (! (methods[methodName] instanceof Function)) return; (function(){ var originMethod = methods[methodName] newMethods[methodName] = [originMethod.length, Function () {try {// execute through OC callback, To obtain parameters of var args = _formatOCToJS (Array) prototype. Slice. Call (the arguments) var lastSelf = global. Self global. Self = args [0] the if (global.self) global.self.__realClsName = realClsName // Delete the first two arguments: In the OC message forwarding, the first two arguments are self and selector, which need to be removed when we call the actual implementation of js. Args.splice (0,1) var ret = originMethod. Apply (originMethod, args) global.self = lastSelf return ret } catch(e) { _OC_catch(e.message, e.stack) } }] })() } }Copy the code

It can be found that the concrete implementation is to iterate over the properties of the method list object (method name), and then add the same properties to the JS empty object. Its value corresponds to an array. The first value of the array is the number of parameters corresponding to the method name, and the second value is a function (that is, the concrete implementation of the method). The _formatDefineMethods function, in short, modifies the JS object passed in defineClass:

{testFuncationOne: function () {testFuncationOne: function () {... TestFuncationOne: [argCount, function (){... new implementation}]}Copy the code

Number is the purpose of passing parameters, the runtime when repair class, cannot be achieved directly resolve original js function, then don’t know the number of parameters, especially in creating new methods, need according to the number of parameters to generate the method signature, namely reduction method name, so only in the end to get the number of parameters of js function to the OC.

// initWithTitle_message_delegate_cancelButtonTitle_otherButtonTitles // oc initWithTitle:message:delegate:cancelButtonTitle:otherButtonTitles:Copy the code

About _OC_defineClass

  1. Use NSScanner to separate the classDeclaration into three parts

    • The name of the class: the className
    • SuperClassName: superClassName
    • Protocol name: protocalNames
  2. Get the Class object using NSClassFromString(className).

    • If the Class object is nil, a new Class is added to the JS side. Register a new Class with objc_allocateClassPair and objc_registerClassPair.
    • If the Class object is not nil, then the JS side is replacing an existing Class
  3. Add/replace instance methods and class methods to the class object based on the instance method and class method parameters passed from the JS side

    • When you add an instance method, you get the class object directly using the previous step. Adding a class method requires calling the objc_getMetaClass method to get the metaclass.
    • If the method is already defined by the class to be replaced, the method is replaced and message forwarding is implemented directly.
    • Otherwise, judge according to the following two situations
      • Traverse protocalNames, through objc_getProtocol method get object to the agreement, then use protocol_copyMethodDescriptionList method to obtain the agreement of the type and name. Matching selectorName passed in JS to obtain typeDescription string, the implementation of the protocol method of message forwarding.
      • In either case, the JS side requests to add a new method. Construct a typeDescription as “@@:\@*” (return type id, parameter value based on the number of parameters defined by JS). The return type and parameter type of the new method can only be id, because only objects can be defined on the JS side. Add the IMP to the class.
  4. Add setProp:forKey and getProp: methods to this class, and use objc_getAssociatedObject and objc_setAssociatedObject to give the JS script the ability to set property

  5. Return {className: CLS} back to the JS script.

But it also includes an overrideMethod method, which is used for both replacement and new methods. Its main purpose is to perform method swizzing, or redirection of methods. All the messages are forwarded to the ForwardInvocation function. The purpose of this is that all the parameters are available in the NSInvocation so that a general IMP can be implemented. Any method any parameters can be through the IMP transit, get method of all parameters callback JS implementation. So the overrideMethod actually does the following:

To implement this, use the -viewwillAppear: method to replace UIViewController:

  1. The UIViewController -ViewwillAppear: method points to _objc_msgForward via the class_replaceMethod() interface. This is a global IMP, and OC calls are forwarded to this IMP if they don’t exist. Replace the method directly with the IMP so that the invocation goes to -Forward Invocation:.

  2. Add -OrigViewwillAppear and -_JPViewwillAppear methods to UIViewController, the former pointing to the original IMP implementation and the latter to the new implementation that will call back JS functions later.

  3. Override the -Forward Invocation of UIViewController: custom implementation. Once the OC calls the UIViewController’s -ViewwillAppear: method, this will be forwarded to the -forwardInvocation:, where an NSInvocation is assembled with the invocation parameters. The invocation method JPviewWillAppear (from the NSInvocation method) will be invoked with the invocation from the NSInvocation method. The whole call process ends, as shown below:




1.png


About _setupJSMethod

if (properties) { properties.forEach(function(o){ _ocCls[className]['props'][o] = 1 _ocCls[className]['props']['set' + O.substr (0,1).touppercase () + o.substr(1)] = 1})} var method = function(className, method, isInst) realClsName) { for (var name in methods) { var key = isInst ? 'instMethods': 'clsMethods', func = methods[name] _ocCls[className][key][name] = _wrapLocalMethod(name, func, realClsName) } }Copy the code

The last step is to put all of the previous methods and properties into _ocCls, and then call require to save the class into the global variable.

At this point, all the objects in our JAVASCRIPT script have been replaced by the Runtime, that is, all that remains is how to properly execute the contents of the JAVASCRIPT function once we have produced the trigger function.

3. Object holding/conversion

Here is a quote from the author:

The statement require('UIView') generates the UIView object in the JS global scope, which has a property called __isCls, indicating that it represents an OC class. Calling UIView's alloc() method will go to the _c() function, where the caller _isCls property is identified as representing the OC class, passing the method name and class name to the OC to complete the call. This is true for calling class methods, but what about instance methods? Uiview.alloc () returns a UIView instance object to JS. How is this OC instance object represented in JS? How can WE call uiView.alloc ().init() when JS gets an instance object?

For a custom ID object, JavaScriptCore passes a pointer to the custom object to JS. The object is not available to JS, but OC can find the object when it is passed back. For the management of the object life cycle, according to my understanding, if JS has a variable reference, the OC object reference count will be increased by 1, JS variable reference release will be reduced by 1, if there is no other holder of OC, the OC object life cycle will follow JS, will be released when JS garbage collection. This pointer can also be returned to OC. To call an instance method of this object in JS, simply pass the pointer and the name of the method to be called back to OC in _c(). How to determine if the caller in _c() is a pointer to an OC object? There is no way to determine if a JS object represents an OC pointer. The solution here is to wrap the object as an NSDictionary before OC returns it to JS:

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

Let the OC object be a value of this NSDictionary, so that in JS the object becomes:

{__obj: [OC Object pointer]}Copy the code

In this way, we can know whether the object represents the OC object pointer by judging whether the object has the _obj attribute. In the _C function, if the caller has the _obj attribute, we can take out the attribute and return it to OC together with the instance method called, and then the instance method is called.

But:

JS cannot call the NSMutableArray/NSMutableDictionary/NSMutableString methods to modify the data of these objects. Because JavaScriptCore converts all three of them to JS Array/Object/String when returning from OC to JS, they are separated from the original Object when returning. This conversion is mandatory in JavaScriptCore and cannot be selected.

The only way to prevent JavaScriptCore from converting is not to return the object directly, but to encapsulate it. JPBoxing does just that.

The NSMutableArray NSMutableDictionary/NSMutableString object as members of the JPBoxing saved in JPBoxing instance object is returned to the JS, JS get JPBoxing object pointer, Back to OC can take to the original members through object NSMutableArray/NSMutableDictionary NSMutableString object, similar to the packing/devanning operations, thus avoiding the these objects are JavaScriptCore transformation.

In fact only a variable NSMutableArray NSMutableDictionary/NSMutableString these three classes necessary to invoke its methods to modify the data in the object, Immutable NSArray/NSDictionary/nsstrings is not necessary to do so, directly into the corresponding type JS would be more convenient to use, but for the sake of simple rules, JSPatch let NSArray NSDictionary/nsstrings also returned in the form of encapsulation, Avoid calling an OC method and having to worry about whether it returns a mutable or immutable object. In the end, the whole rule was clear: NSArray NSDictionary/nsstrings and its subclasses and other ACTS of NSObject object, get on JS is its object pointer, you can call them the OC method, if you want to put the three types of objects into corresponding JS, with additional. ToJS () interface to conversion.

JPBoxing is also used to wrap the pointer and Class in JPBoxing. The pointer and Class are stored on JPBoxing and returned to JS. OC is then unwrapped to retrieve the original pointer and Class. This allows JSPatch to support the transfer of all OC<->JS data types.

4. Type conversion

Here’s a quote from the author:

When JS passes the class/method/object to OC, OC calls the class/object via NSInvocation. There are two things to do to ensure the invocation to the method and return the value:

  1. Get the parameter types of the OC method to be called, and convert the object from JS to the required type for invocation.
  2. Fetch the return value based on the return value type, wrap it as an object and pass it back to JS.

For example, view.setAlpha(0.5), JS passes an NSNumber to OC, and OC needs to know that the parameter is a float value from the NSMethodSignature that calls the OC method. So you convert NSNumber to float and then you call the OC method as a parameter. Int /float/bool, CGRect/CGRange, etc.

5. callSelector

CallSelector that’s the last function we’re going to execute! But before we can execute this function, there’s a lot more going on.

For the _c function

__c: function(methodName) { var slf = this if (slf instanceof Boolean) { return function() { return false } } if (slf[methodName]) { return slf[methodName].bind(slf); } if (! slf.__obj && ! slf.__clsName) { throw new Error(slf + '.' + methodName + ' is undefined') } if (slf.__isSuper && slf.__clsName) { slf.__clsName = _OC_superClsName(slf.__obj.__realClsName ? slf.__obj.__realClsName: slf.__clsName); } var clsName = slf.__clsName if (clsName && _ocCls[clsName]) { var methodType = slf.__obj ? 'instMethods': 'clsMethods' if (_ocCls[clsName][methodType][methodName]) { slf.__isSuper = 0; return _ocCls[clsName][methodType][methodName].bind(slf) } if (slf.__obj && _ocCls[clsName]['props'][methodName]) { if (! slf.__ocProps) { var props = _OC_getCustomProps(slf.__obj) if (! props) { props = {} _OC_setCustomProps(slf.__obj, props) } slf.__ocProps = props; } var c = methodName.charCodeAt(3); If (methodName. Length > 3 && methodName. Substr (0, 3) = = 'set' && > = 65 c & c < = 90) = {= "return =" "" "" "function (val) =" " var="" propName="methodName[3].toLowerCase()" +="" methodName.substr(4)="" slf.__ocProps[propName]="val" }="" else="" function(){="" slf.__ocProps[methodName]="" args="Array.prototype.slice.call(arguments)" _methodFunc(slf.__obj,="" slf.__clsName,="" methodName,="" args,="" slf.__isSuper)="" }<="" code=""/>Copy the code

The _c function is a message forwarding center. It can be divided into two types according to the parameters passed in:

  • For instance methods and class methods, the _methodFunc method is finally called
  • For custom attributes, set and GET operations.

For custom properties, it doesn't actually add the properties to the object in the OC, it just adds an _ocProps object, and then in JS, the _ocProps object holds all of the properties that we defined. To get values, we just need to get names from that property.

For the _methodFunc method, you simply revert the name of the OC method, take parameters, and pass it on to the class or instance method.

var _methodFunc = function(instance, clsName, methodName, args, isSuper, isPerformSelector) { var selectorName = methodName if (! isPerformSelector) { methodName = methodName.replace(/__/g, "-") selectorName = methodName.replace(/_/g, ":").replace(/-/g, "_") var marchArr = selectorName.match(/:/g) var numOfArgs = marchArr ? marchArr.length : 0 if (args.length > numOfArgs) { selectorName += ":" } } var ret = instance ? _OC_callI(instance, selectorName, args, isSuper): _OC_callC(clsName, selectorName, args) return _formatOCToJS(ret) }Copy the code

For callSelector methods:

  1. Initialize the
    • The instance object encapsulated by JS is disassembled to obtain the OC object.
    • Get the corresponding class object and selector based on the class name and selectorName;
    • The NSMethodSignature is constructed from the class object and the Selector. Then the NSInvocation object is constructed based on the signature and the Target and selector are set for the Invocation object
  2. According to the method signature, get the actual type of each parameter of the method, and convert the parameters passed from JS (for example, the actual type of the parameter is int, but JS can only pass the NSNumber object, which needs to be converted through [[jsObj toNumber] intValue]). After conversion, set the parameters for the NSInvocation object using the setArgument method.
  3. Execute the invoke method.
  4. The return value is obtained using the getReturnValue method.
  5. According to the return value type, it is encapsulated into the corresponding object in JS (because JS does not recognize OC object, so the return value of OC object needs to be encapsulated into {className:className, obj:obj}) and returned to the JS end.

conclusion

Ok, so far, all of our processes have been completed and our JS files have taken effect. Of course, the JSPatch principle I mentioned is only part of the basic principle, which can enable our basic process to be realized. There are also some complex operation functions that need further study to be mastered. JSPatch is also a good example for learning Runtime, just like Aspects. You can go and study it.