FrameWork Learning - Mantle
Mantle 소스 코드 의 가장 주요 한 내용 은:
여기 서 이 세 가 지 를 우리 의 분석 점 으로 삼 는 다.
기본 MTLModel
MTLModel 은 추상 적 인 유형 으로 대상 의 초기 화 와 압축 파일 작업 을 처리 하 는 기본 적 인 행 위 를 제공 합 니 다.
초기 화
MTLModel 의 기본 초기 화 방법 - init 는 아무 일 도 하지 않 고 [슈퍼 init] 만 호출 했 습 니 다.동시에 다른 초기 화 방법 을 제공 합 니 다.
- (instancetype)initWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error;그 중에서 매개 변수 dictionary Value 는 사전 으로 대상 을 초기 화 하 는 key - value 쌍 을 포함 합 니 다.우 리 는 그것 의 구체 적 인 실현 을 살 펴 보 자.
- (instancetype)initWithDictionary:(NSDictionary *)dictionary error:(NSError **)error {
    ...
    for (NSString *key in dictionary) {
        // 1.  value   __autoreleasing,     MTLValidateAndSetValue   ,
        //                      
        __autoreleasing id value = [dictionary objectForKey:key];
        // 2. value   NSNull.null,          nil
        if ([value isEqual:NSNull.null]) value = nil;
        // 3. MTLValidateAndSetValue    KVC     value    key    ,
        //        ,           key  。
        //              KVC    value    model   key   。
        //      MTLValidateAndSetValue        ,        。
        BOOL success = MTLValidateAndSetValue(self, key, value, YES, error);
        if (!success) return nil;
    }
    ...
}하위 클래스 는 이 방법 을 다시 쓸 수 있 습 니 다. 대상 의 속성 을 설정 한 후에 진일보 한 처리 나 초기 화 작업 을 할 수 있 습 니 다. 그러나 기억 해 야 할 것 은 슈퍼 를 통 해 부모 클래스 의 실현 을 호출해 야 한 다 는 것 입 니 다.
속성의 키 (key), 값 (value) 가 져 오기
MTLModel 클래스 는 클래스 방법 + propertyKeys 를 제공 합 니 다. 이 방법 은 모든 @ property 성명 의 속성 에 대응 하 는 이름 문자열 의 집합 을 되 돌려 줍 니 다. 단, 속성 과 MTLModel 자체 의 속성 만 읽 는 것 은 포함 되 지 않 습 니 다.이 방법 은 model 의 모든 속성 을 옮 겨 다 닙 니 다. 속성 이 읽 기만 하고 ivar 값 이 NULL 이 아니라면 속성 명 을 나타 내 는 문자열 을 가 져 와 집합 에 넣 습 니 다. 이 는 다음 과 같 습 니 다.
+ (NSSet *)propertyKeys {
    // 1.                 ,       。             。
    NSSet *cachedKeys = objc_getAssociatedObject(self, MTLModelCachedPropertyKeysKey);
    if (cachedKeys != nil) return cachedKeys;
    NSMutableSet *keys = [NSMutableSet set];
    // 2.          
    //    enumeratePropertiesUsingBlock     superclass        MTLModel,
    //        model               (   MTLModel),       block    。
    //      enumeratePropertiesUsingBlock        ,        。
    [self enumeratePropertiesUsingBlock:^(objc_property_t property, BOOL *stop) {
        mtl_propertyAttributes *attributes = mtl_copyPropertyAttributes(property);
        @onExit {
            free(attributes);
        };
        // 3.        ivar NULL   
        if (attributes->readonly && attributes->ivar == NULL) return;
        // 4.         ,       
        NSString *key = @(property_getName(property));
        [keys addObject:key];
    }];
    // 5.            。
    objc_setAssociatedObject(self, MTLModelCachedPropertyKeysKey, keys, OBJC_ASSOCIATION_COPY);
    return keys;
}위 와 같은 방법 이 있 습 니 다. 대상 의 모든 속성 과 해당 하 는 값 을 얻 으 려 면 방법 입 니 다.이 를 위해 MTLModel 은 현재 model 의 모든 속성 과 값 을 포함 하 는 사전 을 읽 기 전용 속성 dictionary Value 를 제공 합 니 다.속성 값 이 nil 이면 NSNull 로 대체 합 니 다.또한 이 속성 은 nil 이 아 닙 니 다.
@property (nonatomic, copy, readonly) NSDictionary *dictionaryValue;
//   
- (NSDictionary *)dictionaryValue {
    return [self dictionaryWithValuesForKeys:self.class.propertyKeys.allObjects];
}병합 대상
합병 대상 은 두 MTLModel 대상 을 사용자 정의 방법 에 따라 대응 하 는 속성 값 을 합 치 는 것 을 말한다.이 를 위해 MTLModel 에서 다음 과 같은 방법 을 정의 했다.
- (void)mergeValueForKey:(NSString *)key fromModel:(MTLModel *)model;이 방법 은 현재 대상 이 지정 한 key 속성의 값 과 model 매개 변수 에 대응 하 는 속성 값 을 지정 한 규칙 에 따라 통합 합 니 다. 이 규칙 은 우리 가 사용자 정의 - merge FromModel: 방법 으로 확정 합 니 다.만약 우리 의 하위 클래스 에서 - merge FromModel: 방법 을 실현 한다 면 그것 을 호출 할 것 입 니 다.찾 지 못 하고 model 이 nil 이 아니라면 model 의 속성 값 으로 현재 대상 의 속성 값 을 대체 합 니 다.구체 적 인 실현 은 다음 과 같다.
- (void)mergeValueForKey:(NSString *)key fromModel:(MTLModel *)model {
    NSParameterAssert(key != nil);
    // 1.      key  "mergeFromModel:"   ,             selector
    //              -mergeFromModel:  , model  nil,  model    
    //              
    //
    //    MTLSelectorWithCapitalizedKeyPattern   C             ,     
    //        ,       
    SEL selector = MTLSelectorWithCapitalizedKeyPattern("merge", key, "FromModel:");
    if (![self respondsToSelector:selector]) {
        if (model != nil) {
            [self setValue:[model valueForKey:key] forKey:key];
        }
        return;
    }
    // 2.   NSInvocation        -mergeFromModel:  。
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:selector]];
    invocation.target = self;
    invocation.selector = selector;
    [invocation setArgument:&model atIndex:2];
    [invocation invoke];
}그 밖 에 MTLModel 은 두 대상 의 모든 속성 치 를 합병 하 는 또 다른 방법 을 제공 했다. 즉,:
- (void)mergeValuesForKeysFromModel:(MTLModel *)model;주의해 야 할 것 은 model 은 현재 대상 이 속 한 클래스 나 하위 클래스 여야 합 니 다.
압축 파일 대상 (아 카 이브)
맨틀 은 MTLModel 에 대한 인 코딩 디 코딩 처 리 를 MTLModel 의 NSCoding 분류 에 넣 어 처 리 했 으 며, 이 분류 및 관련 정 의 는 MTLModel + NSCoding 파일 에 담 았 다.
서로 다른 속성 에 대해 인 코딩 디 코딩 과정 에서 차별 화 될 수 있 습 니 다. 이 를 위해 Mentle 은 MTLModel Encoding Behavior 를 매 거 하여 MTLModel 속성 이 압축 파일 에 인 코딩 된 행 위 를 확인 합 니 다.그 정 의 는 다음 과 같다.
typedef enum : NSUInteger {
    MTLModelEncodingBehaviorExcluded = 0,           //          
    MTLModelEncodingBehaviorUnconditional,          //          
    MTLModelEncodingBehaviorConditional,            //                       。         
} MTLModelEncodingBehavior;모든 속성의 압축 파일 행 위 를 구체 적 으로 설정 할 수 있 습 니 다.MTLModel 류 는 우리 에 게 기본 적 인 실현 을 제공 합 니 다. 다음 과 같 습 니 다.
+ (NSDictionary *)encodingBehaviorsByPropertyKey {
    // 1.         
    NSSet *propertyKeys = self.propertyKeys;
    NSMutableDictionary *behaviors = [[NSMutableDictionary alloc] initWithCapacity:propertyKeys.count];
    // 2.           
    for (NSString *key in propertyKeys) {
        objc_property_t property = class_getProperty(self, key.UTF8String);
        NSAssert(property != NULL, @"Could not find property \"%@\" on %@", key, self);
        mtl_propertyAttributes *attributes = mtl_copyPropertyAttributes(property);
        @onExit {
            free(attributes);
        };
        // 3.     weak ,     MTLModelEncodingBehaviorConditional,     MTLModelEncodingBehaviorUnconditional,    ,     NSNumber       。
        MTLModelEncodingBehavior behavior = (attributes->weak ? MTLModelEncodingBehaviorConditional : MTLModelEncodingBehaviorUnconditional);
        behaviors[key] = @(behavior);
    }
    return behaviors;
}이 되 돌아 오지 않 는 사전 의 속성 은 압축 되 지 않 습 니 다.하위 클래스 는 자신의 수요 에 따라 각 속성의 압축 파일 행 위 를 지정 할 수 있 습 니 다.그러나 실제 적 으로 슈퍼 를 통 해 부류 의 실현 을 호출해 야 한다.
압축 파일 에서 지정 한 속성 을 디 코딩 하기 위해 Mantle 은 다음 과 같은 방법 을 제공 합 니 다.
- (id)decodeValueForKey:(NSString *)key withCoder:(NSCoder *)coder modelVersion:(NSUInteger)modelVersion;기본적으로 이 방법 은 현재 대상 에서 - decodeWith Coder: model 버 전과 유사 한 방법 을 찾 습 니 다. 찾 으 면 해당 방법 을 호출 하고 사용자 정의 방식 으로 속성 디 코딩 을 처리 합 니 다.사용자 정의 방법 이나 coder 가 보안 인 코딩 이 필요 하지 않 으 면 지정 한 key 호출 - [NSCoder decodeObject ForKey:] 방법 을 사용 합 니 다.그 구체 적 인 실현 은 다음 과 같다.
- (id)decodeValueForKey:(NSString *)key withCoder:(NSCoder *)coder modelVersion:(NSUInteger)modelVersion {
    ...
    SEL selector = MTLSelectorWithCapitalizedKeyPattern("decode", key, "WithCoder:modelVersion:");
    // 1.       -decodeWithCoder:modelVersion:  ,   NSInvocation     
    if ([self respondsToSelector:selector]) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:selector]];
        invocation.target = self;
        invocation.selector = selector;
        [invocation setArgument:&coder atIndex:2];
        [invocation setArgument:&modelVersion atIndex:3];
        [invocation invoke];
        __unsafe_unretained id result = nil;
        [invocation getReturnValue:&result];
        return result;
    }
    @try {
        // 2.           -decodeWithCoder:modelVersion:  ,
        //          。
        //
        // coderRequiresSecureCoding            
        if (coderRequiresSecureCoding(coder)) {
            // 3.   coder      ,                       ,      
            //                。
            //      ,MTLModel      allowedSecureCodingClassesByPropertyKey,   
            //                               。            
            //         ,             。               ,
            //             ,            ,          NSValue,
            //          ,            。
            //                  。
            NSArray *allowedClasses = self.class.allowedSecureCodingClassesByPropertyKey[key];
            NSAssert(allowedClasses != nil, @"No allowed classes specified for securely decoding key \"%@\" on %@", key, self.class);
            return [coder decodeObjectOfClasses:[NSSet setWithArray:allowedClasses] forKey:key];
        } else {
            // 4.        
            return [coder decodeObjectForKey:key];
        }
    } @catch (NSException *exception) {
        ...
    }
}물론 모든 인 코딩 디 코딩 작업 은 - init With Coder: 와 - encodeWith Coder: 두 가지 방법 으로 이 루어 져 야 합 니 다.우 리 는 MTLModel 의 하위 클래스 를 정의 할 때 자신의 수요 에 따라 특정한 속성 을 처리 할 수 있 지만 슈퍼 의 실현 을 호출 하여 부모 클래스 의 작업 을 수행 하 는 것 이 좋 습 니 다.MTLModel 은 이 두 가지 방법의 실현 에 대해 소스 코드 를 참고 하 시기 바 랍 니 다. 여기 서 설명 을 많이 하지 않 습 니 다.
어댑터 MTLJSONapadter
MTLModel 대상 과 JSON 사전 간 의 상호 전환 을 편리 하 게 하기 위해 Mantle 은 이러한 MTLJSONapadter 를 제공 하여 이 두 가지 어댑터 로 한다.
MTLJSONserializing 프로 토 콜
Mantle 은 JSON 사전 과 상호 전환 이 필요 한 MTLModel 의 하위 클래스 가 MTLJSONserializing 을 실현 하여 MTLJSONapadter 대상 의 전환 을 편리 하 게 해 야 한다 고 정의 했다.이 협의 에서 세 가지 방법 을 정 의 했 는데 구체 적 으로 다음 과 같다.
@protocol MTLJSONSerializing
@required
+ (NSDictionary *)JSONKeyPathsByPropertyKey;
@optional
+ (NSValueTransformer *)JSONTransformerForKey:(NSString *)key;
+ (Class)classForParsingJSONDictionary:(NSDictionary *)JSONDictionary;
@end이 세 가지 방법 은 모두 같은 방법 이다.그 중에서 + JSONkey Pathsby Property Key 는 반드시 이 루어 져 야 합 니 다. 되 돌아 오 는 사전 은 대상 의 속성 을 JSON 의 서로 다른 key path (문자열 값 이나 NSNull) 에 어떻게 표시 하 는 지 지정 합 니 다.이 사전 에 없 는 속성 은 JSON 에서 사용 하 는 key 값 과 일치 하 는 것 으로 여 겨 집 니 다.NSNull 에 비 친 속성 은 JSON 서열 화 과정 에서 처리 되 지 않 습 니 다.
+ JSONtransformerForKey: 방법 은 JSON 값 을 지정 한 속성 값 으로 변환 하 는 방법 을 지정 합 니 다.반대로 변환기 도 속성 값 을 JSON 값 으로 변환 하 는 데 사용 된다.변환기 가 + JSONtransformer 방법 을 실현 하면 MTLJSONadapter 는 이 구체 적 인 방법 을 사용 하고 + JSONtransformerForKey: 방법 을 사용 하지 않 습 니 다.또한 사용자 정의 변환 이 필요 하지 않 으 면 nil 로 돌아 갑 니 다.
재 작성 + classForParsingJSONdictionary: 방법 은 현재 모델 을 다른 클래스 의 대상 으로 해석 할 수 있 습 니 다.이 대상 클래스 는 매우 유용 합 니 다. 그 중에서 추상 적 인 기 류 는 - [MTLJSONadapter initWithJSONdictionary: model Class:] 방법 에 전달 되 고 실례 화 된 것 은 하위 클래스 입 니 다.
만약 우리 가 MTLModel 의 하위 클래스 가 MTLJSONapadter 를 사용 하여 전환 할 수 있 기 를 원한 다 면 이 협 의 를 실현 하고 해당 하 는 방법 을 실현 해 야 한다.
초기 화
MTLJSONapadter 대상 은 읽 기 전용 속성 이 있 습 니 다. 이 속성 은 어댑터 가 처리 해 야 할 MTLModel 대상 입 니 다. 그 성명 은 다음 과 같 습 니 다.
@property (nonatomic, strong, readonly) MTLModel
*model;이 대상 은 MTLJSONserializing 합 의 를 실현 한 MTLModel 대상 이 어야 한 다 는 것 을 알 수 있다.이 속성 은 읽 기 전용 이기 때문에 초기 화 방법 으로 만 초기 화 할 수 있 습 니 다.
MTLJSONapadter 대상 은 - init 를 통 해 초기 화 할 수 없 으 며 이 방법 은 직접적 으로 단언 합 니 다.클래스 가 제공 하 는 두 가지 초기 화 방법 을 통 해 초기 화 해 야 합 니 다. 다음 과 같 습 니 다.
- (id)initWithJSONDictionary:(NSDictionary *)JSONDictionary modelClass:(Class)modelClass error:(NSError **)error;
- (id)initWithModel:(MTLModel*)model;그 중에서 - (id) initWithJSONdictionary: modelclass: error: 사전 과 변환 할 클래스 를 사용 하여 초기 화 합 니 다.사전 JSONdictionary 는 JSON 데 이 터 를 표시 합 니 다. 이 사전 은 NSJSONserialization 이 되 돌아 오 는 형식 에 부합 해 야 합 니 다.이 인자 가 비어 있 으 면 nil 을 되 돌려 주 고 MTLJSONadapter Error InvalidJSONdictionary 코드 가 있 는 error 대상 을 되 돌려 줍 니 다.이 방법의 구체 적 인 실현 은 다음 과 같다.
- (id)initWithJSONDictionary:(NSDictionary *)JSONDictionary modelClass:(Class)modelClass error:(NSError **)error {
    ...
    if (JSONDictionary == nil || ![JSONDictionary isKindOfClass:NSDictionary.class]) {
        ...
        return nil;
    }
    if ([modelClass respondsToSelector:@selector(classForParsingJSONDictionary:)]) {
        modelClass = [modelClass classForParsingJSONDictionary:JSONDictionary];
        if (modelClass == nil) {
            ...
            return nil;
        }
        ...
    }
    ...
    _modelClass = modelClass;
    _JSONKeyPathsByPropertyKey = [[modelClass JSONKeyPathsByPropertyKey] copy];
    NSMutableDictionary *dictionaryValue = [[NSMutableDictionary alloc] initWithCapacity:JSONDictionary.count];
    NSSet *propertyKeys = [self.modelClass propertyKeys];
    // 1.   model +JSONKeyPathsByPropertyKey   key-value     
    for (NSString *mappedPropertyKey in self.JSONKeyPathsByPropertyKey) {
        // 2.   model        +JSONKeyPathsByPropertyKey             
        //       nil。 +JSONKeyPathsByPropertyKey           model     
        //       。
        if (![propertyKeys containsObject:mappedPropertyKey]) {
            ...
            return nil;
        }
        id value = self.JSONKeyPathsByPropertyKey[mappedPropertyKey];
        // 3.            JSON       NSNull,   nil。
        if (![value isKindOfClass:NSString.class] && value != NSNull.null) {
            ...
            return nil;
        }
    }
    for (NSString *propertyKey in propertyKeys) {
        NSString *JSONKeyPath = [self JSONKeyPathForPropertyKey:propertyKey];
        if (JSONKeyPath == nil) continue;
        id value;
        @try {
            value = [JSONDictionary valueForKeyPath:JSONKeyPath];
        } @catch (NSException *ex) {
            ...
            return nil;
        }
        if (value == nil) continue;
        @try {
            // 4.        ,
            //        ,+JSONTransformerForKey:        +JSONTransformer  ,
            //                  ,    ,      +JSONTransformerForKey:  
            //                
            NSValueTransformer *transformer = [self JSONTransformerForKey:propertyKey];
            if (transformer != nil) {
                // 5.           
                if ([value isEqual:NSNull.null]) value = nil;
                value = [transformer transformedValue:value] ?: NSNull.null;
            }
            dictionaryValue[propertyKey] = value;
        } @catch (NSException *ex) {
            ...
            return nil;
        }
    }
    // 6.    _model
    _model = [self.modelClass modelWithDictionary:dictionaryValue error:error];
    if (_model == nil) return nil;
    return self;
}또한 MTLJSONapadter 는 MTLJSONapadter 대상 을 만 드 는 몇 가지 방법 을 제공 했다. 다음 과 같다.
+ (id)modelOfClass:(Class)modelClass fromJSONDictionary:(NSDictionary *)JSONDictionary error:(NSError **)error;
+ (NSArray *)modelsOfClass:(Class)modelClass fromJSONArray:(NSArray *)JSONArray error:(NSError **)error;
+ (NSDictionary *)JSONDictionaryFromModel:(MTLModel*)model;구체 적 으로 실현 하면 소스 코드 를 참고 할 수 있다.
대상 에서 JSON 데이터 가 져 오기
MTLModel 대상 에서 JSON 데 이 터 를 얻 는 것 은 상기 초기 화 과정 중의 역 과정 이다.이 과정 은 - JSONdictionary 방법 으로 이 루어 집 니 다. 구체 적 으로 다음 과 같 습 니 다.
- (NSDictionary *)JSONDictionary {
    NSDictionary *dictionaryValue = self.model.dictionaryValue;
    NSMutableDictionary *JSONDictionary = [[NSMutableDictionary alloc] initWithCapacity:dictionaryValue.count];
    [dictionaryValue enumerateKeysAndObjectsUsingBlock:^(NSString *propertyKey, id value, BOOL *stop) {
        NSString *JSONKeyPath = [self JSONKeyPathForPropertyKey:propertyKey];
        if (JSONKeyPath == nil) return;
        // 1.       
        NSValueTransformer *transformer = [self JSONTransformerForKey:propertyKey];
        if ([transformer.class allowsReverseTransformation]) {
            if ([value isEqual:NSNull.null]) value = nil;
            value = [transformer reverseTransformedValue:value] ?: NSNull.null;
        }
        NSArray *keyPathComponents = [JSONKeyPath componentsSeparatedByString:@"."];
        // 2.           ,   keypath        ,
        //           obj      ,          ;         ,    
        //               : @{@"nested": @{@"name": @"foo"}}
        id obj = JSONDictionary;
        for (NSString *component in keyPathComponents) {
            if ([obj valueForKey:component] == nil) {
                [obj setValue:[NSMutableDictionary dictionary] forKey:component];
            }
            obj = [obj valueForKey:component];
        }
        [JSONDictionary setValue:value forKeyPath:JSONKeyPath];
    }];
    return JSONDictionary;
}위 에서 알 수 있 듯 이 이 방법 은 실제로 하나의 사전 을 얻 었 다.사전 을 얻 은 뒤 JSON 꼬치 로 서열 화하 기 가 쉽다.
MTLJSONapadter 도 하나의 model 에서 JSON 사전 을 얻 을 수 있 는 간단 한 방법 을 제공 합 니 다. 그 정 의 는 다음 과 같 습 니 다.
+ (NSDictionary *)JSONDictionaryFromModel:(MTLModel
*)model;MTLManagedObjectAdapter
코어 데이터 에 적응 하기 위해 맨틀 은 MTLManaged ObjectAdapter 류 를 전문 적 으로 정의 했다.이 종 류 는 MTLModel 대상 과 NSManaged Object 대상 이전의 전환 에 사용 된다.구체 적 인 것 은 여기 서 상세 하 게 설명 하지 않 겠 습 니 다.
기술 점 총화
Mantle 의 기능 은 주로 대상 간 의 데 이 터 를 전환 하 는 것 이다. 즉, MTLModel 과 JSON 사전 에서 데 이 터 를 어떻게 전환 하 는 지 하 는 것 이다.따라서 사용 하 는 기술 은 대부분 코코아 파운데이션 이 제공 하 는 기능 이다.Core Data 에 대한 처 리 를 제외 하고 주로 사용 하 는 기술 은 다음 과 같은 몇 가지 가 있 습 니 다.
KVC 의 응용: 이것 은 주로 MTLModel 하위 클래스 의 속성 할당 에 나타 나 고 KVC 체 제 를 통 해 값 의 유효성 을 검증 하고 속성 할당 에 나타난다.
NSValueTransform: 이것 은 주로 JSON 값 을 속성 값 으로 변환 하 는 데 사 용 됩 니 다. 우 리 는 변환 기 를 사용자 정의 하여 우리 자신의 변환 수 요 를 만족 시 킬 수 있 습 니 다.
NSInvocation: 이것 은 주로 특정한 key 값 에 대한 호출 을 통일 적 으로 처리 하 는 데 사 용 됩 니 다.예 를 들 어 - merge FromModel: 이런 방법.
Run time 함수 사용: 문자열 에서 방법 에 대응 하 는 문자열 을 가 져 온 다음 selregisterName 함수 로 selector 를 등록 합 니 다.
물론 Mantle 에 서 는 다른 기술 점 도 언급 되 어 서술 을 많이 하지 않 는 다.
이 내용에 흥미가 있습니까?
현재 기사가 여러분의 문제를 해결하지 못하는 경우 AI 엔진은 머신러닝 분석(스마트 모델이 방금 만들어져 부정확한 경우가 있을 수 있음)을 통해 가장 유사한 기사를 추천합니다:
Swift의 패스트 패스Objective-C를 대체하기 위해 만들어졌지만 Xcode는 Objective-C 런타임 라이브러리를 사용하기 때문에 Swift와 함께 C, C++ 및 Objective-C를 컴파일할 수 있습니다. Xcode는 S...
텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
CC BY-SA 2.5, CC BY-SA 3.0 및 CC BY-SA 4.0에 따라 라이센스가 부여됩니다.