iOSでのカテゴリの基本的な実装原則

1.カテゴリの使用シナリオ

Category呼ばれ分类たり类别、OCが提供するクラスを拡張する方法です。カスタムクラスであるかシステムクラスであるかに関係なく、Categoryメソッドを元のクラス(インスタンスメソッドとクラスメソッドの両方)に拡張でき、拡張メソッドは元のメソッドとまったく同じです。たとえば、私のプロジェクトでは、文字列の文字数を数える必要があることがよくありますが、システムはこのメソッドを提供していません。その場合、メソッドCategoryNSStringクラスに拡張でき、インポートされたCategoryヘッダーファイルのみをシステムメソッドとして呼び出すことができます。拡張メソッド。

// 给NSString类添加一个Category,并扩展一个实例方法
@interface NSString (QJAdd)

- (NSInteger)letterCount;

@end
// 在需要使用这个扩展方法的地方引入头文件 #import "NSString+QJAdd.h",然后就可以调用这个扩展方法了
- (void)test{
    NSString *testStr = @"sdfjshdfjk.,d.889";
    NSInteger letterCount = [testStr letterCount];
}

Categoryクラスの拡張に使用されることに加えて、より高度な使用法もあります。これは拆分模块、大きなモジュールを複数の小さなモジュールに分割して、保守と管理を容易にすることです。どういう意味ですか?多くの開発者が抱える問題、つまりAppDelegateこのカテゴリーを引用します。このクラスは、プロジェクトが作成されたときに自動的に生成され、プログラムのライフサイクルを管理するために使用されます。プロジェクトが最初に作成されたとき、このクラスには多くのコードがありませんでしたが、プロジェクトが進むにつれて、このクラスに配置されるコードはますます増えていきました。たとえば、Jiguang Push、Youmeng、Baidu Maps、WeChat SDKなどのさまざまなサードパーティフレームワークを統合する場合、これらのサードパーティフレームワークの初期化作業、さらには関連するビジネスロジックコードもこのカテゴリに分類され、アプリケーションにつながります。の機能はますます複雑にAppDelegateなりその中にはますます多くのコードがあり、中には数千行もあるものもあります。

現時点では、を使用CategoryしてAppDelegate分割できます。まずAppDelegate、コード分割し、同じ関数のコードを抽出して、カテゴリに分類する必要があります。たとえば、Jiguang Pushの新しいカテゴリを作成してから、Jiguang Pushに関連するすべてのコードをこのカテゴリに抽出し、WeChatに関連するすべてのコードを抽出して、WeChatカテゴリに入れることができます。新しい機能は後で追加されます。追加するには、新しいカテゴリを作成する必要があります。メンテナンス中に変更する機能のコードに対応するカテゴリを直接見つけてください。

// 把所有和极光推送有关的代码都抽出来放入这个分类
#import "AppDelegate.h"

@interface AppDelegate (JPush)

@end

 

2.カテゴリの基本的な実装

この問題を説明する前に、OCメソッド呼び出しの基本的なメカニズムとクラスオブジェクトのメモリストレージ構造についてある程度理解しておく必要があります。これに慣れていない場合は、まず、他のブログOCオブジェクトの本質を確認てください

最初にそのような問題について考えてみましょう。Categoryインスタンスメソッドをクラス拡張すると、このインスタンスメソッド呼び出すと、インスタンスのisaポインタを介してクラスオブジェクトも検索され、次にクラスオブジェクトのメソッドリストでこのメソッドが検索されます。 。ではCategory、拡張メソッドはどのようにしてクラスオブジェクトのメソッドリストに追加されますか?コンパイル時または実行時に追加されますか?

まず、Categoryメモリストレージ構造を見てみましょうCategory最下層は実際にはcategory_t一種の構造です。その定義objc4ソースコードobjc-runtime-new.hファイル確認できます。

// 定义在objc-runtime-new.h文件中
struct category_t {
    const char *name; // 比如给Student添加分类,name就是Student的类名
    classref_t cls;
    struct method_list_t *instanceMethods; // 分类的实例方法列表
    struct method_list_t *classMethods; // 分类的类方法列表
    struct protocol_list_t *protocols; // 分类的协议列表
    struct property_list_t *instanceProperties; // 分类的实例属性列表
    struct property_list_t *_classProperties; // 分类的类属性列表
};

この構造からCategory、メソッドリストだけなく、プロトコルリストと属性リストも格納されていることがわかります。

カテゴリを作成するたびに、そのような構造がコンパイル時に生成され、分類方法リストやその他の情報がこの構造に格納されます。コンパイル段階で分類された関連情報とこのカテゴリの関連情報は分離されています。実行時に、特定のクラスのすべてのカテゴリデータが実行時にロードされ、すべてのカテゴリのすべてのメソッド、属性、およびプロトコルデータがそれぞれ配列にマージされ、マージされたデータがこのクラスのデータの前に挿入されます。

詳細なプロセスを理解したい場合は、ソースコードに移動できますソースコードが多すぎるため、ここでは投稿しません。ソースコードを解釈する際のプロセス全体の関数呼び出しプロセスは次のとおりです。

関数Start-> -> -> -> -> -> ->objc-os.mmファイルから_objc_initmap_imagesmap_images_nolock_read_imagesremethodizeClassattachCategoriesattachListsrealloc、memmove、 memcpy

私の理解に基づいて、プロセス全体を説明する例を示します。インスタンスメソッドリストのマージプロセスのみを説明します。クラスメソッドリスト、属性リスト、プロトコルリスト、およびその他の情報のマージプロセスは同じです。

まず、私たちは宣言しStudentたクラスを、そして2つのカテゴリを作成します。Student (aaa)そしてStudent (bbb)次のコードに示すように、もともとおよびカテゴリに、二つの方法があります:

// Student.m文件
#import "Student.h"
@implementation Student

- (void)study{
    NSLog(@"%s",__func__);
}

- (void)studentTest{
    NSLog(@"%s",__func__);
}
@end

// Student+aaa.m文件
#import "Student+aaa.h"
@implementation Student (aaa)

- (void)study{
    NSLog(@"%s",__func__);
}

- (void)studentAaaTest{
    NSLog(@"%s",__func__);
}
@end

// Student+bbb.m文件
#import "Student+bbb.h"
@implementation Student (bbb)

- (void)study{
    NSLog(@"%s",__func__);
}

- (void)studentBbbTest{
    NSLog(@"%s",__func__);
}
@end

 

 

 

  • コンパイルが完了すると、2つのカテゴリaaaとbbbの情報が対応する構造に格納され、構造のインスタンスメソッドリストはそれぞれaaa->instanceMethods = @[@"study",@"studentAaaTest"]とになりますbbb->instanceMethods = @[@"study",@"studentBbbTest"]このときStudent、クラスオブジェクトのメソッドリストはclass_ro_t構造に格納されbaseMethodListます。だから在编译阶段各个方法列表都是分开存储的
  • 運用段階でStudentあるクラスオブジェクト初期化class_rw_t構造まで、この構造にはメソッドのリストもありますmethods。これは2次元配列であり、この時点でコピーされたものの初期化後class_ro_tに使用baseMethodListされmethods = @[baseMethodList]ます。
  • 次に、実行時にaaaとbbbの2つのカテゴリのデータをロードし、それらのメソッドリストをマージします。マージされた配列での順序(ここで名前を付けますcategoryMethodList)は、コンパイルに参加する順序に関連しています。aaaが最初にコンパイルされ、次にbbbがコンパイルされる場合、マージされた配列では、bbbのメソッドリスト前はaaaのメソッドリストが後ろにあるので今回はcategoryMethodList = @[bbb->instanceMethods,aaa->instanceMethods]
  • 次に、それにcategoryMethodListデータを追加しますmethods追加前methodsの容量サイズは1categoryMethodListで、リスト内のメソッドの数に応じ最初に拡張れ(つまり、いくつかのカテゴリがあり、ここでは2つのカテゴリです)、methods拡張後のサイズは3で、最初methods元のデータが追加されます(baseMethodList)最後に移動してからcategoryMethodListデータを挿入する、最終結果はになりmethods = @[bbb->instanceMethods,aaa->instanceMethods,baseMethodList]ます。

これで、分類メソッドリストとこのクラスメソッドリストのマージが完了しました。だから、合併後分类的方法在前面、(最後の方法は、上部にコンパイルされた分類リストを関与しました)本类的方法列表在最后面したがって、分類にこのクラスと同じ名前のメソッドがある場合、分類のメソッドはこのメソッドを呼び出すことによって実行されます。この現象から、このクラスのメソッドは、分類において同名のメソッドでカバーされているように見えますが、実際にはカバーされていませんが、メソッドが呼び出されたときに分類メソッドが最初に検出されるため、分類メソッドが実行されます。たとえば、上記の例では、このクラスと2つのカテゴリにstudyこのメソッドがあります。このクラスのメソッドリストを出力studyすると、と呼ばれる3つのメソッドがあることがわかります

3.カテゴリはどのようにプロパティをクラスに拡張しますか

通常のクラスのプロパティの定義を見てみましょう。たとえば、Studentクラスのプロパティ定義する@property (nonatomic , assign) NSInteger score;と、コンパイラは自動的に_scoreメンバー変数を生成し、このプロパティのsetter / getterメソッドを自動的に実装します。

@implementation Student
{
    NSInteger _score;
}

- (void)setScore:(NSInteger)score{
    _score = score;
}

- (NSInteger)score{
    return _score;
}
@end

Category同じように使用して、拡張属性とクラスメンバー変数を指定できますか?Category基礎となる構造から、この構造category_tにはメソッドリスト、プロトコルリスト、および属性リストがあることがわかりますが、メンバー変数リストがCategoryないため、属性を定義できますが、メンバー変数を定義することはできません。メンバー変数を定義する場合、コンパイラエラーを直接報告します。

Studentカテゴリに属性定義する@property (nonatomic , strong) NSString *name;と、コンパイラは何をしますか?コンパイラは- (void)setName:(NSString *)name;- (NSString *)name;これら2つのメソッドの宣言のみを支援しますがこれら2つのメソッドを実装したり、メンバー変数を定義したりすることはありません。したがって、name属性値を外部のインスタンスオブジェクトにstudent.name = @"Jack"設定すると、setterメソッドが宣言されているため、コンパイラはエラーを報告しませんが、プログラムが実行されるとunrecognized selector、setterメソッドが実装されていないため、例外がスローされます。

nameこの属性を通常どのように使用できますか?セッター/ゲッターメソッドを手動で実装できます。これら2つのメソッドを実装するための鍵は、属性値を保存する方法です。通常のクラスでは_name、属性値を保存するためのメンバー変数を定義しますが、分類ではできません。メンバー変数を定義するので、他の保存方法を考える必要があります。この要件は、次の方法で実現できます。

3.1このクラスの既存の属性を使用して保存する

このクラスに既存の属性を格納するとはどういう意味ですか?直接例を見てみましょう。たとえば、UIView拡張機能xyこれら2つの属性を指定する場合は、次のカテゴリを追加して次のことを実現できます。

// .h文件
@interface UIView (Add)

@property (nonatomic , assign) CGFloat x;
@property (nonatomic , assign) CGFloat y;

@end


// .m文件
#import "UIView+Add.h"
@implementation UIView (Add)

- (void)setX:(CGFloat)x{
    CGRect origionRect = self.frame;
    CGRect newRect = CGRectMake(x, origionRect.origin.y, origionRect.size.width, origionRect.size.height);
    self.frame = newRect;
}

- (CGFloat)x{
    return self.frame.origin.x;
}

- (void)setY:(CGFloat)y{
    CGRect origionRect = self.frame;
    CGRect newRect = CGRectMake(origionRect.origin.x, y, origionRect.size.width, origionRect.size.height);
    self.frame = newRect;
}

- (CGFloat)y{
    return self.frame.origin.y;
}
@end

この方法は、UIView元の属性を介しframeて新しく追加された属性xyにアクセスするためのものです明らかに、この方法には大きな制限があり、上記の特別な状況でのみ使用できます。

3.2カスタムグローバル辞書を介して保存

たとえばStudent、分類追加するにStudent (add)は、2つの属性の分類を定義nameage、次にプロパティ値に格納されているオブジェクトのすべてのインスタンスについて.m定義ファイルのグローバルディクショナリ2nameDicとを分類できます。ここで、オブジェクトのポインタインスタンスはキーです。 、属性値が値として使用されます。すべてのインスタンスオブジェクトの属性値を格納するために使用されますコードは次のとおりです。ageDicnameDicnamenameageDicage

#import "Student+add.h"

// 以实例对象的指针作为key
#define QJKey [NSString stringWithFormat:@"%p",self]

@implementation Student (add)

// 定义2个全局字典用来存储2个新增的属性的值
NSMutableDictionary *nameDic;
NSMutableDictionary *ageDic;

+ (void)load{
    nameDic = [NSMutableDictionary dictionary];
    ageDic = [NSMutableDictionary dictionary];
}

//
- (void)setName:(NSString *)name{
    nameDic[QJKey] = name;
}

- (NSString *)name{
    return nameDic[QJKey];
}

- (void)setAge:(NSInteger)age{
    ageDic[QJKey] = @(age);
}

- (NSInteger)age{
    return [ageDic[QJKey] integerValue];
}

@end

この方法は私たちのニーズを満たすことができますが、問題があります。オブジェクトをインスタンス化するたびに、2つのグローバル辞書に要素を追加し、インスタンス化されたオブジェクトが破棄されると、グローバル辞書がそれに対応します。要素が削除されていないため、これら2つの辞書がますます多くのメモリを占有し、メモリオーバーフローのリスクがあります。

3.3関連するオブジェクトを介して保存

3.3.1関連するオブジェクトAPIの説明

関連するオブジェクトはruntime提供されるAPIのセットであるため、ヘッダーファイルを導入する必要があります#import <objc/runtime.h>


関連するオブジェクトAPIを追加します。

void objc_setAssociatedObject(id object, 
                              const void * key,
                              id value, 
                              objc_AssociationPolicy policy);

たとえば、name属性がStudentカテゴリに追加されます。インスタンスオブジェクトのstuのname属性をJackに割り当てたいと思います。

  • 最初のパラメーター(object):上記の関連オブジェクトstu
  • 2番目のパラメーター(key):ここではvoid *、ポインターのタイプをキーとして渡します。このキーは自分で設定し、関連するオブジェクトは後でこのキーに基づいて取得されます。後で、キーを設定するいくつかの一般的な方法をリストします。
  • 3番目のパラメーター(value):設定する属性値。これは上記のジャックです。
  • 4番目のパラメーター(policy):名前の変更タイプなど、設定する属性の変更タイプstrong, nonatomicここでの対応するポリシーはOBJC_ASSOCIATION_RETAIN_NONATOMICです。具体的な対応は次のとおりです(弱いに対応するポリシーはないことに注意してください)。
objc_AssociationPolicy 対応する修飾子
OBJC_ASSOCIATION_ASSIGN 割当
OBJC_ASSOCIATION_RETAIN_NONATOMIC 強く、非原子的
OBJC_ASSOCIATION_COPY_NONATOMIC コピー、非原子
OBJC_ASSOCIATION_RETAIN 強い、原子
OBJC_ASSOCIATION_COPY コピー、アトミック

関連するオブジェクトを取得します。

id objc_getAssociatedObject(id object, const void * key);

関連するすべてのオブジェクトを削除します。

void objc_removeAssociatedObjects(id object);

キーを設定する一般的な方法:
キーはvoid *ポインタの一種です。原則として、関連付けられたオブジェクトを設定するときのキーが、関連付けられたオブジェクトを取得するときと同じであることを確認する限り、任意のポインタを設定できます。ただし、コードの読みやすさを向上させるために、次の方法でキーを設定できます。

たとえばstuインスタンスオブジェクトのnameプロパティに値割り当てますJack

方法1:

静的グローバルvoid *変数は属性ごとに宣言され、この変数に格納されている値はそれ自体のアドレスであるため、変数値の一意性を保証できます。

// 声明全局静态变量
static void *_name = &_name;

// 设置关联对象
objc_setAssociatedObject(stu, _name, @"Jack", OBJC_ASSOCIATION_RETAIN_NONATOMIC);

// 获取关联对象
NSString *temName = objc_getAssociatedObject(stu, _name);

方法2:
この方法は、グローバル変数が宣言された後に値が割り当てられず、変数のアドレス値がキーとして直接設定されることを除いて、上記の方法と同様です。

static void *_name;

objc_setAssociatedObject(stu, &_name, @"Jack", OBJC_ASSOCIATION_RETAIN_NONATOMIC);

NSString *temName = objc_getAssociatedObject(stu, &_name);

方法3:

属性名文字列のアドレスをキーとして使用します。key = @"name"これは、文字列自体をキーに割り当てるのではなく、文字列のアドレスをキーに割り当てるためであることに注意してくださいiOSでは、@"name"この文字列を指すように定義されているポインタ変数の数に関係なく、これらのポインタは実際には同じメモリスペースを指しているため、キーの一意性を保証できます。

objc_setAssociatedObject(stu, @"name", @"Jack", OBJC_ASSOCIATION_RETAIN_NONATOMIC);

NSString *temName = objc_getAssociatedObject(stu, @"name");

方法4:

@selectorクラス内の同じメソッド名に対応するSELアドレス常に同じであるため、プロパティのgetterメソッドをキーとして使用しますコードを書くときにプロンプ​​トが表示されるため、この方法を使用することをお勧めします。

objc_setAssociatedObject(stu, @selector(name), @"Jack", OBJC_ASSOCIATION_RETAIN_NONATOMIC);

NSString *temName = objc_getAssociatedObject(stu, @selector(name));

3.3.2関連するオブジェクトを介してプロパティ値を保存する

// .h文件
#import "Student.h"
@interface Student (Add)

@property (nonatomic , strong) NSString *name;
@property (nonatomic , assign) NSInteger age;
@end


// .m文件
#import "Student+Add.h"
#import <objc/runtime.h>
@implementation Student (Add)

- (void)setName:(NSString *)name{
    objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSString *)name{
    return objc_getAssociatedObject(self, @selector(name));
}

- (void)setAge:(NSInteger)age{
    objc_setAssociatedObject(self, @selector(age), @(age), OBJC_ASSOCIATION_ASSIGN);
}

- (NSInteger)age{
    return [objc_getAssociatedObject(self, @selector(age)) integerValue];
}
@end

3.3.3関連オブジェクトストレージの原則

質問がある人もいるかもしれませんが、関連オブジェクトの基本的な実装はどのように実装されていますか?属性を介してメンバー変数を生成し、それらをクラスオブジェクトのメンバー属性リストにマージしますか?実際にはそうではありません。関連するオブジェクトは個別に保存されます。下部に関連するオブジェクトテクノロジを実装する4つのコアオブジェクトがあります。

  • ObjcAssociation:このオブジェクト二つの部材があるuintptr_t _policyid _value2は、我々が渡され、関連するオブジェクトのパラメータを設定することは明らかであるpolicyvalue
  • ObjectAssociationMap:これはHashMap(キーと値のペアに格納され、辞書のように理解することができる)、関連するオブジェクトを設定する経過時間keyとしての価値をするなどのオブジェクトたとえば、カテゴリが3つの属性を追加し、そのインスタンスオブジェクトがこれらの3つの属性に値を割り当てる場合、このインスタンスには3つの要素があります。このインスタンスオブジェクトの1つの属性にnilの値が割り当てられると、これが変更されます。属性に対応するキーと値のペアが削除され、2つの要素が残ります。HashMapObjcAssociationHashMapHashMapHashMapHashMap
  • AssociationsHashMap:これはHashMap、関連する属性object設定するときに渡されるパラメーターを受け取るものもあります(実際には、値はアルゴリズムを介してオブジェクトに対して計算されます)。ObjectAssociationMapとして取るしたがって、クラス(このクラスのカテゴリに関連付けられたオブジェクトがある場合)がオブジェクトをインスタンス化するHashMapと、要素が追加され、インスタンス化されたオブジェクトが解放されると、対応するキーと値のペアもインスタンス化されます。これによりHashMap削除されましたプログラムの実行全体AssociationsHashMapで1つしかないことに注意してください。つまり、クラスに関連付けられているすべてのオブジェクト情報がこのに格納されますHashMap
  • AssociationsManager:名前からわかるように、マネージャーです。プログラム全体で1つしかなく、1つしか含まれていないことに注意してくださいAssociationsHashMap

これら4つのオブジェクトの関係図を次の図に示します。

 

 

関連オブジェクトStorage.pngの原理

おすすめ

転載: blog.csdn.net/wangletiancsdn/article/details/105248713