1.カテゴリの使用シナリオ
Category
呼ばれ分类
たり类别
、OCが提供するクラスを拡張する方法です。カスタムクラスであるかシステムクラスであるかに関係なく、Category
メソッドを元のクラス(インスタンスメソッドとクラスメソッドの両方)に拡張でき、拡張メソッドは元のメソッドとまったく同じです。たとえば、私のプロジェクトでは、文字列の文字数を数える必要があることがよくありますが、システムはこのメソッドを提供していません。その場合、メソッドCategory
をNSString
クラスに拡張でき、インポートされた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_init
map_images
map_images_nolock
_read_images
remethodizeClass
attachCategories
attachLists
realloc、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
拡張機能x
とy
これら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
て新しく追加された属性x
とy
値にアクセスするためのものです。明らかに、この方法には大きな制限があり、上記の特別な状況でのみ使用できます。
3.2カスタムグローバル辞書を介して保存
たとえばStudent
、分類を追加するにStudent (add)
は、2つの属性の分類を定義name
しage
、次に、プロパティ値に格納されているオブジェクトのすべてのインスタンスについて、.m
定義ファイルのグローバルディクショナリ2nameDic
とを分類できます。ここで、オブジェクトのポインタインスタンスはキーです。 、属性値が値として使用されます。すべてのインスタンスオブジェクトの属性値を格納するために使用されます。コードは次のとおりです。ageDic
nameDic
name
name
ageDic
age
#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 _policy
とid _value
2は、我々が渡され、関連するオブジェクトのパラメータを設定することは明らかであるpolicy
とvalue
。ObjectAssociationMap
:これはHashMap
(キーと値のペアに格納され、辞書のように理解することができる)、関連するオブジェクトを設定する経過時間key
としての価値をするなどのオブジェクト。たとえば、カテゴリが3つの属性を追加し、そのインスタンスオブジェクトがこれらの3つの属性に値を割り当てる場合、このインスタンスには3つの要素があります。このインスタンスオブジェクトの1つの属性にnilの値が割り当てられると、これが変更されます。属性に対応するキーと値のペアが削除され、2つの要素が残ります。HashMap
键
ObjcAssociation
HashMap
值
HashMap
HashMap
HashMap
AssociationsHashMap
:これはHashMap
、関連する属性object
を設定するときに渡されるパラメーターを受け取るものでもあります键
(実際には、値はアルゴリズムを介してオブジェクトに対して計算されます键
)。ObjectAssociationMap
として取る值
。したがって、クラス(このクラスのカテゴリに関連付けられたオブジェクトがある場合)がオブジェクトをインスタンス化するHashMap
と、要素が追加され、インスタンス化されたオブジェクトが解放されると、対応するキーと値のペアもインスタンス化されます。これによりHashMap
削除されました。プログラムの実行全体AssociationsHashMap
で1つしかないことに注意してください。つまり、クラスに関連付けられているすべてのオブジェクト情報がこのに格納されますHashMap
。AssociationsManager
:名前からわかるように、マネージャーです。プログラム全体で1つしかなく、1つしか含まれていないことに注意してくださいAssociationsHashMap
。
これら4つのオブジェクトの関係図を次の図に示します。
関連オブジェクトStorage.pngの原理