Classの構造およびメソッドキャッシュ

9583 ワード

runtimeソースコードから見たClassの構造は次の通りです.
struct objc_class : objc_object {
    // Class ISA;
    Class superclass; 
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

    class_rw_t *data() { 
        return bits.data();
    }
    ......
}

// bits.data();
class_rw_t* data() {
   return (class_rw_t *)(bits & FAST_DATA_MASK);
}

次のフィールドをそれぞれ説明します.
  • superclass:親へのポインタ
  • cache:呼び出されたメソッドキャッシュ
  • bits:特定のクラス情報を取得するための
  • class_rw_t:クラス具体情報の構造体は、bits&FAST_DATA_MASKは
  • を得た
    次にclassを見てみましょうrw_tにはどのような情報が含まれていますか、class_rw_t構造は以下の通りである.
    struct class_rw_t {
        uint32_t flags;
        uint32_t version;
        const class_ro_t *ro;        // 
        method_array_t methods;      // 
        property_array_t properties; // 
        protocol_array_t protocols;  //  
        Class firstSubclass;
        Class nextSiblingClass;
        char *demangledName;
        ......
    };
    

    class_rw_tにはメソッド、プロパティ、プロトコルなどが含まれており、roがあります.このroはclass_を指します.ro_tオブジェクト、class_ro_tにはクラス初期の情報が含まれており,読み取り専用である.
    class_ro_tの構成は以下の通りである.
    struct class_ro_t {
        uint32_t flags;
        uint32_t instanceStart;
        uint32_t instanceSize; //instance 
    #ifdef __LP64__
        uint32_t reserved;
    #endif
        const uint8_t * ivarLayout;
        const char * name;              // 
        method_list_t * baseMethodList; // 
        protocol_list_t * baseProtocols; // 
        const ivar_list_t * ivars;       //  
        const uint8_t * weakIvarLayout;
        property_list_t *baseProperties; // 
        method_list_t *baseMethods() const {
            return baseMethodList;
        }
    };
    

    class_rw_t中のmethods、properties、protocolsは2次元配列であり、クラスの初期および分類の方法、属性、プロトコルを含む読み書き可能である.最初はclassがなかったrw_t,class_rw_tは実行時に作成されclass_ro_tの内容と分類の内容が追加されます.
    上記の結論は、次のruntimeのソースコードから見ることができます.一部のコードを削除し、上記のフローのみを保持します.
    /***********************************************************************
    * realizeClass
    * Performs first-time initialization on class cls, 
    * including allocating its read-write data.
    * Returns the real class structure for the class. 
    * Locking: runtimeLock must be write-locked by the caller
    **********************************************************************/
    static Class realizeClass(Class cls)
    {
        runtimeLock.assertWriting();
    
        const class_ro_t *ro;
        class_rw_t *rw;
        Class supercls;
        Class metacls;
        bool isMeta;
    
        if (!cls) return nil;
        if (cls->isRealized()) return cls;
        assert(cls == remapClass(cls));
        ro = (const class_ro_t *)cls->data(); // bits ro 
        if (ro->flags & RO_FUTURE) { 
            // This was a future class. rw data is already allocated.
            rw = cls->data();
            ro = cls->data()->ro;
            cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
        } else {
            // Normal class. Allocate writeable class data.
            rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
            rw->ro = ro;
            rw->flags = RW_REALIZED|RW_REALIZING;
            cls->setData(rw); //  rw ro rw ro, rw cls bits
        }
        // Attach categories
        methodizeClass(cls);
    
        return cls;
    }
    

    上記のソースコード注釈から、この関数はクラスの最初の初期化時に実行され、最初はrwがなく、classのbitsはroを指していることがわかります.rwはroをrwに付与するroを作成し、clsにrwを付与するbitsを作成し、最後に注釈から分類を処理する内容であることがわかる.
    関数methodizeClassのソースコードは次のとおりです.
    static void methodizeClass(Class cls)
    {
        runtimeLock.assertWriting();
    
        bool isMeta = cls->isMetaClass();
        auto rw = cls->data();
        auto ro = rw->ro;
    
        // Methodizing for the first time
        if (PrintConnecting) {
            _objc_inform("CLASS: methodizing class '%s' %s", 
                         cls->nameForLogging(), isMeta ? "(meta)" : "");
        }
    
        // Install methods and properties that the class implements itself.
        method_list_t *list = ro->baseMethods();
        if (list) {
            prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
            rw->methods.attachLists(&list, 1);
        }
    
        property_list_t *proplist = ro->baseProperties;
        if (proplist) {
            rw->properties.attachLists(&proplist, 1);
        }
    
        protocol_list_t *protolist = ro->baseProtocols;
        if (protolist) {
            rw->protocols.attachLists(&protolist, 1);
        }
    
        // Root classes get bonus method implementations if they don't have 
        // them already. These apply before category replacements.
        if (cls->isRootMetaclass()) {
            // root metaclass
            addMethod(cls, SEL_initialize, (IMP)&objc_noop_imp, "", NO);
        }
    
        // Attach categories.
        category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
        attachCategories(cls, cats, false /*don't flush caches*/);
    
    }
    

    上記のソースコードから分かるように、rwからroを見つけて中のbaseMethods、baseProperties、baseProtocolsを取り出し、rw対応のmethods、properties、protocolsに追加します.最後に追加されていない分類内容を取り出して追加します.分類メソッドを追加する方法、すなわちattachCategories関数の具体的な実装については、AboutCategoryを参照してください.
    メソッドのキャッシュ
    メソッドの呼び出しは、毎回クラス、親クラス、メタクラスの検索効率が低い場合、runtimeで呼び出されたメソッドをキャッシュし、クラスのcacheに格納します.
    メソッドをキャッシュする前に、メソッドの最下位構造を理解します.
    struct method_t {
        SEL name;          // 
        const char *types; // ( 、 )
        IMP imp;           // ( )
    };
    
  • imp:関数の具体的な実装
  • typedef id (*IMP)(id, SEL, ...); 
    
  • SEL:メソッド関数名を表し、メソッドアドレッサとも呼ばれ、下位構造はchar*と似ています.
  • types:戻り値とパラメータ符号化を含む文字列iOSは@encodeの命令を提供し、具体的なタイプを文字列符号化として表すことができる.下表(一部code)
  • を参照.
    code
    Meaning
    c
    A char
    i
    An int
    s
    A short
    l
    A long
    q
    A long long
    c
    An unsigned char
    I
    An unsigned int
    S
    An unsigned short
    L
    An unsigned long
    Q
    An unsigned long long
    f
    A float
    d
    A double
    B
    A C++ bool or a C99 _Bool
    V
    A void
    *
    A charactor string(char *)
    @
    An object(whether statically typed or typed id)
    :
    A method selector(SEL)
    ^type
    A pointer to type
    例を挙げるとtypes="i20@0:8 i 16",OCのメソッドはidタイプのselfとSELにデフォルトで入力され,次の意味を表す.
    戻り値のタイプ
    パラメータの全長
    パラメータ1タイプおよび開始位置
    パラメータ2タイプおよび開始位置
    パラメータ3タイプおよび開始位置
    intタイプ
    20
    idタイプ
    SEL
    intタイプ
    キャッシュcache_を見てみましょうtの構造:
    struct cache_t {
        struct bucket_t *_buckets; // , 
        mask_t _mask; //   - 1
        mask_t _occupied; // 
    }
    struct bucket_t {
        cache_key_t _key;  // SEL key
        IMP _imp; // 
    }
    

    Classのメソッドキャッシュ(cache_t)はハッシュテーブルで実現され,メソッドの検索効率を向上させることができる.
    ハッシュテーブルとも呼ばれるハッシュテーブル(Hash table)は、キー値(Key value)に基づいて直接アクセスするデータ構造である.つまり、キー値をテーブルの1つの場所にマッピングすることでレコードにアクセスし、検索を高速化します.このマッピング関数をハッシュ関数と呼び,記録を格納する配列をハッシュリストと呼ぶ.与えられたテーブルMには、関数f(key)が存在し、任意の与えられたキーワード値keyに対して、関数を代入した後、そのキーワードを含むテーブルに記録されたアドレスを得ることができれば、テーブルMをハッシュ(Hash)テーブル、関数f(key)をハッシュ(Hash)関数と呼ぶ.
    一般的なハッシュテーブルは、ターゲットkey&または%前の値によって、ハッシュテーブルの位置の下付きラベルを得る.クラスメソッドキャッシュはメソッドSELをkeyとして使用します&maskは位置の下付きラベルを得て、メソッドアドレスをハッシュテーブルに格納する.ハッシュ・テーブルの下付きラベルは0から始まり、最大ハッシュ・テーブルの長さを1つ減らします.これも_maskサイズがハッシュテーブルの長さが1つ減少した理由.
    メソッドキャッシュ関数のソースコードを次に示します.
    static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
    {
        cacheUpdateLock.assertLocked();
        // Never cache before +initialize is done
        if (!cls->isInitialized()) return;
    
        // Make sure the entry wasn't added to the cache by some other thread 
        // before we grabbed the cacheUpdateLock.
        if (cache_getImp(cls, sel)) return;
        cache_t *cache = getCache(cls);
        cache_key_t key = getKey(sel);
    
        // Use the cache as-is if it is less than 3/4 full
        mask_t newOccupied = cache->occupied() + 1;
        mask_t capacity = cache->capacity();
        if (cache->isConstantEmptyCache()) {
            // Cache is read-only. Replace it.
            cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
        }
        else if (newOccupied <= capacity / 4 * 3) {
            // Cache is less than 3/4 full. Use it as-is.
            //  3/4 
        }
        else {
            // Cache is too full. Expand it.
            cache->expand();
        }
        // Scan for the first unused slot and insert there.
        // There is guaranteed to be an empty slot because the 
        // minimum size is 4 and we resized at 3/4 full.
        // , 。 4 3/4 , 
        bucket_t *bucket = cache->find(key, receiver);
        if (bucket->key() == 0) cache->incrementOccupied();//  _occupied 
        bucket->set(key, imp);
    }
    

    上記のソースコードから、ハッシュリストの最小長さが4であり、3/4容量に達したときに拡張され、テーブルに必ず挿入可能な位置があることがわかる.挿入前に、メソッドがテーブルに格納されているかどうかを検索します.保存されていません.occupiedに1を追加します.
    キャッシュメソッドを検索する関数を見てみましょう.
    bucket_t * cache_t::find(cache_key_t k, id receiver)
    {
        assert(k != 0);
    
        bucket_t *b = buckets();
        mask_t m = mask();
        mask_t begin = cache_hash(k, m); //key & mask 
        mask_t i = begin;
        do {
            if (b[i].key() == 0  ||  b[i].key() == k) {
                return &b[i];
            }
        } while ((i = cache_next(i, m)) != begin);
    
        // hack
        Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
        cache_t::bad_cache(receiver, (SEL)k, cls);
    }
    
    // arm64 cache_next
    static inline mask_t cache_next(mask_t i, mask_t mask) {
        return i ? i-1 : mask;
    }
    

    異なるkey&maskで得られる下付き文字は同じである可能性があるため、key&maskで初期下付き文字を得た後、ハッシュリストの下付き文字の対を持つbucketオブジェクトを手に入れてkeyをターゲットkeyと比較し、等しい表示が私たちが望むbucketである.異なる場合はi-1の位置をループし、i=0の場合はmaskの位置のbucketが望ましいかどうかを確認します.遍歴した後にまだ正しいものが見つからない場合は、いくつかのエラー処理をして、具体的にcache_を検索します.t::bad_Cache関数を表示します.