cache exploration and analysis

Posted by treilad on Tue, 25 Jan 2022 01:36:34 +0100

I cache_t structure analysis

First, attach a cache_t principle analysis diagram

1.cache_t source code analysis

First, take a look at the general structure of cache in the source code

struct cache_t {
private:
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask; // 8
    union {
        struct {
            explicit_atomic<mask_t>    _maybeMask; // 4
#if __LP64__
            uint16_t                   _flags;  // 2
#endif
            uint16_t                   _occupied; // 2
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache; // 8
    };
    //Partial code

Try through the rough analysis of the source code and print through LLDB mode. The code is as follows

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        LGPerson *p  = [LGPerson alloc];
        Class pClass = [LGPerson class];
        [p saySomething];
        NSLog(@"%@",pClass);
        

    }
    return 0;
}

Start breakpoint debugging

//Get cache_ Tinternal properties
(lldb) p/x pClass
(Class) $0 = 0x0000000100004518 LGPerson
(lldb) x/4gx $0
0x100004518: 0x0000000100004540 0x0000000100353140
0x100004528: 0x000000010034b380 0x0000802800000000

(lldb) p (cache_t *)0x000000010034b380
(cache_t *) $2 = 0x000000010034b380
(lldb) p *$2
(cache_t) $3 = {
  _bucketsAndMaybeMask = {
    std::__1::atomic<unsigned long> = {
      Value = 0
    }
  }
   = {
     = {
      _maybeMask = {
        std::__1::atomic<unsigned int> = {
          Value = 0
        }
      }
      _flags = 0
      _occupied = 0
    }
    _originalPreoptCache = {
      std::__1::atomic<preopt_cache_t *> = {
        Value = 0x0000000000000000
      }
    }
  }
}
(lldb) 

Through the above debugging, it seems that there is no useful data, so you can only continue to check the source code to see if there is any method to obtain the desired data. Refer to the previous analysis

    static constexpr uintptr_t bucketsMask = ~0ul;
    static_assert(!CONFIG_USE_PREOPT_CACHES, "preoptimized caches not supported");
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
    static constexpr uintptr_t maskShift = 48;
    static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1;//translation
    static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << maskShift) - 1;
    
    static_assert(bucketsMask >= MACH_VM_MAX_ADDRESS, "Bucket field doesn't have enough bits for arbitrary pointers.");
#if CONFIG_USE_PREOPT_CACHES
    static constexpr uintptr_t preoptBucketsMarker = 1ul;
    static constexpr uintptr_t preoptBucketsMask = bucketsMask & ~preoptBucketsMarker;
#endif
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    // _bucketsAndMaybeMask is a buckets_t pointer in the low 48 bits
    // _maybeMask is unused, the mask is stored in the top 16 bits.

    // How much the mask is shifted by.
    static constexpr uintptr_t maskShift = 48;

    // Additional bits after the mask which must be zero. msgSend
    // takes advantage of these additional bits to construct the value
    // `mask << 4` from `_maskAndBuckets` in a single instruction.
    static constexpr uintptr_t maskZeroBits = 4;

    // The largest mask value we can store.
    static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1;
    
    // The mask applied to `_maskAndBuckets` to retrieve the buckets pointer.
    static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << (maskShift - maskZeroBits)) - 1;
    
    // Ensure we have enough bits for the buckets pointer.
    static_assert(bucketsMask >= MACH_VM_MAX_ADDRESS,
            "Bucket field doesn't have enough bits for arbitrary pointers.");

#if CONFIG_USE_PREOPT_CACHES
    static constexpr uintptr_t preoptBucketsMarker = 1ul;
#if __has_feature(ptrauth_calls)
	//Note means to indicate what 64 bits in memory represent respectively. Refer to the previous isa memory analysis
    // 63..60: hash_mask_shift
    // 59..55: hash_shift
    // 54.. 1: buckets ptr + auth
    //      0: always 1
    static constexpr uintptr_t preoptBucketsMask = 0x007ffffffffffffe;
    static inline uintptr_t preoptBucketsHashParams(const preopt_cache_t *cache) {
        uintptr_t value = (uintptr_t)cache->shift << 55;
        // masks have 11 bits but can be 0, so we compute
        // the right shift for 0x7fff rather than 0xffff
        return value | ((objc::mask16ShiftBits(cache->mask) - 1) << 60);
    }

Judging from this source code, it is found that cache_ It seems that some data in the structure of t can also be obtained through memory translation

    void reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld);
    void collect_free(bucket_t *oldBuckets, mask_t oldCapacity);

    static bucket_t *emptyBuckets();
    static bucket_t *allocateBuckets(mask_t newCapacity);
    static bucket_t *emptyBucketsForCapacity(mask_t capacity, bool allocate = true);
    static struct bucket_t * endMarker(struct bucket_t *b, uint32_t cap);
    void bad_cache(id receiver, SEL sel) __attribute__((noreturn, cold));

public:
    // The following four fields are public for objcdt's use only.
    // objcdt reaches into fields while the process is suspended
    // hence doesn't care for locks and pesky little details like this
    // and can safely use these.
    unsigned capacity() const;
    struct bucket_t *buckets() const;//Structure pointer
    Class cls() const;

Discovery bucket_t is used very frequently, so click in to have a look

struct bucket_t {
private:
    // IMP-first is better for arm64e ptrauth and no worse for arm64.
    // SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
    explicit_atomic<uintptr_t> _imp;
    explicit_atomic<SEL> _sel;
#else
    explicit_atomic<SEL> _sel;
    explicit_atomic<uintptr_t> _imp;
#endif

    // Compute the ptrauth signing modifier from &_imp, newSel, and cls.
    uintptr_t modifierForSEL(bucket_t *base, SEL newSel, Class cls) const {
        return (uintptr_t)base ^ (uintptr_t)newSel ^ (uintptr_t)cls;
    }

Finally found the data we are interested in_ imp,_sel, and each sel corresponds to an imp, then continue debugging through LLDB

2.LLDB debugging and analysis cache_t

Through cache_ For memory translation of buckets() in T, you must first understand that buckets() is not an array. Through the previous structural analysis, it is a structure pointer. Naturally, the translation here is also carried out in the unit of (bucket_t *) to obtain another bucket_ T's memory address, and each structure bucket_ There are corresponding in t_ imp,_sel, the results are as follows

//Find cache_ Tinternal method
(lldb) p/x pClass
(Class) $0 = 0x0000000100004500 LGPerson
(lldb) p (cache_t *)0x0000000100004510
(cache_t *) $1 = 0x0000000100004510
(lldb) p *$1
(cache_t) $2 = {
  _bucketsAndMaybeMask = {
    std::__1::atomic<unsigned long> = {
      Value = 4301545824
    }
  }
   = {
     = {
      _maybeMask = {
        std::__1::atomic<unsigned int> = {
          Value = 3
        }
      }
      _flags = 32808
      _occupied = 1
    }
    _originalPreoptCache = {
      std::__1::atomic<preopt_cache_t *> = {
        Value = 0x0001802800000003
      }
    }
  }
}
(lldb) p $2.buckets()[0]//Memory translation. buckets() here is not an array
(bucket_t) $3 = {
  _sel = {
    std::__1::atomic<objc_selector *> = (null) {
      Value = (null)
    }
  }
  _imp = {
    std::__1::atomic<unsigned long> = {
      Value = 0
    }
  }
}
(lldb) p $2.buckets()[1]
(bucket_t) $4 = {
  _sel = {
    std::__1::atomic<objc_selector *> = (null) {
      Value = (null)
    }
  }
  _imp = {
    std::__1::atomic<unsigned long> = {
      Value = 0
    }
  }
}
(lldb) p $2.buckets()[2]
(bucket_t) $5 = {
  _sel = {
    std::__1::atomic<objc_selector *> = "" {
      Value = ""
    }
  }
  _imp = {
    std::__1::atomic<unsigned long> = {
      Value = 30976
    }
  }
}

I believe you also found the problem, that is, print p $2 When buckets() [2], values have values, but not before. After you add and call more methods, the order of values is not orderly, which involves hash function and Zipper method In this aspect, you can check it by yourself. In short, the main purpose of this method is to speed up the efficiency of method insertion and reading, because if you use an array to access the method list, it will greatly affect the efficiency of insertion and reading hash function Each bucket address will have a corresponding hash value Zipper method Solve the conflict problem of hash algorithm to facilitate addition, deletion, modification and query

The problem now is how to get the sel and imp in buckets and continue the source code

 inline SEL sel() const { return _sel.load(memory_order_relaxed); }
 inline IMP imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls) const {

From the source code, you can see that you can call the corresponding function

(lldb) p $6.imp(nil,pClass)
(IMP) $7 = 0x0000000100003c30 (KCObjcBuild`-[LGPerson sayNoBB])
(lldb) p $6.sel()
(SEL) $8 = "sayNoBB"

II cache_ Analysis of source code structure

1. Why break away from source code analysis

I believe that through these days of study, we will find that various data in the source code, such as the structure, will be mixed with many static variables, some environmental judgments, or the definition and implementation of functions, so that we will be confused and disturbed in the viewing process. If we remove the code we don't care about and concentrate on analyzing the content we want to explore, Is it easier to sort out the relationship? Then let's try it and summarize it as follows

  • The source code cannot be debugged
  • Complex structure relationship and cumbersome LLDB debugging
  • Small scale sampling (it will not be affected by other internal factors, so the understanding is simpler and clearer)

2. Source code analysis

First of all, we all know_ objc_class, the main member variable, we also define the following to simplify the code

// cache class
struct ycx_objc_class {
    Class isa;
    Class superclass;
    struct ycx_cache_t cache;             // formerly cache pointer and vtable
    struct ycx_class_data_bits_t bits;
};

And then_ class_data_bits_t

struct ycx_class_data_bits_t {
    uintptr_t bits;
};

Then cache_t

typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits

//struct bucket_t *cache_t::buckets() const
//{
//    uintptr_t addr = _bucketsAndMaybeMask.load(memory_order_relaxed);
//    return (bucket_t *)(addr & bucketsMask);
//}
//_ Buckets substitution_ bucketsAndMaybeMask
struct ycx_cache_t {
    struct ycx_bucket_t *_bukets; // 8
    mask_t    _maybeMask; // 4
    uint16_t  _flags;  // 2
    uint16_t  _occupied; // 2
};

Because we want to analyze the cache_t's bucket_t. Let's also define it

struct ycx_bucket_t {
    SEL _sel;
    IMP _imp;
};

Next, I will try the code. Of course, the following code is the result of many attempts combined with the source code

LGPerson *p  = [[LGPerson alloc]init];
Class pClass = p.class;  // objc_clas
[p say1];
[p say2];
[p say3];
[p say4];
[p say5];
[p say6];

[pClass sayHappy];
struct ycx_objc_class *ycx_class = (__bridge struct ycx_objc_class *)(pClass);
NSLog(@"%hu - %u",ycx_class->cache._occupied,ycx_class->cache._maybeMask);
        
for (mask_t i = 0; i<ycx_class->cache._maybeMask; i++) {
	struct ycx_bucket_t bucket = ycx_class->cache._bukets[i];
	NSLog(@"%@ - %pf",NSStringFromSelector(bucket._sel),bucket._imp);
}

The printing results are as follows

2021-06-24 15:55:12.950582+0800 003-cache_t Break away from source environment analysis[17820:270757] 5 - 7
2021-06-24 15:55:12.950650+0800 003-cache_t Break away from source environment analysis[17820:270757] say4 - 0x5910f
2021-06-24 15:55:12.950742+0800 003-cache_t Break away from source environment analysis[17820:270757] say6 - 0x59b0f
2021-06-24 15:55:12.950788+0800 003-cache_t Break away from source environment analysis[17820:270757] say3 - 0x5920f
2021-06-24 15:55:12.950854+0800 003-cache_t Break away from source environment analysis[17820:270757] (null) - 0x0f
2021-06-24 15:55:12.950914+0800 003-cache_t Break away from source environment analysis[17820:270757] say5 - 0x5940f
2021-06-24 15:55:12.950966+0800 003-cache_t Break away from source environment analysis[17820:270757] say2 - 0x58f0f
2021-06-24 15:55:12.950997+0800 003-cache_t Break away from source environment analysis[17820:270757] (null) - 0x0f

3. Several problems are found through multiple tests:

  • Why does it exist (null) - 0x0f
  • Where are the class methods
  • What does 5-7 mean
  • Why does a method call not print out

III Analyze the principle of cache inserting data

1.cache insertion

First, find the main process code of insert

void cache_t::insert(SEL sel, IMP imp, id receiver)
{
    runtimeLock.assertLocked();

    // Never cache before +initialize is done
    if (slowpath(!cls()->isInitialized())) {
        return;
    }

    if (isConstantOptimizedCache()) {
        _objc_fatal("cache_t::insert() called with a preoptimized cache for %s",
                    cls()->nameForLogging());
    }

#if DEBUG_TASK_THREADS
    return _collecting_in_critical();
#else
#if CONFIG_USE_CACHE_LOCK
    mutex_locker_t lock(cacheUpdateLock);
#endif

    ASSERT(sel != 0 && cls()->isInitialized());

    // Use the cache as-is if until we exceed our expected fill ratio.
    mask_t newOccupied = occupied() + 1; // 1+1
    unsigned oldCapacity = capacity(), capacity = oldCapacity;
    if (slowpath(isConstantEmptyCache())) {
        // Cache is read-only. Replace it.  The cache is read-only and can only be replaced
        if (!capacity) capacity = INIT_CACHE_SIZE;//4
        reallocate(oldCapacity, capacity, /* freeOld */false);
    }
    // CACHE_END_MARKER 1 is reserved for comparison calculation after + 1
    else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) {
        // Cache is less than 3/4 or 7/8 full. Use it as-is.
        //More than 75% capacity expansion
    }
#if CACHE_ALLOW_FULL_UTILIZATION
    else if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) {
        // Allow 100% cache utilization for small buckets. Use it as-is.
    }
#endif
    else { //Double capacity expansion in case of more than 75%
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        //true is passed here, indicating that the old buckets memory will be cleaned up in the case of capacity expansion
        reallocate(oldCapacity, capacity, true);
    }

    bucket_t *b = buckets();
    mask_t m = capacity - 1; // 4-1=3
    mask_t begin = cache_hash(sel, m);//hash address 
    mask_t i = begin;

    // Scan for the first unused slot and insert there.
    // There is guaranteed to be an empty slot.
    //Prevent hash conflict and cycle hash until it is different from the initial hash
    do {
        if (fastpath(b[i].sel() == 0)) {
            incrementOccupied();//_ occupied++;   To put it bluntly, insert a new bucket every time_ t  _ occupied++
            b[i].set<Atomic, Encoded>(b, sel, imp, cls());
            return;
        }
        if (b[i].sel() == sel) {
            // The entry was added to the cache by some other thread
            // before we grabbed the cacheUpdateLock.
            return;
        }
    } while (fastpath((i = cache_next(i, m)) != begin));

    bad_cache(receiver, (SEL)sel);
#endif // !DEBUG_TASK_THREADS
}

Reallocate cache function cache_t::reallocate code

void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld)
{
    bucket_t *oldBuckets = buckets();
    bucket_t *newBuckets = allocateBuckets(newCapacity);

    // Cache's old contents are not propagated. 
    // This is thought to save cache memory at the cost of extra cache fills.
    // fixme re-measure this
    
    //Cached old content is not propagated
    //This is considered to save cache memory at the cost of additional cache filling.
    //Measure again
    ASSERT(newCapacity > 0);
    ASSERT((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);
	//Set BucketsAndMask to insert empty container (bucket)
    setBucketsAndMask(newBuckets, newCapacity - 1);
    //Clear original memory
    if (freeOld) {
        collect_free(oldBuckets, oldCapacity);
    }
}

2. Analyze the source code and verify the cache insertion algorithm

Through the above source code analysis, the following conclusions are drawn

  • 1. Each bucket_ T (bucket) corresponds to a sel, imp memory address
  • 2. Storage bucket_ The base value of T (bucket) is capacity = INIT_CACHE_SIZE = 4
  • 3. Double capacity expansion if it exceeds 3 / 4 (capacity = capacity? Capacity * 2: init_cache_size)
  • 4. Each capacity expansion is to re open up new storage space (setBucketsAndMask(newBuckets, newCapacity - 1))
  • 5. Each capacity expansion will empty the original cache collection_ Free (oldbuckets, oldcapacity), restart storage;
  • 6. Each bucket_ T (bucket) addresses are hash function cache_hash(sel, m) is calculated (so printing will find that it is not orderly)

Next, let's verify:

1. Do not call any method first. You can see that class will be called by default


2. Supplementary method say1 can be found_ occupied is 2, no more than 3 / 4 of 4, that is, 3, so the printing method is say1, class


3. Continue to supplement the method say2. After more than 3 / 4, double the capacity expansion, and reduce 1 to 7. Then empty the old method, and only say2 is left


4. Let him expand the capacity again to determine whether our conclusion is correct

3. Final questions

Through the above calculation, I believe careful students have found a problem. It seems that the capacity expansion has started before 3 / 4 every time. For example, only adding method say1 and adding the default class will have two methods, but when you add say2, the capacity has been expanded, that is, 2-3 ((3 + 1) * 3 / 4). Then add five methods, and the first one is cleared, that is, 5-7 ((7 + 1) * 3 / 4), It seems that each expansion starts when it is 1 less than 3 / 4 of the maximum capacity. In order to confirm my conjecture, it is verified once

		//If the code is too long, intercept the key places
		[p say1];//2 (+class)
        
        [p say2];//First expansion
        [p say3];
        [p say4];
        [p say5];
        [p say6];//5
        
        [p say7];//Second expansion
        [p say8];
        [p say9];
        [p say10];
        [p say11];
        [p say12];
        [p say13];
        [p say14];
        [p say15];
        [p say16];
        [p say3];//11
        
        [p say4];//The third expansion

As a result, only one say4 was printed

2021-06-25 16:11:18.967561+0800 003-cache_t Break away from source environment analysis[18318:292158] LGPerson say : -[LGPerson say1]
2021-06-25 16:11:18.968502+0800 003-cache_t Break away from source environment analysis[18318:292158] LGPerson say : -[LGPerson say2]
2021-06-25 16:11:18.968550+0800 003-cache_t Break away from source environment analysis[18318:292158] LGPerson say : -[LGPerson say3]
2021-06-25 16:11:18.968577+0800 003-cache_t Break away from source environment analysis[18318:292158] LGPerson say : -[LGPerson say4]
2021-06-25 16:11:18.968599+0800 003-cache_t Break away from source environment analysis[18318:292158] LGPerson say : -[LGPerson say5]
2021-06-25 16:11:18.968620+0800 003-cache_t Break away from source environment analysis[18318:292158] LGPerson say : -[LGPerson say6]
2021-06-25 16:11:18.968641+0800 003-cache_t Break away from source environment analysis[18318:292158] LGPerson say : -[LGPerson say7]
2021-06-25 16:11:18.968662+0800 003-cache_t Break away from source environment analysis[18318:292158] LGPerson say : -[LGPerson say8]
2021-06-25 16:11:18.968683+0800 003-cache_t Break away from source environment analysis[18318:292158] LGPerson say : -[LGPerson say9]
2021-06-25 16:11:18.969202+0800 003-cache_t Break away from source environment analysis[18318:292158] LGPerson say : -[LGPerson say10]
2021-06-25 16:11:18.969246+0800 003-cache_t Break away from source environment analysis[18318:292158] LGPerson say : -[LGPerson say11]
2021-06-25 16:11:18.969282+0800 003-cache_t Break away from source environment analysis[18318:292158] LGPerson say : -[LGPerson say12]
2021-06-25 16:11:18.969306+0800 003-cache_t Break away from source environment analysis[18318:292158] LGPerson say : -[LGPerson say13]
2021-06-25 16:11:18.969327+0800 003-cache_t Break away from source environment analysis[18318:292158] LGPerson say : -[LGPerson say14]
2021-06-25 16:11:18.969348+0800 003-cache_t Break away from source environment analysis[18318:292158] LGPerson say : -[LGPerson say15]
2021-06-25 16:11:18.969375+0800 003-cache_t Break away from source environment analysis[18318:292158] LGPerson say : -[LGPerson say16]
2021-06-25 16:11:18.969396+0800 003-cache_t Break away from source environment analysis[18318:292158] LGPerson say : -[LGPerson say3]
2021-06-25 16:11:18.969468+0800 003-cache_t Break away from source environment analysis[18318:292158] LGPerson say : -[LGPerson say4]
2021-06-25 16:11:18.978601+0800 003-cache_t Break away from source environment analysis[18318:292158] 1 - 31
2021-06-25 16:11:18.978656+0800 003-cache_t Break away from source environment analysis[18318:292158] (null) - 0x0f
2021-06-25 16:11:18.978684+0800 003-cache_t Break away from source environment analysis[18318:292158] (null) - 0x0f
2021-06-25 16:11:18.978707+0800 003-cache_t Break away from source environment analysis[18318:292158] (null) - 0x0f
2021-06-25 16:11:18.978728+0800 003-cache_t Break away from source environment analysis[18318:292158] (null) - 0x0f
2021-06-25 16:11:18.978747+0800 003-cache_t Break away from source environment analysis[18318:292158] (null) - 0x0f
2021-06-25 16:11:18.978767+0800 003-cache_t Break away from source environment analysis[18318:292158] (null) - 0x0f
2021-06-25 16:11:18.978787+0800 003-cache_t Break away from source environment analysis[18318:292158] (null) - 0x0f
2021-06-25 16:11:18.978875+0800 003-cache_t Break away from source environment analysis[18318:292158] say4 - 0xc7e0f
2021-06-25 16:11:18.978905+0800 003-cache_t Break away from source environment analysis[18318:292158] (null) - 0x0f
2021-06-25 16:11:18.978926+0800 003-cache_t Break away from source environment analysis[18318:292158] (null) - 0x0f
2021-06-25 16:11:18.979386+0800 003-cache_t Break away from source environment analysis[18318:292158] (null) - 0x0f
2021-06-25 16:11:18.979411+0800 003-cache_t Break away from source environment analysis[18318:292158] (null) - 0x0f
2021-06-25 16:11:18.979432+0800 003-cache_t Break away from source environment analysis[18318:292158] (null) - 0x0f
2021-06-25 16:11:18.979451+0800 003-cache_t Break away from source environment analysis[18318:292158] (null) - 0x0f
2021-06-25 16:11:18.979576+0800 003-cache_t Break away from source environment analysis[18318:292158] (null) - 0x0f
2021-06-25 16:11:18.979601+0800 003-cache_t Break away from source environment analysis[18318:292158] (null) - 0x0f
2021-06-25 16:11:18.979783+0800 003-cache_t Break away from source environment analysis[18318:292158] (null) - 0x0f
2021-06-25 16:11:18.979843+0800 003-cache_t Break away from source environment analysis[18318:292158] (null) - 0x0f
2021-06-25 16:11:18.979958+0800 003-cache_t Break away from source environment analysis[18318:292158] (null) - 0x0f
2021-06-25 16:11:18.979991+0800 003-cache_t Break away from source environment analysis[18318:292158] (null) - 0x0f
2021-06-25 16:11:18.980014+0800 003-cache_t Break away from source environment analysis[18318:292158] (null) - 0x0f
2021-06-25 16:11:18.980035+0800 003-cache_t Break away from source environment analysis[18318:292158] (null) - 0x0f
2021-06-25 16:11:18.980076+0800 003-cache_t Break away from source environment analysis[18318:292158] (null) - 0x0f
2021-06-25 16:11:18.980161+0800 003-cache_t Break away from source environment analysis[18318:292158] (null) - 0x0f
2021-06-25 16:11:18.980339+0800 003-cache_t Break away from source environment analysis[18318:292158] (null) - 0x0f
2021-06-25 16:11:18.980471+0800 003-cache_t Break away from source environment analysis[18318:292158] (null) - 0x0f
2021-06-25 16:11:18.980755+0800 003-cache_t Break away from source environment analysis[18318:292158] (null) - 0x0f
2021-06-25 16:11:18.980797+0800 003-cache_t Break away from source environment analysis[18318:292158] (null) - 0x0f
2021-06-25 16:11:18.980822+0800 003-cache_t Break away from source environment analysis[18318:292158] (null) - 0x0f
2021-06-25 16:11:18.980844+0800 003-cache_t Break away from source environment analysis[18318:292158] (null) - 0x0f
2021-06-25 16:11:18.980864+0800 003-cache_t Break away from source environment analysis[18318:292158] (null) - 0x0f
2021-06-25 16:11:18.981074+0800 003-cache_t Break away from source environment analysis[18318:292158] Hello, World!
Program ended with exit code: 0

It can be seen from this that in fact, during each capacity expansion, a piece of memory will be occupied by default. As for what it is, don't worry. For more highlights, please listen to the next chapter!!!

Topics: iOS xcode Flutter objective-c