preface
We explored the loading of categories in the basic principles of IOS and the loading of categories. Today we’re going to look at the storage of attributes in categories, also known as associated objects, and the exploration of class extension
The preparatory work
- Objc – 818.2 –
- Iced watermelon
Class extensions
Class extension
- Attach additional attributes, member variables, and method declarations to a class
- Many blogs on the web write that class extension is a special kind of anonymous classification. I do not agree with this statement. Classification and class extension are fundamentally different
- The bottom of the classification is
category_t
Struct type, while the class extension layer is compiled directly into the main class - General class extension write
.m
In a file, general private attributes or attributes that you want to distinguish independently are written to the class extension
Class extension format
.m
Format in file
@interface LWTeacher(a)
{
NSInteger height; // Member variables
}
@property(nonatomic,assign)NSInteger lw_age;/ / property- (void)helloWord;// Object method+ (void)helloClass;/ / class methods
@end
Copy the code
This declaration of class extensions in.m files is something that developers use almost every day, but few of us know it as a class extension. Note that class extensions must be written directly in the class declaration and the class implementation
Question: there are no class declarations in the.m file, only the extension and implementation class declarations are in the.h file, which will be expanded into the.m file at compile time
Extension
#import "LWTeacher.h"
@interface LWTeacher (a)
@property(nonatomic,assign)int age;
@end
Copy the code
There is only one.h file and no corresponding.m when creating the class extension. All of the implementation of a class Extension is implemented in a.m file. Extension means that the Extension method of the.m file is written into a separate header file
Class extension low-level exploration
The purpose of writing LWTeacher in main.m is to better explore the. CPP file, the code is as follows
@interface LWTeacher : NSObject
- (void)sayHello;
@end
@interface LWTeacher(a)
{
NSInteger height;
}
@property(nonatomic,assign)NSInteger lw_age; - (void)helloWord; + (void)helloClass;
@end
@implementation LWTeacher
+(void)load{
}
- (void)sayHello{
}
- (void)helloWord{
}
+(void)helloClass{
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
LWTeacher * teacher = [[LWTeacher alloc] init];
[teacher sayHello];
}
return 0;
}
Copy the code
Generate main. CPP file from main.m file
The variables and methods shown in the figure for class extensions are determined at compile time and are stored in the class. To verify that this is done at compile time, set a breakpoint in the _read_images method
It is clear from the diagram that the method of class extension is also in RO, which is determined at compile time. So now it follows that a class can have multiple class extensions, but all implementations are implemented in a.m file
Associated object –set
The application scenario of the associated object is generally adding attributes in the classification. Now we will explore the underlying implementation of the associated object. Create LWTeacher+HH and LWPerson+LWA
int main(int argc, const char * argv[]) {
@autoreleasepool {
LWPerson * person = [[LWPerson alloc] init];
person.lw_name = @"luwenhan";
person.lw_showHello = @"hello";
person.lw_showHello1 = @"H1111";
person.lw_showHello2 = @"H2222";
LWTeacher * teacher = [[LWTeacher alloc] init];
teacher.lw_teacher = @"teacher";
}
return 0;
}
//LWPerson+LWA
@implementation LWPerson (LWA)
+(void)load{
}
-(void)setLw_name:(NSString *)lw_name{
objc_setAssociatedObject(self, "lw_name", lw_name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
-(NSString *)lw_name{
return objc_getAssociatedObject(self, "lw_name"); }...// LWTeacher+HH
@implementation LWTeacher (HH)
+(void)load{
}
-(void)setLw_teacher:(NSString *)lw_teacher{
objc_setAssociatedObject(self, "lw_teacher", lw_teacher, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
-(NSString *)lw_teacher{
return objc_getAssociatedObject(self, "lw_teacher");
}
@end
Copy the code
Define 4 attributes in LWPerson+LWA classification and create define 1 attribute in LWTeacher+HH classification. Add a breakpoint in objc_setAssociatedObject, run the source code, and go to objc_setAssociatedObject
void
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
_object_set_associative_reference(object, key, value, policy);
}
Copy the code
Enter the _object_set_associative_reference method inside the code is more, look at the implementation of the core code
void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{
if(! object && ! value)return; // No value is returned directly
if (object->getIsa()->forbidsAssociatedObjects())
...
// Object is encapsulated into DisguisedPtr to facilitate the unified processing at the lower level
DisguisedPtr<objc_object> disguised{(objc_object *)object};
// Encapsulate policy and value into objcassociety to facilitate unified processing at the bottom level
ObjcAssociation association{policy, value};
// retain the new value (if any) outside the lock.
// Determine whether to retain or copy according to the policy
association.acquireValue(a);bool isFirstAssociation = false;
{ // Instantiate AssociationsManager note that this is not a singleton
AssociationsManager manager;
// instantiate the global AssociationsHashMap
AssociationsHashMap &associations(manager.get());
// If value has a value
if (value) {
//AssociationsHashMap: associative table ObjectAssociationMap: associative table of objects
// First find out if there are any object associated tables in the backup of the object encapsulated file
// Return the table directly if there is one, and create the object associated tables in 'backup' if there is not
When (The number of objects +1 is greater than or equal to 3/4, expand the capacity twice
auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
if (refs_result.second) {
/* it's the first association we make */
// The object is associated for the first time
isFirstAssociation = true;
}
/* establish or replace the association */
// Get the address of the value stored in ObjectAssociationMap
auto &refs = refs_result.first->second;
// Store the values that need to be stored in the address where the values are stored in the associated table
If 'result' = false, if 'result' = true '
// Create association type When the number of associations +1 exceeds 3/4, the system performs double capacity expansion
auto result = refs.try_emplace(key, std::move(association));
if(! result.second) {// Exchange 'association' with 'association'
// Replace the old value with the new value
association.swap(result.first->second); }}else {
//value does not have a value
// Find a backup of the corresponding ObjectAssociationMap
auto refs_it = associations.find(disguised);
// If the corresponding ObjectAssociationMap object association table is found
if(refs_it ! = associations.end()) {
Refs_it ->second contains association type data
auto &refs = refs_it->second;
// Query the association based on the key
auto it = refs.find(key);
if(it ! = refs.end()) {
// Update the old association value
// The old association is stored in the association
association.swap(it->second);
If value= nil, free 'association' stored in the associated object table
refs.erase(it);
// If all attribute data in the object association table is cleared, the object association table will be released
if (refs.size() = =0) {
associations.erase(refs_it);
}
}
}
}
}
SetHasAssociatedObjects is called every time the first associated object is called
// Set the 'has_assoc' property of 'isa pointer' to 'true' by using the setHasAssociatedObjects method to mark the presence of an associated object
if (isFirstAssociation)
object->setHasAssociatedObjects(a);// release the old value (outside of the lock).
// Release old values because any old values are swapped to 'association'
// The new value of 'association' is stored in the object association table
association.releaseHeldValue(a); }Copy the code
_object_set_associative_reference has two core functions
- According to the
object
Query in the associated tableObjectAssociationMap
If not, open up memory creationObjectAssociationMap
, the rules are created inThree quarters of
, perform double capacity expansion - Based on the
key
Noneassociation
It’s the associated datavalue
andpolicy
, if the query found directly update the data inside, if not to fetch emptyasociation
Type and then put the value in, ifObjectAssociationMap
Is the first time to disassociate data or the number of associated data is satisfiedThree quarters of
, perform double capacity expansion - Capacity expansion rules and
cache
Method stores the same rules
AssociationsManager Manager you might wonder why AssociationsHashMap & Associations is a singleton, Explore the AssociationsManager and AssociationsHashMap structures
class AssociationsManager {
using Storage = ExplicitInitDenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap>;
static Storage _mapStorage;
public:
// The constructor locks
AssociationsManager() { AssociationsManagerLock.lock(a); }// Destructor unlocks
~AssociationsManager() { AssociationsManagerLock.unlock(a); }// Get a global AssociationsHashMap
AssociationsHashMap &get(a) {
return _mapStorage.get(a); }static void init(a) {
_mapStorage.init();
}
};
Copy the code
Static Storage _mapStorage is a global static variable that can only be used inside the AssociationsManager. If you place it outside the AssociationsManager, the AssociationsManager has a constructor and a destructor. AssociationsManager is equivalent to calling the constructor lock function. When the scope of AssociationsManager ends, the destructor is automatically called to unlock the AssociationsManager. AssociationsManager is designed to prevent multithreaded access from being messy, and AssociationsHashMap is a global static variable fetch that is called only once
Verify whether AssociationsManager is a singleton
AssociationsManager is of class type. The MManager has not been initialized yet, but the Manager is of AssociationsManager type. And manager becomes AssociationsManager*, which is the pointer type, and manager stores the object type of AssociationsManager, The &manager, &Manager1, and &Manager2 addresses are different
Verify whether AssociationsHashMap & Associations is a singleton
obviouslyAssociationsHashMap &associations
Is a singleton, only singleton value open memory only one address, other Pointers are stored in this piece of address
try_emplace
methods
The following use breakpoint debugging with the flow of the way to explore
template <typename. Ts>std::pair<iterator, bool> try_emplace(const KeyT &Key, Ts &&... Args) {
BucketT *TheBucket;
// Find the corresponding Bucket according to the key
if (LookupBucketFor(Key, TheBucket))
// Use make_pair to generate the corresponding key-value pairs
return std::make_pair(
makeIterator(TheBucket, getBucketsEnd(), true),
false); //false indicates that the hash associated table already exists in a bucket
// If there is no query to insert data into bucket, return bucket
TheBucket = InsertIntoBucket(TheBucket, Key, std::forward<Ts>(Args)...) ;// Use make_pair to generate the corresponding key-value pairs
return std::make_pair(
makeIterator(TheBucket, getBucketsEnd(), true),
true);//true indicates that buckets are added to the hash association table for the first time
}
Copy the code
- First go according to
key
Let’s see if there’s a matchbucket
, if there will bebucket
Package return - If not, go to find empty
bucket
If no empty bucket exists, it will be createdbucket
And then save the databucket
In the
Args is an empty ObjectAssociationMap{} object association table
LookupBucketFor
To explore the
Enter the LookupBucketFor method and you’ll find two identical methods, source code below
The difference between these two methods is that the second argument, one with const modifier and the other without const modifier, is obviously the second call to the first method. And the second parameter value is a pointer type, also known as pointer pass.
- First obtained by key
bucket
Address depositConstFoundBucket
Pointer, and then theConstFoundBucket
The address of the pointer assigned to&FoundBucket
The pointer looks like thisFoundBucket
The stored data is updated in real time - Return if found
true
template<typename LookupKeyT>
bool LookupBucketFor(const LookupKeyT &Val,
const BucketT *&FoundBucket) const {
// Get buckets first address
const BucketT *BucketsPtr = getBuckets(a);// Get the current number of buckets
const unsigned NumBuckets = getNumBuckets(a);// NumBuckets = 0 returns false
if (NumBuckets == 0) {
FoundBucket = nullptr;
return false;
}
// FoundTombstone - Keep track of whether we find a tombstone while probing.
const BucketT *FoundTombstone = nullptr;
const KeyT EmptyKey = getEmptyKey(a);// Get the bucket key
const KeyT TombstoneKey = getTombstoneKey(a);assert(! KeyInfoT::isEqual(Val, EmptyKey) && ! KeyInfoT::isEqual(Val, TombstoneKey) &&
"Empty/Tombstone value shouldn't be inserted into map!");
// Hash retrives the subscript just like in cache
unsigned BucketNo = getHashValue(Val) & (NumBuckets- 1);
unsigned ProbeAmt = 1;
// Do the while loop
while (true) {
ThisBucket = the first address + the number of bytes
const BucketT *ThisBucket = BucketsPtr + BucketNo;
// Found Val's bucket? If so, return it.
// If 'key' of 'bucket' is equal to 'Val', return the current bucket
if (LLVM_LIKELY(KeyInfoT::isEqual(Val, ThisBucket->getFirst()))) {
FoundBucket = ThisBucket;
return true;
}
// If we found an empty bucket, the key doesn't exist in the set.
// Insert it and return the default value.
// An empty bucket can be inserted into an empty bucket
if (LLVM_LIKELY(KeyInfoT::isEqual(ThisBucket->getFirst(), EmptyKey))) {
// If we've already seen a tombstone while probing, fill it in instead
// of the empty bucket we eventually probed to.
FoundBucket = FoundTombstone ? FoundTombstone : ThisBucket;
return false;
}
// If this is a tombstone, remember it. If Val ends up not in the map, we
// prefer to return it than something that would require more probing.
// Ditto for zero values.
if (KeyInfoT::isEqual(ThisBucket->getFirst(), TombstoneKey) && ! FoundTombstone) FoundTombstone = ThisBucket;// Remember the first tombstone found.
if (ValueInfoT::isPurgeable(ThisBucket->getSecond() &&! FoundTombstone) FoundTombstone = ThisBucket;// Otherwise, it's a hash collision or a tombstone, continue quadratic
// probing.
if (ProbeAmt > NumBuckets) {
FatalCorruptHashTables(BucketsPtr, NumBuckets);
}
// BucketNo ++
BucketNo += ProbeAmt++;
// Get the subscript in hash
BucketNo &= (NumBuckets- 1); }}Copy the code
In LookupBucketFor breakpoint under debugging source code as follows
Because it’s the first associated object that hasn’t created the associated object table yet, so the address is empty, and there’s no bucket, so return false FoundBucket = nil
InsertIntoBucket sets a breakpoint in the try_emplace method to see if writing TheBucket is nil
TheBucket is indeed nil, so let’s go into the insert process
InsertIntoBucket
Btemplate <typename KeyArg, typename. ValueArgs>BucketT *InsertIntoBucket(BucketT *TheBucket, KeyArg &&Key, ValueArgs &&... Values) {
// Get an empty bucket
TheBucket = InsertIntoBucketImpl(Key, Key, TheBucket);
// insert the key into TheBucket
TheBucket->getFirst() = std::forward<KeyArg>(Key);
// insert value into key second in TheBucket: :new (&TheBucket->getSecond()) ValueT(std::forward<ValueArgs>(Values)...) ;return TheBucket;
}
Copy the code
The core function retrieves an empty bucket and then performs a key-value matching store to insert the key and value into the bucket
InsertIntoBucketImpl
template <typename LookupKeyT>
BucketT *InsertIntoBucketImpl(const KeyT &Key, const LookupKeyT &Lookup,
BucketT *TheBucket) {
// NewNumEntries indicate that a bucket will be inserted
unsigned NewNumEntries = getNumEntries() + 1;
// Get the total number of buckets
unsigned NumBuckets = getNumBuckets(a);// If the number to be inserted is greater than or equal to 3/4 of the total number, double the capacity
if (LLVM_UNLIKELY(NewNumEntries * 4 >= NumBuckets * 3)) {
this->grow(NumBuckets * 2);// Bucket = 0; // Bucket = 0; // Bucket = 0
LookupBucketFor(Lookup, TheBucket);
NumBuckets = getNumBuckets(a); }else if (LLVM_UNLIKELY(NumBuckets-(NewNumEntries+getNumTombstones()) <=
NumBuckets/8)) {
// The load is less than 1/8
this->grow(NumBuckets);
LookupBucketFor(Lookup, TheBucket);
}
ASSERT(TheBucket);
//
if (KeyInfoT::isEqual(TheBucket->getFirst(), getEmptyKey())) {
// Replacing an empty bucket.
// The number of buckets currently occupied + 1
incrementNumEntries(a); }else if (KeyInfoT::isEqual(TheBucket->getFirst(), getTombstoneKey())) {
// Replacing a tombstone.
incrementNumEntries(a);decrementNumTombstones(a); }else {
// we should be purging a zero. No accounting changes.
ASSERT(ValueInfoT::isPurgeable(TheBucket->getSecond()));
TheBucket->getSecond(). ~ValueT(a); }return TheBucket;
}
Copy the code
The core function is to open memory and get an empty bucket. Open memory is this->grow(NumBuckets), and get an empty bucket by LookupBucketFor, which is a breakpoint under this->grow(NumBuckets)
Enter the double capacity expansion process grow(NumBuckets * 2). Follow the breakpoint process to enter the grow method
void grow(unsigned AtLeast) {
static_cast<DerivedT *>(this) - >grow(AtLeast);
}
Copy the code
Breakpoint into grow(unsigned AtLeast) method
#define MIN_BUCKETS 4
void grow(unsigned AtLeast) {
unsigned OldNumBuckets = NumBuckets;
BucketT *OldBuckets = Buckets;
// MIN_BUCKETS = 4
allocateBuckets(std::max<unsigned>(MIN_BUCKETS, static_cast<unsigned> (NextPowerOf2(AtLeast- 1))));
ASSERT(Buckets);
if(! OldBuckets) {this->BaseT::initEmpty(a);return;
}
this->moveFromOldBuckets(OldBuckets, OldBuckets+OldNumBuckets);
// Free the old table.
operator delete(OldBuckets);
}
Copy the code
allocateBuckets
The method is based onbucket
The type andNum
Number of open up to open up memory spacebucket
The number of isMIN_BUCKETS
和NextPowerOf2(AtLeast-1))
Take the maximum.MIN_BUCKETS
Is a macro with a value of zero4
BaseT::initEmpty()
Initialize emptybucket
Specific type basisT
Note the type of empty bucketkey
forfirst
The corresponding value is theta1
moveFromOldBuckets
Will the oldbuckets
Move to newbuckets
Phi, this sumcache
Expansion is different- The release of the old
buckets
Breakpoint into NextPowerOf2 method
/ / 32 bits
NextPowerOf2 - returns the NextPowerOf2 (32 bits)
/// strictly greater than A. Returns zero on overflow.
inline uint32_t NextPowerOf2(uint32_t A) {
A |= (A >> 1);
A |= (A >> 2);
A |= (A >> 4);
A |= (A >> 8);
A |= (A >> 16);
return A + 1;
}
/ / 64
NextPowerOf2 - returns the NextPowerOf2 (64 bits)
/// strictly greater than A. Returns zero on overflow.
inline uint64_t NextPowerOf2(uint64_t A) {
A |= (A >> 1);
A |= (A >> 2);
A |= (A >> 4);
A |= (A >> 8);
A |= (A >> 16);
A |= (A >> 32);
return A + 1;
}
Copy the code
The breakpoint in macOS is the Uint32_t NextPowerOf2 method, which is strictly greater than A and returns 0 if it is greater. And parameter A is an unsigned integer. The current parameter passed is -1. The rule for converting -1 to binary is: 1= 0x01, reverse =0xfe, plus 1= 0xFF, and if it is 32 bits, the binary of -1 is 0xFFFFFFFF. If it is 64-1 is A binary 0 XFFFFFFFFFFFFFFFF, on A | = (A > > n), is equal to itself, then A + 1 overflow is equal to zero, of course, you can also be interpreted as 1 + 1 = 0, because is two times the capacity, and also between binary 2 times, So as long as it’s within the range it’s going to be equal to the value that’s passed in
Breakpoint into initEmpty
void initEmpty(a) {
setNumEntries(0);
setNumTombstones(0);
ASSERT((getNumBuckets() & (getNumBuckets(a)- 1)) = =0 &&
"# initial buckets must be a power of two!");
// Set an empty key
const KeyT EmptyKey = getEmptyKey(a);for (BucketT *B = getBuckets(), *E = getBucketsEnd(a); B ! = E; ++B) ::new (&B->getFirst()) KeyT(EmptyKey);
}
Copy the code
The breakpoint goes into getEmptyKey, getEmptyKey method and there are a number of methods that come in at the end as follows
static inline DisguisedPtr<T> getEmptyKey(a) {
return DisguisedPtr<T>((T*)(uintptr_t)- 1);
}
Copy the code
DisguisedPtr Everyone should be familiar with the Object is also encapsulated into the DisguisedPtr type
template* * (typename T>
class DisguisedPtr {
uintptr_t value;
static uintptr_t disguise(T* ptr) {
return- (uintptr_t)ptr; }... }Copy the code
PTR = (uintptr_t)-1, uintptr_t) PTR Uintptr_t (uintptr_t)-1) = 1 uintptr_t = 1
After capacity expansion, call LookupBucketFor(Lookup, TheBucket). A Lookup is a wrapped object passed in as a parameter
Calling LookupBucketFor returns an empty bucket. It is one of buckets created after expansion. The total number of buckets is four
Breakpoints continue debugging by entering the incrementNumEntries() method
The incrementNumEntries() method gets the current number of buckets, 0 and 1
void incrementNumEntries(a) {
// getNumEntries() is the original number of entries
// getNumEntries() +1 indicates the number to be inserted
// Set the number of NumEntries
setNumEntries(getNumEntries() + 1);
}
Copy the code
The source code shows that the number of existing NumEntries is increased by one. IncrementNumEntries () returns bucket after execution
TheBucket
thefirst
The assignment
// Get the first key in the key value
KeyT &getFirst(a) { return std::pair<KeyT, ValueT>::first; }
Copy the code
TheBucket->getFirst() = STD ::forward
(Key) assign Key to first in TheBucket
Assign Key to first of TheBucket
TheBucket
thesecond
The assignment
Second of TheBucket may have dirty data by default. Assigning value to second successfully returns TheBucket wrapped in make_pair
isFirstAssociation
First association
Secound = true is assigned to the second argument in the make_pair method with true, which means that the object is associated for the first time
To obtainObjectAssociationMap
Obtain the ObjectAssociationMap in the AssociationsHashMap table in backup (refs_result.first->second)
Insert association data or update existing data in ObjectAssociationMap
Result. second = true indicates that the data is not stored in ObjectAssociationMap for the first time. If result.second = false indicates that the data is present in ObjectAssociationMap for further details. The first property stored at this point is lw_name in the LWPerson class, and the value stored is luwenhan
The structure of ObjectAssociationMap is similar to that of cache. It can store values as well as other variables
Set up theisa
Is the associated object property
.if (isFirstAssociation)
object->setHasAssociatedObjects()
...
Copy the code
The setHasAssociatedObjects method is called when isFirstAssociation first sets the value of the associated object
objc_object::setHasAssociatedObjects()
{
if (isTaggedPointer()) return; .isa_t newisa, oldisa = LoadExclusive(&isa.bits);
do {
newisa = oldisa;
if(! newisa.nonpointer || newisa.has_assoc) {ClearExclusive(&isa.bits);
return;
}
// Set has_assoc in ISA to true
newisa.has_assoc = true;
} while (slowpath(!StoreExclusive(&isa.bits, &oldisa.bits, newisa.bits)));
}
Copy the code
The setHasAssociatedObjects method sets the object to have an associated object, that is, the has_assoc property of the ISA pointer is set to true and the old value is released by the releaseHeldValue method
Flow chart of associating object storage
Associated object –get
id
objc_getAssociatedObject(id object, const void *key)
{
return _object_get_associative_reference(object, key);
}
Copy the code
Objc_getAssociatedObject calls _object_get_associative_reference. In the _object_get_associative_reference method, the associated object value is relatively easy to look up the table
id
_object_get_associative_reference(id object, const void *key)
{
// Create an empty associated object
ObjcAssociation association{};
{ // Create a management class
AssociationsManager manager;
// Get the globally unique HashMap table
AssociationsHashMap &associations(manager.get());
// Iterator is an iterator that actually finds objc_Object and its corresponding ObjectAssociationMap
{first = objc_object,second =ObjectAssociationMap
AssociationsHashMap::iterator i = associations.find((objc_object *)object);
// If the iterator encapsulates a structure that is not the last to enter the judgment flow
if(i ! = associations.end()) {
/ / get ObjectAssociationMap
ObjectAssociationMap &refs = i->second;
// Get the iterator of ObjectAssociationMap
ObjectAssociationMap::iterator j = refs.find(key);
// If the iterator encapsulates a structure that is not the last to enter the judgment flow
if(j ! = refs.end()) {
/ / for association
association = j->second;
/ / retain new values
association.retainReturnedValue(a); }}}//release old value, return new value
return association.autoreleaseReturnedValue(a); }Copy the code
Comments have been written very clearly, do not repeat again, direct debugging to see the final result
Associative objects are simply a process of looking up the value of a table and returning the stored value. Compared with the associated object store value, the value process is easier to understand
conclusion
We may think that the underlying exploration of the associated object is not commonly used, there is no need to explore, in fact, you will have a deep understanding of apple’s underlying packaging ideas and hash tables during the exploration process