Whew Mantle

11444 ワード

What is Mantle
MantleはModelレイヤをシンプル化するサードパーティ製ライブラリです
Mantle effet
  • ModelとJSONのために
  • を書きたくない.
  • Modelがarchiveとunarchiveをサポートするために
  • を書きたくない
  • ModelサポートCopyのために
  • をたくさん書きたくない
  • モデルには不要なモデルがたくさん含まれているのを見たくありません.
  • ModelサポートMergeに苦労したくない
  • Modelにdescription
  • を書き換えたくない
    Have a try
    1.一つのグループの資料を模擬するModel
    @interface JCGroupProfile : MTLModel 
    @property (nonatomic, copy, readonly) NSString *gid;
    @property (nonatomic, copy, readonly) NSString *ownerID;
    @property (nonatomic, copy, readonly) NSString *name;
    @property (nonatomic, copy, readonly) NSString *sign;
    @property (nonatomic, strong, readonly) NSArray *photos;
    @property (nonatomic, strong, readonly) NSDate *createDate;
    @property (nonatomic, assign, readonly) NSUInteger *level;
    @property (nonatomic, assign, readonly) NSUInteger *memberCount;
    @property (nonatomic, assign, readonly) NSUInteger *memberMaxCount;
    @property (nonatomic, assign, readonly) BOOL isVip;
    @end
    

    2.ModelとJSONのKey値マッピング関係を記述するためにMTLJSOnSerializingプロトコルを実装する.あるフィールドの値がJSONの2次ノードの下にある場合、keypathで設定できます.例コードの@「ownerID」:@「owner_info.userid」+(NSDictionary*)JSOnKeyPathsByPropertyKey{return@{@「gid」:@「gid」@「gid」@「ownerID」:@「owner_info.userid」@「name」@「name」@「sign」@「sign」@「sign」@「photos」@「photos」@「createDate」:@「create_time」@「level」:@"level",@"memberCount":@"member_count",@"memberMaxCount":@"member_max_count", @"isVip": @"is_vip", }; }
    3.NSDateのような非常规型やカスタム型がある场合は、型の変换が必要です.
    + (NSValueTransformer *)JSONTransformerForKey:(NSString *)key
    {
        if ([key isEqualToString:@"createDate"]) {
            return [MTLValueTransformer transformerUsingForwardBlock:^id(id value, BOOL *success, NSError *__autoreleasing *error) {
                if (success && value && [value isKindOfClass:[NSNumber class]]) {
                    return [NSDate dateWithTimeIntervalSince1970:[(NSNumber *)value longValue]];
                }
                return nil;
            } reverseBlock:^id(id value, BOOL *success, NSError *__autoreleasing *error) {
                if (success && value && [value isKindOfClass:[NSDate class]]) {
                    return @([(NSDate *)value timeIntervalSince1970]);
                }
                return nil;
            }];
        }
        return nil;
    }
    

    4.実際に使用
    - (void)hanldeResponseDic:(NSDictionary *)jsonDic
    {
        // JSON Convert To Model
        JCGroupProfile *groupProfile = [MTLJSONAdapter modelOfClass:JCGroupProfile.class fromJSONDictionary:jsonDic error:nil];
    
        // support copy
        JCGroupProfile *groupProfileCopy = [groupProfile copy];
    
        // support merge
        [groupProfile mergeValuesForKeysFromModel:groupProfileCopy];
    
        // support compare two Model Obj
        BOOL isEqual = [groupProfile isEqual:groupProfileCopy];
    
        // support archive
        NSString *documentPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
        NSString *filePath = [documentPath stringByAppendingPathComponent:@"file.archiver"];
        [NSKeyedArchiver archiveRootObject:groupProfile toFile:filePath];
    
        // support description
        NSLog(@"groupProfile = %@", groupProfile);
    
        // Model Convert To JSON
        NSDictionary *jsonDicFromModel = [MTLJSONAdapter JSONDictionaryFromModel:groupProfile error:nil];
    }
    

    5.サブクラスシーンへの対応を試みる
    @interface JCVipGroupProfile : JCGroupProfile
    @property (nonatomic, assign, readonly) NSUInteger vipLevel;
    @property (nonatomic, copy, readonly) NSString *activity;
    @end
    

    サブクラスは、MTLJSONSerializingプロトコルにおけるキー値マッピング方法および特殊なタイプの変換方法を再実装すべきであり、一般的な方法は、superを呼び出し、selfを補充することである.
    + (NSDictionary *)JSONKeyPathsByPropertyKey
    {
        NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithDictionary:[super JSONKeyPathsByPropertyKey]];
        [dic setValuesForKeysWithDictionary:@{
                                              @"vipLevel" : @"vip_level",
                                              @"activity" : @"activity",
                                              }];
        return dic;
    }
    
    + (NSValueTransformer *)JSONTransformerForKey:(NSString *)key
    {
        if ([key isEqualToString:@"aPropertyKey"]) {
            //               
        }
        return [super JSONTransformerForKey:key];
    }
    

    6.クラスタシーンに対応してみます.クラスクラスタの定義を見てみましょう.
    クラスクラスタはFoundationフレームワークで広く使用されている設計モードである.クラスクラスタは、いくつかのプライベートで特定のサブクラスを共通の抽象的なスーパークラスの下に組み合わせ、この方法でクラスを組織することで、機能の豊富さを減らすことなく、オブジェクトフレームワーク向けの公開アーキテクチャを簡素化することができます.
    実はクラスタの簡単な理解は抽象工場です.グループ資料ベースクラスGroupProfileがあると仮定し,サブクラスにはGameGroupProfile,VipGroupProfile,SuperVipGroupProfile,ShopGroupProfileなどがあり,各サブクラス資料には独自に拡張されたフィールドがあり,この4つのサブクラスタタイプは互いに反発している.(ゲーム群であると同時に会員群であるはずがない)サーバがグループ資料を返すJSONを想定し,JSONをしてModelを回す際には,基本クラスでMTLJSONSerializingプロトコルの1つの方法を実現することで,JSON解析時にどのタイプのサブクラスオブジェクトに変換すべきかを迅速に決定できる.
    + (Class)classForParsingJSONDictionary:(NSDictionary *)JSONDictionary
    {
        if ([JSONDictionary[@"is_vip"] boolValue]) {
            return [JCVipGroupProfile class];
        }
        if ([JSONDictionary[@"is_supervip"] boolValue]) {
            return [JCSuperVipGroupProfile class];
        }
        if ([JSONDictionary[@"is_game"] boolValue]) {
            return [JCGameGroupProfile class];
        }
        if ([JSONDictionary[@"is_shot"] boolValue]) {
            return [JCShopGroupProfile class];
        }
        return self;
    }
    

    このとき、解析JSONは、解析の対象タイプとしてMTLJSOnAdapterにGroupProfileタイプを直接渡すことができる.
    7.空スカラー異常処理
    {
        "level": null
    }
    

    サーバAPIが上記のフィールドを返したと仮定すると、Mantleはどのように解析変換しますか?まず、Mantleのソース・コード・クリップを見てみましょう.
    __autoreleasing id value = [dictionary objectForKey:key];
    if ([value isEqual:NSNull.null]) value = nil;
    BOOL success = MTLValidateAndSetValue(self, key, value, YES, error);
    

    Mantle内部でnull値がnilに変換されていることがわかります.MantleはKVCに基づいてpropertyに値を付与する.propertyがNSNumberなどの参照タイプである場合、okを実行します.PropertyがIntのような基本データ型である場合、例外NSI n v a l i d ArgumentException NSObjectの非公式プロトコルNSKeyValue Codingが解決策を提供します.
    /* 
      Given that an invocation of -setValue:forKey: would be unable to set the keyed value because the type of the parameter of the corresponding accessor method is an NSNumber scalar type or NSValue structure type but the value is nil, set the keyed value using some other mechanism. 
      The default implementation of this method raises an NSInvalidArgumentException. 
      You can override it to map nil values to something meaningful in the context of your application.
    */
    - (void)setNilValueForKey:(NSString *)key;
    

    ほとんどの場合、基本データ型属性のデフォルト値は0であるため、MantleソースコードにMTLModelを直接見つけることができる.m,overrideメソッド:
    - (void)setNilValueForKey:(NSString *)key
    {
        [self setValue:@0 forKey:key];
    }
    

    これにより、すべてのMTLModelサブクラスをカスタマイズし、空スカラーの異常を回避できます.デフォルト値を-1などに設定する必要がある場合は、MTLModelサブクラスでoverrideメソッドを使用できます.
    - (void)setNilValueForKey:(NSString *)key
    {
        if ([key isEqualToString:@"level"]) {
            self.level = -1;
        } else {
            [super setNilValueForKey:key];
        }
    }
    

    Mantle Source Code Analysis
    isEqual And Hash
    MTLModelクラスのソースコードには、-hash-isEqual:の2つの方法がoverrideされており、すべてのサブクラスが判断対象が等しく、hash値の計算をよくサポートしています.
    - (NSUInteger)hash {
       NSUInteger value = 0;
       for (NSString *key in self.class.permanentPropertyKeys) {
          value ^= [[self valueForKey:key] hash];
       }
       return value;
    }
    
    - (BOOL)isEqual:(MTLModel *)model {
       if (self == model) return YES;
       if (![model isMemberOfClass:self.class]) return NO;
    
       for (NSString *key in self.class.permanentPropertyKeys) {
            id selfValue = [self valueForKey:key];
            id modelValue = [model valueForKey:key];
    
            BOOL valuesEqual = ((selfValue == nil && modelValue == nil) || [selfValue isEqual:modelValue]);
            if (!valuesEqual) return NO;
       }
       return YES;
    }
    

    2つのオブジェクトが等しいかどうかを判断する考え方:クラスオブジェクトの関連属性を遍歴し、それらが等しいかどうかを順次検出し、1つが等しくない場合はNOを返す.そうでなければ、YESに戻ります.
    ハッシュ衝突
    2つのオブジェクトが等しい場合、ハッシュ値は必ず等しいことを知っています.しかしハ系値が等しい場合、2つのオブジェクトは必ずしも等しいとは限らない.この現象をハッシュ衝突と呼ぶ.言い換えれば、1つのハッシュテーブルにおいて、keyハッシュの後に対応するアドレスに値が格納されている場合、この場合、ハッシュ衝突である.
    なぜハッシュ衝突を減らすのか
    Objective-Cはhash演算を必要とするコンテナにとって,ハッシュ衝突を回避することが重要である.ハッシュ衝突では、2つ以上のkeyがハッシュテーブル内の同じストレージ位置にマッピングされます.ハッシュ・テーブルは、古いvalueの格納場所を維持し、衝突位置に最も近い使用可能な格納場所に新しいvalueを配置します.ハッシュ・テーブルに衝突が発生し、ストレージ・データが増加すると、再衝突の可能性が高くなり、衝突が発生した場合のストレージ・スペースを探すのにかかる時間も大きくなります.
    ハッシュアルゴリズム
    Mantleソースコードから分かるように、MTLModelのハッシュアルゴリズムは、すべての属性のhash値を^演算によりハッシュ値を合成している.次に、クラスタ情報Modelクラスがクラスタ名とクラスタ署名属性のみである場合、上のハッシュアルゴリズムは、−(NSUInteger)hash{return[_namehash]^[_signhash];}
    groupAとgroupBは等しくないが、ハッシュ値は等しい可能性がある.
    /*
     * [@"  " hash] ^ [@"  " hash] 
     *    
     * [@"  " hash] ^ [@"  " hash] 
     * 
     *                 ,               
     */
    
    groupA.name = @"  ";
    groupA.sign = @"  ";
    
    groupB.name = @"  ";
    groupB.sign = @"  ";
    

    ハッシュ衝突は避けられないが,このアルゴリズムの効率も非常に高く,実際の動作でもハッシュ衝突はほとんど起こらないが,最適化に関する議論は依然として可能である.MTLModelのハッシュ関数として非対称ハッシュアルゴリズムを用いて試みた.
    #define NSUINTEGER_BIT (CHAR_BIT * sizeof(NSUInteger))
    #define NSUINTEGER_RORATE(value, shift) ((((NSUInteger)value) << shift) | (((NSUInteger)value) >> (NSUINTEGER_BIT - shift)))
    
    - (NSUInteger)hash {
        NSUInteger value = 0;
        NSUInteger mark = 0;
        for (NSString *key in self.class.permanentPropertyKeys) {
            if (mark % 2 == 0) {
                value ^= NSUINTEGER_RORATE([[self valueForKey:key] hash], NSUINTEGER_BIT/2);
            } else {
                value ^= [[self valueForKey:key] hash];
            }
            ++mark;        
        }
    
        //     
        if (value > NSUIntegerMax) {
            while (value > NSUIntegerMax) {
                value -= NSUIntegerMax;
            }
        } else if (value < 0) {
            while (value < 0) {
                value += NSUIntegerMax;
            }
        }
        return value;
    }
    

    このアルゴリズムは表面的にはあまり問題ないように見えるが,実際にはピットがある:高速列挙オブジェクトpermanentPropertyKeysはNSSetインスタンスであり,NSSetの遍歴は無秩序であり,ここでのハッシュ値計算は非対称であるため,同元素を持つ複数のNSSetオブジェクト遍歴で計算されるハッシュ値が異なる可能性が高い.言い換えれば、2つの同じオブジェクトのハッシュ値が等しくないことになる.再最適化されたハッシュアルゴリズム:
    - (NSUInteger)hash {
        NSMutableArray *keysArray = [NSMutableArray array];
        for (NSString *key in self.class.permanentPropertyKeys) {
            [keysArray addObject:key];
        }
        NSArray *sortedKeysArray = [keysArray sortedArrayUsingComparator:^NSComparisonResult(NSString *obj1, NSString *obj2) {
            return [obj1 compare:obj2];
        }];
    
        NSUInteger value = 0;
        for (NSString *key in sortedKeysArray) {
            if ([sortedKeysArray indexOfObject:key] % 2 == 0) {
                value ^= NSUINTEGER_RORATE([[self valueForKey:key] hash], NSUINTEGER_BIT/2);
            } else {
                value ^= [[self valueForKey:key] hash];
            }
        }
    
        //     
        if (value > NSUIntegerMax) {
            while (value > NSUIntegerMax) {
                value -= NSUIntegerMax;
            }
        } else if (value < 0) {
            while (value < 0) {
                value += NSUIntegerMax;
            }
        }
        return value;
    }
    

    このときのハッシュアルゴリズムは我々の目的を達成したが,MTLModelの従来のハッシュアルゴリズムよりも効率が悪い.Model層としては解析と計算が効率的でなければならないが,MTLModelのハッシュアルゴリズムは総合的に考えると確かに良い選択である.