This paper starts with a hook case of iOS daily development. Firstly, it briefly introduces the dynamic characteristics of Objective-C and the common defects of traditional hook methods, such as naming conflict, cumbersome operation, accidental break of hook chain and uncontrollable scope of hook. Then a lightweight hook scheme called SDMagicHook based on instance granularity of message forwarding mechanism is introduced in detail.
Github project address: github.com/larksuite/S… .
background
One day in a certain year, the product Xiao S proposed a simple but not simple demand to the developer Jun Xiao Q: expand the click area of a button. Q was glad to hear that: fortunately, this is a button I have customized, just need to rewrite the button pointInside:withEvent: method. I saw small Q hand knife fell in the product small S worship in the eyes easily completed. The code is as follows:
The next day, product S once again full of expectations to find the developer Q: Oba ~, help me to expand the click area of this button. Small Q this time but made difficult, heart secretly thought: this is the system to provide the standard UI components inside the button ah, I can only be used to change ah, I see you this is clearly deliberately embarrassed me fat tiger! I… I… I. —- little Q pawn.
In this case, the plight of little Q is truly pitiful. But think it over, is it true that there is no solution to this problem? As a matter of fact, it is not. Look at the officers, sit quietly and listen to my analysis.
1. Objective-c dynamic features
As an ancient and flexible language, objective-C has many Dynamic features that developers are interested in, especially Dynamic typing, Dynamic binding, Dynamic loading and other features. Many features that might seem impossible in other languages can be leveraged in OC to achieve more with less.
1.1 Dynamic typing
Dynamic typing means that the real type of an object is determined at runtime. For example, we can send any message to an object of type ID. This is legal at compile time, because the type can be determined dynamically, and the time when the message really takes effect is also after the object’s type is determined at run time, as discussed below. We can even modify an object’s isa pointer dynamically at run time to change its type, and the implementation of KVO in OC isa typical application of dynamic typing.
1.2 Dynamic Binding
When the type of an object is determined, its corresponding properties and respondable messages are also determined, which is dynamic binding. Once the binding is complete, the actual function address can be looked up in the type information at run time based on the type of the object and executed.
1.3 Dynamic Loading
Load the required materials and code resources according to requirements. Instead of loading all components at startup, users can load some executable code resources according to requirements. The executable code can contain new classes.
With these dynamic features of OC in mind, let’s revisit the requirements of the product: the product only wants to arbitrarily modify the click area of any button, and this time the button happens to be a child View in the system’s native component. Therefore, the key problem to be solved is how to change the “click detection method” of a component instantiated with system native classes. In the OC dynamic typing feature, we said that “messages really work when the object’s type is determined at runtime” and “we can even dynamically modify an object’s isa pointer at runtime to change its type. The implementation of KVO in OC isa typical application of dynamic typing”. Here you should have some ideas, we might as well copy the principle of KVO to implement.
2. The first version of SDMagicHook scheme
To use this kVO-like isa pointer replacement scheme, the following issues need to be resolved:
2.1 How do I dynamically create a new class
In OC, we can dynamically generate new classes by calling the objc_allocateClassPair and objc_registerClassPair functions of the Runtime. We then call the object_setClass function to replace an object’s ISA with our own temporary class.
2.2 How do I name these temporary classes
To make sense of a temporary class name, we need to see the relationship between the temporary class and its base class, so we can concatenate the new class name with NSString stringWithFormat:@ “SDHook*%s”, originalClsName, But there is a very obvious problem is that an object can not be exclusive to a proprietary class, so we can continue to expand the class name, might as well add a unique mark of the object – memory address, The new class name composition looks like this [NSString stringWithFormat:@ “SDHook_%s_%p”, originalClsName, self], which looks perfect this time, but can be problematic in extreme cases, For example, if we create objects of the same type over and over again in a for loop 10,000 times, there is a high probability that the memory address of the new object will be the same as the memory address of the previously freed object, and we will release the temporary classes used by an object very soon after its destruction. There is a chance that the class that the newly generated object is using will be released and crash will happen. To solve this problem, we need to add a random tag to the temporary class name to reduce the probability of this happening, The final class name composition looks like this [NSString stringWithFormat:@ “SDHook_%s_%p_%d”, originalClsName, self, Mgr.randomFlag].
2.3 When to destroy these temporary classes
We can dynamically associate each NSObject object with an instance of SDNewClassManager by objc_setAssociatedObject, which holds the temporary classes used by the current object. The SDNewClassManager instance is also destroyed when the current object is destroyed, and we can then do some destruction of temporary classes in the dealloc method of the SDNewClassManager instance. However, we cannot immediately destroy the temporary class, because the object has not been completely destructed and it is still doing some other cleaning operations. If we destroy the temporary class at this time, it will inevitably cause crash, so we need to delay the destruction of these temporary classes for a little time, the code is as follows:
Ok, so far we have implemented the first version of the Hook scheme, but there are two obvious problems:
- Adding a category each time a hook defines a function is relatively cumbersome;
- If we implement a method of the same name in each category of a Class, only one method will be called.
To this end, we developed the second version to improve and optimize the deficiencies of the first version.
3. Optimized SDMagicHook scheme
To solve these two problems, we can generate an IMP with a block and then replace the IMP with a method corresponding to the target Selector. Example API code:
This block scheme looks much simpler and more convenient, but it also faces the problem that any hook scheme can’t avoid. That is, how to call the corresponding native method in the block?
3.1 Key point 1: How do I call native methods inside a block
In the first version of the scheme, we added a hook specific method to a class’s category, and after the method exchange, the callback to the native method was done by sending the selector message corresponding to the hook specific method itself to the instance. But now we use the block to create an “anonymous function” to replace the native method. Since it is an anonymous function, there is no explicit selector, which means that there is no way to find its native method after the method exchange.
The key now is to find an appropriate Selector to map to the native function being hooked. And so far, the only Selector that we can easily call in the current compilation environment that’s associated with this block is the Selector of the original method which is pointInside:withEvent: in our demo. So this pointInside:withEvent: Selector becomes a one-to-many mapping key, and when someone sends a pointInside:withEvent: message externally to our button, We should first forward pointInside:withEvent: to the IMP of our custom block implementation, Then, when a pointInside:withEvent: message is sent to Button again inside the block, the message is forwarded to the system’s native method implementation, and a perfect method scheduling is completed.
3.2 Key Point 2: How to design a message scheduling scheme
In OC, to dispatch method distribution, it is necessary to obtain the control of message forwarding, and to obtain the control of message forwarding, it is necessary to force the receiver to trigger its message forwarding mechanism every time it receives the message, and then make corresponding scheduling in the process of message forwarding. In this example we replace the imp pointer to the method corresponding to the target button’s pointInside:withEvent: with _objc_msgForward. This way, whenever someone calls the button’s pointInside:withEvent: method it ends up in the forwardInvocation: method, which we implement to handle the method dispatch.
Because the imp pointer to the target button’s pointInside:withEvent: method is replaced with _objc_msgForward, So we need to add methods A and B to store the block custom implementation and native implementation of the target Button’s pointInside:withEvent: method, respectively. Then when you need on a custom method internal call the original method by calling callOriginalMethodInBlock: this API to explicitly told that the sample code is as follows:
CallOriginalMethodInBlock method of internal implementation is actually added an identifier for this call used when scheduling in method to determine whether need to invoke the original method, its implementation code is as follows:
When the target Button instance receives A pointInside:withEvent: message, our custom message scheduling mechanism will be enabled. If OriginalCallFlag is false, the custom implementation method A will be called, otherwise the original implementation method B will be called. Thus, a method scheduling is implemented smoothly. The flow chart and sample code are as follows:
Imagine a scenario where we have a global keyWindow and all businesses want to listen to the layoutSubviews method of the keyWindow. How do we manage and maintain the relationships between hook implementations added to the keyWindow? What if an object is about to be destroyed and needs to unhook the previous keyWindow?
Our solution is to generate a hook table for each target native method being hooked, generate internal selectors for it in the order in which the hooks occur and add them to the hook table. When keyWindow receives a layoutSubviews message, we extract the corresponding Hook selector from the hook table and send it to the KeyWindow to perform the corresponding action. If you delete a hook, you simply remove the corresponding selector from the hook table. The code is as follows:
4. Prevent accidental break of hook chain
We all know that when we hook a method, we need to call the original method that is hooked in the method body of our hook code. If this step is omitted, the hook chain will be broken, so that the original method that is hooked will never be called. If someone hook this method before you, all the hooks before you will inexplicably fail. Because this is a hidden problem, it is often difficult for you to realize that your hook operation has caused serious problems for others.
In order to facilitate the hook operator to quickly and timely find this problem, we added a set of “Hook chain fracture detection mechanism” in DEBUG mode, whose implementation principle is roughly as follows:
As already mentioned, we achieve the goal of hook method of custom scheduling, which makes us have a chance to at the end of these method calls to detect whether the execution of the method through callOriginalMethodInBlock invoke the original method. If it is found that a method body is not the original method body of the target function to be hooked and the original method has not been called after the execution of this method, it will send an interrupt signal through Raise (SIGTRAP) to suspend the current program to remind the developer that the original method was not called during the hook operation.
5. Advantages and disadvantages of SDMagicHook
Compared with the traditional scheme of adding a custom method into a category and then hook, the advantages and disadvantages of SDMagicHook are as follows:
Advantages:
- With only one block, you can hook any method of any instance without adding any category. It is simple and efficient, which can greatly improve the efficiency of debugging programs.
- Hook scope can be controlled within the granularity of a single instance to minimize hook side effects.
- You can hook any common instance or even any class, whether it is generated by yourself or provided by a third party.
- Any hook can be added or removed at any time, easy to manage the hook.
Disadvantages:
- In order to ensure thread safety when adding and deleting hooks, SDMagicHook adds read and write locks within the instance granularity when performing operations related to adding and deleting hooks. If there are frequent hook operations in multiple threads, some thread waiting overhead may be brought, but it can be ignored in most cases.
- Because it is based on the instance dimension, it is more suitable to handle the scenario of a single instance of a class hook. If you need your hook to be effective for all instances of a class, it is recommended to stick to the traditional way of hook.
conclusion
SDMagicHook can be used directly in OC and Swift UIKit layers, and the hook scope can be limited to one instance you specify to avoid contaminating other unrelated instances. Api design is simple and easy to use, you only need to spend a minute to get started easily and quickly, we hope this solution can bring you a better iOS development experience.
Welcome to Bytedance Technical Team