preface
Recently I saw someone in the group sent an interview question, the topic is as follows:
@interface Spark : NSObject
@property(nonatomic,copy) NSString *name;
@end
@implementation Spark
- (void)speak {
NSLog(@"My name is:%@",self.name);
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
id cls = [Spark class];
void *obj = &cls;
[(__bridge id)obj speak];
}
Copy the code
Question: This code will run as Complie error? |Runtime crash? |NSLog ?
The ultimate problem is the result of this code.
process
At first glance, I immediately wanted to say that this thing must be compiling wrong or crashing
So I wrote some code along the way, and it turns out:
WTF? How can it work, and the result is still
I believe you will be as surprised as ME when you see this result. It is not logical. How could it be successfully executed and print the current controller?
parsing
There is no magic to computers. If a piece of code works, there must be a reason for it.
What we need to do is analyze why it works.
- Why the call doesn’t crash we need to understand,
cls
The meaning of.
In C, CLS is a pointer to the Spark class
When we do void *obj = &cls; When this statement is executed, we get a pointer to the pointer CLS
In fact, after this step, the pointer obj already functions as an object-c Object. Why? Now we can look at how the Runtime implementation works, but I’ll just say a few things here
Struct objc_object {Class isa OBJC_ISA_AVAILABILITY; }; Struct objc_class {Class isa OBJC_ISA_AVAILABILITY; #if ! __OBJC2__ Class super_class OBJC2_UNAVAILABLE; const char *name OBJC2_UNAVAILABLE; long version OBJC2_UNAVAILABLE; long info OBJC2_UNAVAILABLE; long instance_size OBJC2_UNAVAILABLE; struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; struct objc_method_list **methodLists OBJC2_UNAVAILABLE; struct objc_cache *cache OBJC2_UNAVAILABLE; struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; #endif } OBJC2_UNAVAILABLE; Struct objc_method_list {struct objc_method_list *obsolete OBJC2_UNAVAILABLE; int method_count OBJC2_UNAVAILABLE; #ifdef __LP64__ int space OBJC2_UNAVAILABLE; #endif /* variable length structure */ struct objc_method method_list[1] OBJC2_UNAVAILABLE; } OBJC2_UNAVAILABLE; // method struct objc_method {SEL method_name OBJC2_UNAVAILABLE; char *method_types OBJC2_UNAVAILABLE; IMP method_imp OBJC2_UNAVAILABLE; }Copy the code
Quote:IOS Runtime Details – Simple book
Part of the introduction above is incorrect because this is only displayed in <objc/ Runtime.h >, but it is felt that the change will not be visible if you delete it directly. I will give the correct explanation and data source below
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits;
}
Copy the code
Source: Apple Obj4 Open source code Line 1012 replaces objc_class in the above brief reference
You can see that the first field of objc_object is ISA pointing to a Class
That is, if we have a pointer to the address of Class, the object is ready to use, but the values of its member variables and so on have not been initialized.
So next use (__bridge ID)obj and the call will not cause a problem
- Why can I print a ViewController object?
There are two parts to this problem
1. When was the name attribute assigned? When was the ViewController object passed in?Copy the code
First, we need to understand how the data of a class object is stored.
Here I quote many arguments as above, let’s explore for ourselves
Time for code:
@interface Cls : NSObject
@property(nonatomic,strong) NSString *test;
@property(nonatomic,strong) NSString *test1;
@end
@implementation Cls
- (void)printPrinter {
NSLog(@"self:%p",self);
NSLog(@"self.test:%p",&_test);
NSLog(@"self.test1:%p",&_test1);
}
@end
Copy the code
Next, call printPrinter to print the object pointer address:
As you can see, the pointer offset member variable differs by 8 bytes from the pointer header address, and each member variable differs by 8 bytes from the previous member variable offset.
At this point, we still don’t know what to say about these two problems. But we know that Pointers to an object-c Object and Pointers to its member variables must be contiguous. This provides some ideas for the following analysis.
Next, I add a line of code to the original problem:
[super viewDidLoad];
NSString *str = @"11111";
id cls = [Spark class];
Copy the code
Why add this line of code, this step is after deep (blind) consideration (B), mainly considering the function internal parameter generation will need to store somewhere, but this part of the store address, we do not know, its implementation is hidden by the system. Our code has no obvious setup code, so it must be implemented by these conditions. So when we added this line of code, not surprisingly, the print changed
2018-11-29 20:49:39.254021+0800 test[1961:92498] 2018-11-29 20:49:39.254021+0800 test[1961:92498
It’s going to be the value that we mentioned above, which is pretty much what we expected
So a basic assumption emerges:
Because the address structure on the stack overlaps with the address structure required by the original class, all addresses have access to the corresponding value. We generated a Spark object from the default behavior of the stack!
To verify, let’s print the pointer stack addresses for CLS and STR
NSLog(@"cls address:%p str address:%p",&cls,&str);
Copy the code
2018-11-29 21:03:30.490989+0800 test[2129:122769] CLS address: 0x7ffeeBF4FA00 STR address: 0x7FFeeBF4FA08
We can see that the difference between them is exactly 8, and they are exactly as defined by the object structure. So this can also explain why the print result My name is:11111 occurs.
Note: The reason this exists is because of the small-encoder mode of the internal variables of the function, which assigns parameter addresses from the high address of the stack to the low address, so we print CLS addresses smaller than STR addresses.
This solves the first small problem. The answer is that we have pieced together the address data structure format of the Spark object when generating the stack arguments, just like the real object address data structure, so self.name is assigned the memory address at the moment the CLS is generated.
Now, the next question is when did the ViewController come in?
In this step we can only look at the operation performed before the CLS object is generated, [super viewDidLoad]; We only performed this step, so that must be the result of this operation. To verify, we can change the call order
id cls = [Cls class];
[super viewDidLoad];
Copy the code
When we do this, we find that the speak method crashes with EXC_BAC_ACCESS, indicating that we are referencing wild Pointers.
[super viewDidLoad]; I must have done something to push the ViewController’s self onto the stack.
Now we need to explore what we’re doing. We can rewrite viewcontroller.m into c++ code with the following command line and watch what happens.
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc ViewController.m -o ViewController.cpp
Copy the code
static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));
Copy the code
When we call [super viewDidLoad], we pass in the argument self, pushing self as a value, but _cmd is not used, so, It’s not pushed on the stack.
So far, the problem has been explained.
The answer
All NSObject objects start with an address that points to the object’s class. This condition is sufficient and necessary. Conversely, if an address refers to a class, we can use that address as an object. So the compilation will pass, and it won’t get an unrecognized selector error.
And the reason why it prints out as a ViewController object is because the data structure of CLS on the stack matches the data structure of CLS as a real class, clS. name’s original address is the address of the ViewController object on the stack, So NSLog can print
Thinking about
This kind of question, the thing that inspects is deep, and combine a lot of knowledge point. However, when we get the interview question and can think about it, we must take it into consideration. My idea of this question is also gradually improved in continuous experiments, and I have tried a lot. In fact, the process of finding the answer to the interview question is similar to the process of finding the code and finding the bug, which is to eliminate variables, gradually explore, and finally combine the exploration process with the concept.
note
Maybe the answer is not very professional, I hope you can tell me if you have a more professional answer. By the way, promote your blog synchronously :www.wdtechnology.club/
Thank you for reading
This article is published simultaneously in my blog and is not allowed to be used without authorization