Swift & runtimeHistorical technical note

Objective-C runtime fundamentals: properties and instance variables

Distinguishes Objective-C properties from instance variables, examining runtime structures, memory offsets and synthesize to explain how properties are generated. Examples then show why dynamically inspecting and manipulating an object's structure can be useful.

Original byline:SindriLin

Originally credited to SindriLin; edited and archived by Bison.

Source listed in the original

English translation of the Chinese original, translated on . The article body uses automated translation; app descriptions, historical reading notes and video subtitles receive separate editorial checks. Original publication dates, attribution, code and version notes are retained.

Editor: Bison
Contributor: SindriLin’s Notes

“The runtime refers to mechanisms that operate while a program is running. In iOS development, runtime features give oc its distinctive appeal.”

The runtime refers to mechanisms that operate while a program is running. In iOS development, runtime features give oc its distinctive appeal.
For C and C++, the methods a class object can call and the operations it can perform are determined when the program is compiled. The runtime mechanism allows
oc can dynamically create classes, perform black-box testing, extend properties, and more at runtime, greatly increasing its flexibility. Today, using some runtime mechanisms,
let's discuss properties and variables in oc. (This is the first article in my runtime series. If you disagree with any of the points raised, please join the
comments to discuss them with me.)

property and ivar


First, properties (property) and member variables (ivar) are different things. In oc, the distinction is as follows:

  • Member variables
    Member variables are usually declared in a class declaration's@interface or class implementation@implementation variables declared in the braces that follow,
    Their default visibility is@protected protected visibility (inaccessible outside the file), as well as@public public visibility,@private
    private visibility and@package package visibility
@interface Person
{
    @public       ///< 所有可视范围都能访问
    NSString * name;
    NSString * sex;
    @private      ///<  只有本类能够访问
    NSString * personalWealth;
    @protected    ///< 本类和子类都能访问 
    NSString * housesNumber;
    @package      ///<  框架内视为public,框架外为private
    NSString * familyWealth;
}


  • Properties

Compared with a variable, a property involves several compiler operations: 1. Use @synthesize to generate its corresponding ivar, usually named 下划线+属性名. 2. Generate a setter to set the ivar. 3. Generate a getter to retrieve it.

In a sense, a property wraps a member variable, adding setter and getter methods to make it
better suited to object-oriented programming. (If you do not understand why setters and getters are needed, see this article:
What getter and setter methods are for)

Property memory structure and @synthesize

In my earlier article,KVO implementation article
, I briefly discussed class memory structure. Here, we will look more closely at how memory changes after declaring properties and running the program. We will find that
@synthesize actually does.

My Person class now looks like this:

@interface Person: NSObject {
    NSString * _name;
    NSString * _sex;
    char _ch;
}

@property(nonatomic, copy) NSString * abc;
@property(nonatomic, copy) NSString * efg;

@end    


@implementation Person

- (instancetype)init {
    if (self = [super init]) {
        NSLog(@"%p, %p, %p, %p, %p, %p, %p", self, &_name, &_sex, &_ch, _abc, _efg);
    }
    return self;
}

@end


  • Question one: Address offsets of member variables

Although OC has its own characteristics as a dynamic language, its class structure does not differ greatly from that of other languages.
In terms of class structure, member-variable addresses are offsets from the class object's own address, so these variables'
addresses should increase by 0x8 each time, or 0x4 on a 32-bit system. The log output from the code above is as follows:

0x7fb649c0c9b0, 0x7fb649c0c9c0, 0x7fb649c0c9c8, 
0x7fb649c0c9d0, 0x7fb649c0c9d8, 0x7fb649c0c9e0

As expected, the last three addresses differ by 0x8. However, the class object and its first member variable differ by 0x10. Why
is that? In Apple's open-source file's relevant code, we can find Class
type definition

typedef struct objc_class *Class;
struct objc_class {
    Class isa;
    ······
}

Class represents the Objective-C class structure. This code shows that it is a pointer to the objc_class struct, which contains an isa pointer. That extra pointer also explains the 0x10 offset in the output above: the two addresses differ by an isa. A later runtime article will discuss this in more detail.

  • Question two: How are address offsets calculated?

A pointer occupies 8bit on a 64-bit system, but char uses only one bit, so why is the offset also 8 here? Is it calculated like a structure's address offset? This brings us to class_addIvar(const char *, NSUInteger *, NSUInteger *), a function for adding a variable to a class. Its final parameter specifies the variable's memory alignment. Apple explains that parameter as follows:

The instance variable’s minimum alignment in bytes is 1«align. The minimum alignment of an instance variable depends on the ivar’s type and the machine architecture. For variables of any pointer type, pass log2(sizeof(pointer_type)).

This states that alignment is the variable's minimum alignment in bytes, but it does not explain exactly how the alignment works. In
objc-runtime-new.mm
contains code that calculates address offsets. Looking at that code makes this clearer:

    uint32_t offset = cls->unalignedInstanceSize();
    uint32_t alignMask = (1<<alignment)-1;
    offset = (offset + alignMask) & ~alignMask;

In brief, Apple specifies a default variable offset of 1 << alignment, which in this context is the pointer length. Thus Objective-C class-structure offsets differ from ordinary struct offsets: anything shorter than 8bit is assigned an 8bit offset.

  • Question three: How are property backing variables stored?

As discussed earlier, a property declared with @property automatically receives an underscore-prefixed ivar and associated setter and getter during compilation, allowing _property access within the class. The offset output shows that property-generated variables follow the member variables. How does that happen? In question two, I mentioned the runtime function class_addIvar(), described in Xcode as follows:

    * @note This function may only be called after objc_allocateClassPair and before objc_registerClassPair. 
    *       Adding an instance variable to an existing class is not supported.
    * @note The class must not be a metaclass. Adding an instance variable to a metaclass is not supported.
    * @note The instance variable's minimum alignment in bytes is 1<<align. The minimum alignment of an instance 
    *       variable depends on the ivar's type and the machine architecture. 
    *       For variables of any pointer type, pass log2(sizeof(pointer_type)).

During compilation, class operations include allocating class memory and adding variables, properties, and method lists. The class must then be registered before use. The class_addIvar() function is used before registration to add instance variables to the class and its variable list. From this, we infer that @synthesize uses it during compilation to add and store a property's instance variable. If this guess is correct, those variables should appear in the instance-variable list. The runtime provides a way to test that too: Ivar * class_copyIvarList(Class, unsigned int *) returns the class structure's variable list. The following code retrieves all variables of Person and prints their names:

unsigned int ivarCount;
Ivar * ivars = class_copyIvarList([Person class], &ivarCount);

for (int idx = 0; idx < ivarCount; idx++) {
    Ivar ivar = ivars[idx];
    NSLog(@"%s", ivar_getName(ivar));
}
free(ivars);

The instance-variable list for the Person class above is printed as follows:

2016-01-07 21:59:49.580 LXDCodingDemo[3036:255608] _omg
2016-01-07 21:59:49.581 LXDCodingDemo[3036:255608] _name
2016-01-07 21:59:49.581 LXDCodingDemo[3036:255608] _ch
2016-01-07 21:59:49.581 LXDCodingDemo[3036:255608] sct
2016-01-07 21:59:49.581 LXDCodingDemo[3036:255608] _sex
2016-01-07 21:59:49.581 LXDCodingDemo[3036:255608] _copying
2016-01-07 21:59:49.581 LXDCodingDemo[3036:255608] _egf
2016-01-07 21:59:49.581 LXDCodingDemo[3036:255608] _hij

We can see that @synthesize does call this method, binding property and variable memory through class_addIvar().

  • Question four: What exactly does @synthesize do?

This question may seem surprising. From the preceding code and discussion, @synthesize clearly generates and binds variable memory. In the Person class, for example, declaring the abc property makes the compiler automatically generate @synthesize abc = _abc;, which means we can write it ourselves. What if we bind the property to an existing member variable instead, using @synthesize abc = _name? What addresses will be printed afterward?

@implementation Person
@synthesize abc = _name;      ///< 自定义绑定属性

- (instancetype)init {
    if (self = [super init]) {
        NSLog(@"%p, %p, %p, %p", &_name, &_sex, &_ch, &_efg);
    }
    return self;
}

@end

Adding this custom binding causes an error in the original code. Binding the abc property to _name means the compiler no longer generates _abc, so that variable cannot be found in the class. After creating a Person instance, the console's address output remains unchanged, still differing by 0x8.

0x7ff92b45a438, 0x7ff92b45a440, 0x7ff92b45a448, 0x7ff92b45a458

To test the relationship between abc and _name, I added this code to main:

Person * p = [Person new];
p.abc = @"123";
NSLog(@"%@, %@", p.abc, p->_name);

The output for abc matches _name. This small example shows that when @synthesize supplies memory for a property, it first searches for an existing instance variable with the same name. If found, it generates a getter and setter to access that memory. Otherwise, it creates a new member variable and then binds the accessors. Thus @synthesize does more than simply call class_addIvar() when adding a variable; it also traverses the variable list.

1

Just as black has its opposite in white, @synthesize has a counterpart. In Objective-C, @dynamic propertyName prevents the compiler from binding variable storage and generating setters and getters for the property, leaving us to provide these methods at runtime. The runtime messaging article will explain this.

  • Question five: How does @synthesize determine a property's type?

What if the custom binding changed from _name to _ch? The compiler reports an error because of type checking. But how does it determine a property's type when generating default storage? Properties also have attributes such as copy, strong, and weak, which affect setter implementation. How does @synthesize distinguish them? The less commonly used keyword @encode returns a type-encoding description. I added the following code to main and obtained this console output:

NSLog(@"%s, %s, %s", @encode(Person), @encode(CGRect), @encode(NSInteger));

///输出
{Person=#@@c}, {CGRect={CGPoint=dd}{CGSize=dd}}, q

looks rather confusing. Apple's official documentation explains that the compiler uses C characters to represent every OC type, while using@encode(type)
returns the encoding for this type. The mappings between these encodings can be found in Type encoding
.

The output shows that Person has encoding #@@c, where # denotes an object and the following elements represent id, id, and char. In the class file, these correspond to _name, _sex, and _ch. This shows how @synthesize identifies the type of a property's bound variable. The class_addIvar() function accepts a const char * argument describing attributes and variable types, allowing @synthesize to pass the obtained encoding and generate the corresponding variable.

How is a property's type determined? Apple also provides the following in the runtime: property_getAttributes()
to obtain an object's type attributes. These attributes likewise use@encode a similar set of type encodings. These type encodings'
standard table can also be found in Property type encoding.
If you enjoy reading the code of open-source frameworks, the recently emerging YYModel you can see the author's extensive use of type encoding:

2

Application

Theory that cannot be put into practice is nonsense. — 沃德天·毫率

I have outlined many plausible theories, but they are of little use unless we can apply them. Data persistence is unavoidable in development. My company's projects are small and do not store much data, so I generally use the archiving provided by NSCoding. That often leads to code like this:

3

Writing it this way is not a problem while the model contains little data. As the amount of model data grows, this approach
may cause:

1. With many fields, mismatched strings in archiving can cause saving or loading to fail. 2. The workload increases. As mentioned above, runtime's class_copyIvarList() retrieves every instance variable of a class; class_copyPropertyList() does the equivalent for properties. We can use it to enumerate properties and their names, then implement a one-step archiving macro similar to a singleton macro. The core code follows:

unsigned int propertyCount; 
objc_property_t * properties = class_copyPropertyList([self class], &propertyCount);    

for (int idx = 0; idx < propertyCount; idx++) {
    objc_property_t property = properties[idx];
    NSLog(@"\n--name: %s\n--attributes: %s", property_getName(property), property_getAttributes(property));  }   
}   
free(properties);


Print the property's information in the console:

--name: abc
--attributes: T@"NSString",C,N,V_abc

--name: efg
--attributes: T@"NSString",C,N,V_efg

--name: hij
--attributes: T@"NSString",C,N,V_hij

Both runtime-based archiving and unarchiving enumerate the class's properties. Define an LXDCodingHandler block to hold the processing for each objc_property_t property and pass it into this enumeration:

typedef void(^LXDCodingHandler)(objc_property_t property, NSString * propertyName);

I have already wrapped the relevant code so that a model can be serialized in a single line.
Demo project


Recommended reading: A Windows AliWangWang-style login screen with a paper-feed animation