📜 ⬆️ ⬇️

Redefining method implementation. Inspired by java

Continuation can be read here habrahabr.ru/post/170265

Introduction


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]; //  view }]; [self presentViewController:vc animated:YES completion:^{ }]; // vc 

2) Implementing the UITableViewDataSource protocol

  // ds (  MMProxy ) MMProxy *ds = [MMProxy proxyWithMMObject]; //     NSArray *arr=@[@"one",@"two",@"three", @"four",@"five"]; //   (  ,    ) //:  isRequired         (@required) //   (@option) //     [ds addMethod:@selector(numberOfSectionsInTableView:) fromProtocol:@protocol(UITableViewDataSource) isRequired:NO blockImp:^NSUInteger(id object, UITableView* tb) { return 1; }]; [ds addMethod:@selector(tableView:numberOfRowsInSection:) fromProtocol:@protocol(UITableViewDataSource) isRequired:YES blockImp:^NSUInteger (id object, UITableView* tb) { return [arr count]; }]; [ds addMethod:@selector(tableView:cellForRowAtIndexPath:) fromProtocol:@protocol(UITableViewDataSource) isRequired:YES blockImp:^id(id obj, UITableView* tb, NSIndexPath* indexPath) { static NSString *TableIdentifier = @"SimpleTableItem"; UITableViewCell *cell = [tb dequeueReusableCellWithIdentifier:TableIdentifier]; if (cell == nil) cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:TableIdentifier]; cell.textLabel.text=arr[indexPath.row]; return cell; }]; self.tableView.dataSource=(id<UITableViewDataSource>)ds; [self.tableView reloadData]; 

3) Like Java.

 //UIOnClickListener  .    onClick:    //-(IBAction)onClick:(id)sender{}; [button1 addTarget:[[UIOnClickListener new] overrideMethod:@selector(onClick:) blockImp:^void(id obj,id sender){ NSLog(@"bump 1"); }] action:@selector(onClick:) forControlEvents:UIControlEventTouchUpInside]; [button2 addTarget:[[UIOnClickListener new] overrideMethod:@selector(onClick:) blockImp:^void(id obj,id sender){ NSLog(@"bump 2"); }] action:@selector(onClick:) forControlEvents:UIControlEventTouchUpInside]; 


Implementation


So, I had an idea and a plan for its implementation:

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:

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:


 -(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]; //  2,   1 [str2 intValue]; //  2 

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:

 // 0x0000042 NSString *str1= @""; //   0x0000042_intValue [str1 overrideMethod:@selector(intValue) imp:^int(NSString* selfStr){ return 1;}]; // 0x0000043 NSString *str2= @""; //   0x0000043_intValue [str2 overrideMethod:@selector(intValue) imp:^int(NSString* selfStr){ return 2;}]; [str1 0x0000042_intValue]; // 1 [str2 0x0000043_intValue]; // 2 


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:
  1. when using NSProxy wrapper
  2. 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]; // 1 

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]; // Override point for customization after application launch. [p overrideMethod:@selector(applicationDidBecomeActive:) blockImp:^void(id obj,UIApplication* app) { UIAlertView *alv=[[UIAlertView alloc] initWithTitle:@"ss" message:nil delegate:nil cancelButtonTitle:@"fuuu" otherButtonTitles:nil]; [alv show]; [alv release]; }]; return YES; } 

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:


How it all works when using the crutch for NSObject

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:

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

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/MMMutableMethods
PS
“Chukchi is not a writer, Chukchi reader.” Perhaps the article is written too incomprehensibly or contains errors. I apologize, write LS, we will correct.

Source: https://habr.com/ru/post/168105/


All Articles