Continuation can be read here
habrahabr.ru/post/170265Introduction
Studying the basics of developing for Android, I had to get acquainted with such a wonderful language like Java. Reading the next section of Google's GetStarted, I came across this design:
Button.OnClickListener mTakePicSOnClickListener = new Button.OnClickListener() { @Override public void onClick(View v) { } };
Declare a representative of OnClickListener and override his onClick method (correct me Java programmers). Hmm, I thought, but this feature would be cool to fuck in Objective-C, namely the ability to override the implementation of a method for an object (a specific object, and not a method implementation for all objects of a class) and even through blocks in runtime (!) And forgot about all this ... until I found myself in a half-empty bus in rainy weather. There was a lot of time and I decided to think about what can be done here.
Why was this necessary? Initially, I wanted to be able to do this:
')
tableView1.delegate = [[NSObject new] override:@selector(tableView:didDeselectRowAtIndexPath:) imp:^void(NSIndexPath* ip){ NSLog(@"selected row %i", ip.row); }] tableView2.delegate = [[NSObject new] override:@selector(tableView:didDeselectRowAtIndexPath:) imp:^void(NSIndexPath* ip){ NSLog(@"selected row %i", ip.row); }]
Please note that it is supposed to change the delegate and add / predetermine methods for it. And the tableView remains original, without any changes.
In that very place, I felt that this was quite realizable thanks to the
rich inner world of the Objective-C Runtime.
And yes, that very place did not let me down.
Examples
Let's start with examples. On the implementation and pitfalls, I'll tell you below
1) Override the viewWillApear method for the newly created UIViewContoller
UIViewController* vc = [[UIViewController new] overrideMethod:@selector(viewWillAppear:) blockImp:^id(UIViewController* selfVC, BOOL anim) { selfVC.view.backgroundColor=[UIColor redColor];
2) Implementing the UITableViewDataSource protocol
3) Like Java.
Implementation
So, I had an idea and a plan for its implementation:
- get the desired method by the selector (Method)
- override its implementation (IMP). Of course, through the blocks. For stylish and fashionable now
- ...
- PROFIT
Fool, how naive I was.
Attempt # 1
Here you should probably make a small digression and tell a little about the methods. Let's see what a method in Objective-C is:
- SEL - selector, method name
- IMP - implementation of the method. Normal C function
- method_types is a string describing the types of the method arguments, as well as the type of the value that the method returns.
Since it was planned to change the implementation of the method, first of all it was necessary to deal with the IMP. But to my regret, Apple's documentation was too superficial. All that could be found by IMP on this
page :
An Objective-C method is simply a function. You can add a function using the function class_addMethod. Therefore, given the following function:
and a small example IMP for a method of the form
- (void) method; But what to do with methods that have parameters (or returning something) was not clear. And the most important thing! C-function is good, but I would like to use blocks, how to deal with them? These two questions led me to this wonderful
article and I received answers to all my questions. Starting with ios 4 Runtime Api allows you to get IMP directly from the unit. By the way, Apple’s Runtime API documentation is not a word about this. I will give a description of the IMP and examples from the same article:
- IMP in the form of a C function has at least two arguments: the s-object whose method was invoked (probably correct to say the object to which the message was sent) and _c is the method selector. All the rest are method arguments.
- IMP as a block is similar to the previous one, but without a selector.
-(void)doSomething: void(*doSomethingIMP)(id s, SEL _c); void(^doSomethingBLOCK)(id s); -(void)doSomethingWith:(int)x; void(*doSomethingWithIMP)(id s, SEL _c, int x); void(^doSomethingWithBLOCK)(id s, int x); -(int)doToThis:(NSString*)n withThat:(double)d; int(*doToThis_withThatIMP)(id s, SEL _c, NSString *n, double d); int(^doToThis_withThatBLOCK)(id s, NSString *n, double d);
At that time, I could get information about the method, create an IMP from the block and replace the old IMP with a new one from the block, all thanks to runtime. That's it, I thought, how simple it is, and why has nobody done it yet? And only here I realized my mistake ... I will show with an example:
NSString *str1= @""; [str1 overrideMethod:@selector(intValue) imp:^int(NSString* selfStr){ return 1;}]; NSString *str2= @""; [str2 overrideMethod:@selector(intValue) imp:^int(NSString* selfStr){ return 2;}]; [str1 intValue];
Not that this is bad ... redefining the implementation of a method for all instances of a class using a block is cool, but not exactly what I needed initially.
Attempt # 2
And I started all over again, making a “knight's move”. It was decided instead of overriding the implementation of the old method to generate a new one, but with a unique name. And, intercepting sending a message to an object, replace the command / method selector. In the role of the unique name of the selector, the pair <address> _ <old selector> was selected. Those. The previous code should have been converted to this:
Of course, no problems for the user. It was planned to do all this through the mechanism of intercepting messages. Most likely this is my mistake, but I didn’t know and now I don’t know how it can be implemented differently, maybe you have some ideas?
In Objective-C, it is possible to intercept the sending of a message to an object in the following cases:
- when using NSProxy wrapper
- if the object receives a message that it cannot process (unrecognized selector)
As you see, it is impossible to intercept any message sent to the NSObject object (or did I just not find it?).
Using the NSProxy wrapper could help, but it:
1) not so elegant
NSString *str1= @""; NSProxy *proxy=[NSProxy proxyWithObject:str1]; [proxy overrideMethod:@selector(intValue) imp:^int(NSString* selfStr){ return 1;}]; [proxy intValue];
2) Sometimes it does not work. This is not how it can be done, since applicationDidBecomeActive will be sent to the delegate, not a wrapper from the proxy. Why do so? This is a completely different question ...
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { NSProxy *p=[NSProxy proxyWithObject:self];
And I decided to implement a redefinition of the implementation of the method through both mechanisms:
1) via NSProxy. As more correct, but not a universal solution
2) through a crutch, but universal, for any representative of NSObject.
And since NSObject and NSProxy are root classes, I theoretically implemented a predefined method implementation for any objective-c class at runtime. In practice, this was not entirely true.
So, briefly try to describe how everything works when using NSProxy:
- when overriding the implementation of a method, a new method is generated of the form <object address> _ <method selector>.
- when sending a proxy message, it is checked whether the class has a method of the form <object address> _ <method selector>
- if there is such a method, then a message is sent to the object (not a proxy) with this selector.
- if there is no method, a message is sent to the object with the original selector <method selector>
How it all works when using the crutch for NSObject
- implemented category for NSObject
- when overriding the implementation of a method, a new method is generated of the form <object address> _ <method selector>.
- it also generates a new method of the form <mm_old> _ <method selector>, which contains the IMP (implementation) of the original method
- the original method is “deleted”
- when sending a message to an object by the old selector, the forwardInvocation mechanism is triggered, since the old method is “deleted”
- In forwardInvocation, it is checked whether the class can handle a selector of the form <object address> _ <method selector>. If so, it is called.
- in forwardInvocation, it checks if the class can handle the <mm_old> _ <method selector>. If so, it is called.
- otherwise, we call [self doesNotRecognizeSelector: anInvocation.selector]; - standard implementation.
The use of such a crutch is fraught with problems. So, for example, if a custom class overrides the forwardInvocation method, then for it override will no longer work correctly. And most importantly, the destruction of the old method and working with forwardInvocation is a very serious blow to performance. And for all instances of the class. I will try to explain: if we redefine the intValue for one object in this way in the NSString class, then this class will never be the same. Now when sending the intValue message, all representatives of the NSString will be called mm_old_intValue, and moreover through the forwardInvocation mechanism.
By the way, about the removal. Unfortunately in Objective-C 2.0, the ability to remove methods was removed. For this, I had to make another crutch. By deletion, I mean replacing IMP of a deleted method with IMP of a non-existing method. Something like this
IMP forw=class_getMethodImplementation(clas, @selector(methodThatDoesNotExist:iHope:::::)); IMP oldImpl= method_setImplementation(method,forw);
Or equivalently, use _objc_msgForward.
IMP oldImpl= method_setImplementation(method,(IMP)_objc_msgForward);
Yes, now everything does not look so rosy, as it was in the examples. But what fun you can do by overriding the methods of viewControllers, AppDelegatov, delegates and other objects. And all this in two lines of code. And the methods are different: black, white, public, and
privat . Oh, something suffered me.
Restrictions
Although there are a few other limitations that I did not mention earlier:
- You cannot override methods from the NSObject protocol (for both NSProxy and for the NSObject category), as well as methods of the NSInvokation class (for the NSObject category)
- to call the super method you will have to call runtime api
- to access the private properties of the object, you will have to refer to runtime api
Maybe something else I don't know about. Such manipulations on the verge of a foul, they are like the East - a delicate matter.
How to use
There are two possible scenarios for changing the implementation:
Using MMProxy wrapper. To do this, you need to initialize MMProxy with the object that interests us and call the method to override it. Messages should be sent to the proxy, not to the object.
Example:
id object; MMProxy *p= [MMProxy proxyWithObject:object]; [object overrideMethod:@selector(onClick:) blockImp:^void(id obj,id sender){ }] ; [p onClick:nil]
For delegates, I recommend using a proxy created by the proxyWithMMObject method
MMProxy *ds = [MMProxy proxyWithMMObject]; [ds addMethod:@selector(numberOfSectionsInTableView:) fromProtocol:@protocol(UITableViewDataSource) isRequired:NO blockImp:^NSUInteger(id object, UITableView* tb) { return 1; }];
The second method is implemented through a category, and for this you can call the methods you need on any object inherited from NSObject. I already spoke about restrictions.
Both options implement a protocol with the following methods:
- (id) overrideMethod: (SEL) sel blockImp: (id) block;
allows you to change the implementation of the method. If the method for the specified selector is not found by the class (or there are any other problems), an exception will be generated
- (id) addMethod: (SEL) sel fromProtocol: (Protocol *) p isRequired: (BOOL) isReq blockImp: (id) block;
allows to add a method to an object in case the class does not have such a method. Similarly, if any problems occur, an exception will be thrown.
Application area
Separately, it is worth discussing the scope and general necessity of such an approach. As for me, yes, it is necessary. This is another approach that allows you not only to shoot yourself in the foot, but also to tear down the floor of the head.
Well, among other things
- fun
- adding block support to where everything was originally based on delegates. And this is a universal solution when using NSProxy. It is suitable for UIAlertViewDelegate, and for UIActionSheet, and for tableView, without the need to modify these classes (through categories or inheritance)
- debugging
- tests, this is of course not OCMock, but still.
Can "this" be used in industrial development. Definitely not.
At least for now. But you can play now.
And of course, the main goal of the article is to discuss.
Download the test project and see the implementation here:
github.com/Flanker4/MMMutableMethodsPS“Chukchi is not a writer, Chukchi reader.” Perhaps the article is written too incomprehensibly or contains errors. I apologize, write LS, we will correct.