前言
iOS App啟動優(yōu)化《二進(jìn)制重排》我們講述了App的pre-main階段的流程以及二進(jìn)制重排的原理,接著我們就用這篇文章來實(shí)現(xiàn)二進(jìn)制重排。
1 配置Clang插樁
我們打開Clang的官方文檔# Clang 13 documentation
在這里有一個Tracing PCs,PC指的是PC寄存器,CPU在讀取代碼的指針即讀取虛擬內(nèi)存的那一行代碼。
所以Tracing PCs跟蹤的是CPU執(zhí)行到的代碼。
如何使用的呢,官方文檔有詳細(xì)介紹,我們來配置下,我們先添加一個標(biāo)記
-fsanitize-coverage=trace-pc-guard
如圖

我們編譯一下,如圖

這里報鏈接錯誤,找不到符號,這是因為還需要實(shí)現(xiàn)兩個回調(diào)函數(shù)
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
uint32_t *stop) {
static uint64_t N; // Counter for the guards.
if (start == stop || *start) return; // Initialize only once.
printf("INIT: %p %p\n", start, stop);
for (uint32_t *x = start; x < stop; x++)
*x = ++N; // Guards should start from 1.
}
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (!*guard) return;
void *PC = __builtin_return_address(0);
char PcDescr[1024];
printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}
我們再次編譯,編譯成功。
這里有一行printf("INIT: %p %p\n", start, stop);代碼,我們看下這個start和stop是什么,我們運(yùn)行一下,看下效果,如圖

這里打了兩個地址,我們來分析下這些是什么。
start和stop都是uint32_t即unsigned int類型的指針,這說明上面打印的地址存放的是unsigned int類型數(shù)據(jù),這些unsigned int類型數(shù)據(jù)到底是什么,我們看下,如圖

這里的start和stop代表符號個數(shù),我們看下最后個數(shù)據(jù),如圖

其中11000000是最后一個數(shù)據(jù),stop往上走4個字節(jié)讀取最后一個數(shù)據(jù)。
*for (uint32_t *x = start; x < stop; x++)
x = ++N;
這里就是從start位置到stop這個位置讀取符號個數(shù),這里是11個符號,我們來驗證下
void test() {
}
我們在ViewController.m加入
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
}
再次運(yùn)行,如下所示

這里變成了13,我們剛才增加了兩個方法,加進(jìn)去了,這說明我們的方法和函數(shù)都攔截到了。
我們再來嘗試下block,如下
void (^block)(void) = ^(void) {
};
我們再來看,如圖

這說明我們的block也攔截住了。
這個Clang的Trace是全局的,其它的文件一樣可以攔截。
+ (void)load {
}
+ (void)initialize {
}
我們再加上這兩個函數(shù),調(diào)試如下圖

說明load和initialize方法可以攔住的。
__sanitizer_cov_trace_pc_guard我們來調(diào)試下這個函數(shù),打個斷點(diǎn),如圖

運(yùn)行項目,點(diǎn)擊屏幕,看下堆棧,如圖

我們可以看到touchesBegan這個方法調(diào)起了__sanitizer_cov_trace_pc_guard這個函數(shù),我們改下touchesBegan的代碼,如下
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"屏幕點(diǎn)擊了");
}
再次運(yùn)行,如圖

經(jīng)過測試發(fā)現(xiàn),__sanitizer_cov_trace_pc_guard這個函數(shù)在NSLog之前調(diào)用了,我們在block,test()函數(shù)加上打印,發(fā)現(xiàn)都在NSLog之前調(diào)用了__sanitizer_cov_trace_pc_guard這個函數(shù),說明它可以攔截當(dāng)前項目中的所有符號,系統(tǒng)庫和三方庫不會攔截。
我們重新排列的是代碼的實(shí)現(xiàn)的二進(jìn)制,系統(tǒng)庫和三方庫不在我們
項目中生成Mach-o文件。
我們自定義的屬性生成的get和set方法是可以攔截到的。
2 Clang的原理分析
__sanitizer_cov_trace_pc_guard這個函數(shù)是HOOK一切的回調(diào)函數(shù),我們來分析下它是如何做到的。
我們在這個函數(shù)打個斷點(diǎn),然后運(yùn)行,過掉所有斷點(diǎn),再次打上斷點(diǎn),點(diǎn)擊屏幕,斷住之后,我們來分析下它的匯編,如圖

我們看下touchBegan,如圖

這里可以看出當(dāng)touchBegan被調(diào)起的時候,立馬進(jìn)入了__sanitizer_cov_trace_pc_guard這個回調(diào)函數(shù),
我們看到匯編bl 0x1000359e4 ; __sanitizer_cov_trace_pc_guard at ClangTrace.m:29,這是bl到了__sanitizer_cov_trace_pc_guard這個函數(shù),這說明我們只要添加了Clang插樁的標(biāo)記,編譯器就會在所有的方法,函數(shù),block的代碼實(shí)現(xiàn)的邊緣的加上一句bl 0x1000359e4 ; __sanitizer_cov_trace_pc_guard代碼,在實(shí)現(xiàn)函數(shù)的代碼之前加上了這句代碼,同樣在函數(shù)和block中都有這樣的代碼。
這里相當(dāng)于修改了二進(jìn)制文件,如何修改的,我們分析下。
在所有的方法前面插入一行代碼,只有編譯器能做到,編譯器在讀到我們的方法,函數(shù),block時就會插入這行代碼。
我們是通過Other c Flags 添加的標(biāo)記,所以肯定是在編譯期做這個插入代碼的動作。
3 獲取到符號
我們的目標(biāo)是最終要生成order文件,那就需要獲取到符號名稱和順序,怎么才能獲取到呢,我們分析看看。
我們先打開void *PC = __builtin_return_address(0);這個函數(shù),__builtin_return_address這個函數(shù)返回的是上一個函數(shù)的地址,也就是調(diào)用者,這個PC就是上一個函數(shù)的地址,也就是函數(shù)的第一行代碼的地址,第0行插入了bl 0x1000359e4 ; __sanitizer_cov_trace_pc_guard at ClangTrace.m:29代碼
我們可以通過個地址獲取到符號的名字,代碼如下
Dl_info info;
dladdr(PC, &info);
這里需要導(dǎo)入#import <dlfcn.h>這個頭文件,函數(shù)的信息會存在info這個結(jié)構(gòu)體中,我們看下這個結(jié)構(gòu)體的定義,如下
/*
* Structure filled in by dladdr().
*/
typedef struct dl_info {
const char *dli_fname; /* Pathname of shared object */
void *dli_fbase; /* Base address of shared object */
const char *dli_sname; /* Name of nearest symbol */
void *dli_saddr; /* Address of nearest symbol */
} Dl_info;
- dli_fname m文件名字
- dli_fbase m文件的地址
- dli_sname 函數(shù)或方法的名字
- dli_saddr 地址
我們來驗證下,代碼如下
printf("fname:%s\nfbase:%p\nsname:%s\nsaddr:%p\n",info.dli_fname,info.dli_fbase,info.dli_sname, info.dli_saddr);
運(yùn)行,效果如下
fname:/var/mobile/Containers/Bundle/Application/CDD47581-31B2-48C9-930D-B6D683798DB3/ClangTrace.app/ClangTrace
fbase:0x100024000
sname:+[ClangTrace load]
saddr:0x100029a30
INIT: 0x10002d788 0x10002d7e0
fname:/var/mobile/Containers/Bundle/Application/CDD47581-31B2-48C9-930D-B6D683798DB3/ClangTrace.app/ClangTrace
fbase:0x100024000
sname:main
saddr:0x100029e5c
fname:/var/mobile/Containers/Bundle/Application/CDD47581-31B2-48C9-930D-B6D683798DB3/ClangTrace.app/ClangTrace
fbase:0x100024000
sname:-[AppDelegate window]
saddr:0x100029d2c
fname:/var/mobile/Containers/Bundle/Application/CDD47581-31B2-48C9-930D-B6D683798DB3/ClangTrace.app/ClangTrace
fbase:0x100024000
sname:-[AppDelegate setWindow:]
saddr:0x100029d7c
fname:/var/mobile/Containers/Bundle/Application/CDD47581-31B2-48C9-930D-B6D683798DB3/ClangTrace.app/ClangTrace
fbase:0x100024000
sname:-[AppDelegate application:didFinishLaunchingWithOptions:]
saddr:0x100029ad0
fname:/var/mobile/Containers/Bundle/Application/CDD47581-31B2-48C9-930D-B6D683798DB3/ClangTrace.app/ClangTrace
fbase:0x100024000
sname:-[AppDelegate window]
saddr:0x100029d2c
fname:/var/mobile/Containers/Bundle/Application/CDD47581-31B2-48C9-930D-B6D683798DB3/ClangTrace.app/ClangTrace
fbase:0x100024000
sname:-[AppDelegate window]
saddr:0x100029d2c
fname:/var/mobile/Containers/Bundle/Application/CDD47581-31B2-48C9-930D-B6D683798DB3/ClangTrace.app/ClangTrace
fbase:0x100024000
sname:-[ViewController viewDidLoad]
saddr:0x1000297a4
fname:/var/mobile/Containers/Bundle/Application/CDD47581-31B2-48C9-930D-B6D683798DB3/ClangTrace.app/ClangTrace
fbase:0x100024000
sname:-[AppDelegate window]
saddr:0x100029d2c
這里就獲取了符號名稱以及調(diào)用順序。
4 利用原子隊列保存符號
我們在__sanitizer_cov_trace_pc_guard加入
NSLog(@"%@", [NSThread currentThread]);
然后順ViewController.m文件加入代碼
+ (void)load {
}
+ (void)initialize {
}
void test() {
NSLog(@"test函數(shù)執(zhí)行");
}
void (^block)(void) = ^(void) {
NSLog(@"block函數(shù)執(zhí)行");
};
- (void)sleepT{
sleep(3);
}
- (void)viewDidLoad {
[super viewDidLoad];
[self performSelectorInBackground:@selector(sleepT) withObject:nil];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// NSLog(@"屏幕點(diǎn)擊了");
test();
}
運(yùn)行項目,看下效果,如下所示
2021-09-02 13:40:26.424 ClangTrace[2817:871645] <NSThread: 0x12ee2be50>{number = 1, name = main}
+[ViewController load]
2021-09-02 13:40:26.425 ClangTrace[2817:871645] <NSThread: 0x12ee2be50>{number = 1, name = main}
main
2021-09-02 13:40:27.311 ClangTrace[2817:871645] <NSThread: 0x12ee2be50>{number = 1, name = main}
+[ViewController initialize]
2021-09-02 13:40:27.312 ClangTrace[2817:871645] <NSThread: 0x12ee2be50>{number = 1, name = main}
-[AppDelegate window]
2021-09-02 13:40:27.318 ClangTrace[2817:871645] <NSThread: 0x12ee2be50>{number = 1, name = main}
-[AppDelegate setWindow:]
2021-09-02 13:40:27.324 ClangTrace[2817:871645] <NSThread: 0x12ee2be50>{number = 1, name = main}
-[AppDelegate application:didFinishLaunchingWithOptions:]
2021-09-02 13:40:27.324 ClangTrace[2817:871645] <NSThread: 0x12ee2be50>{number = 1, name = main}
-[AppDelegate window]
2021-09-02 13:40:27.325 ClangTrace[2817:871645] <NSThread: 0x12ee2be50>{number = 1, name = main}
-[AppDelegate window]
2021-09-02 13:40:27.330 ClangTrace[2817:871645] <NSThread: 0x12ee2be50>{number = 1, name = main}
-[ViewController viewDidLoad]
2021-09-02 13:40:27.336 ClangTrace[2817:871748] <NSThread: 0x12ed20eb0>{number = 2, name = (null)}
-[ViewController sleepT]
2021-09-02 13:40:27.346 ClangTrace[2817:871645] <NSThread: 0x12ee2be50>{number = 1, name = main}
-[AppDelegate window]
這里打印了線程的信息,其中<NSThread: 0x12ed20eb0>{number = 2,name = (null)}這里說明了在子線程也是可以獲取的。
所以__sanitizer_cov_trace_pc_guard這個回調(diào)也是多線程的,我們的方法在子線程執(zhí)行的話,這個回調(diào)函數(shù)也是在子線程執(zhí)行的。在這里存儲數(shù)據(jù)的話,就有多線程的訪問,就會造成線程不安全。
我們就用線程安全的隊列OSAtomic來處理,代碼如下
// 定義原子隊列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
// 定義符號的結(jié)構(gòu)
typedef struct {
void * pc; // 函數(shù)地址
void * next; // 下一個函數(shù)節(jié)點(diǎn)
}SymboNode;
我們在修改__sanitizer_cov_trace_pc_guard這個函數(shù)的代碼,如下
/// HOOK一切的回調(diào)函數(shù)
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
void *PC = __builtin_return_address(0);
// 創(chuàng)建結(jié)構(gòu)體
SymboNode *node = malloc(sizeof(SymboNode));
// 先給node賦值,下個節(jié)點(diǎn)暫時先為空
*node = (SymboNode){PC, NULL};
// 結(jié)構(gòu)體入棧,node存入symbolList,并把下一個地址給到node的next屬性
OSAtomicEnqueue(&symbolList, node, offsetof(SymboNode, next));
}
我們在touchesBegan加入代碼
while (YES) {
SymboNode *node =OSAtomicDequeue(&symbolList, offsetof(SymboNode, next));
if (node == NULL) {
break;
}
//獲取符號信息
Dl_info info;
dladdr(node->pc, &info);
printf("%s\n",info.dli_sname);
}
運(yùn)行看下效果,如下
[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
發(fā)現(xiàn)死循環(huán)了,這是為什么呢?
__sanitizer_cov_trace_pc_guard這個函數(shù)把我們的循環(huán)也給攔截了,如何解決呢,我們需要修改Other C Flags的標(biāo)記,如下
-fsanitize-coverage=func,trace-pc-guard
我們再運(yùn)行一下項目看下結(jié)果,如下
-[ViewController touchesBegan:withEvent:]
-[SceneDelegate sceneDidBecomeActive:]
-[SceneDelegate sceneWillEnterForeground:]
-[ViewController sleepT]
-[ViewController viewDidLoad]
-[SceneDelegate window]
-[SceneDelegate window]
-[SceneDelegate window]
-[SceneDelegate scene:willConnectToSession:options:]
-[SceneDelegate window]
-[SceneDelegate window]
-[SceneDelegate setWindow:]
-[SceneDelegate window]
+[ViewController initialize]
-[AppDelegate application:didFinishLaunchingWithOptions:]
main
+[ViewController load]
結(jié)果正常了,所以我們應(yīng)該只攔截方法。
5 方法順序調(diào)整和去除重復(fù)的符號
從上面的運(yùn)行結(jié)果可以看出,這個順序是反的,并且這里還有很多重復(fù),需要我們處理一下,代碼如下
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 定義數(shù)組
NSMutableArray<NSString *> *sybleNames = [NSMutableArray array];
while (YES) {
SymboNode *node =OSAtomicDequeue(&symbolList, offsetof(SymboNode, next));
if (node == NULL) {
break;
}
//獲取符號信息
Dl_info info;
dladdr(node->pc, &info);
// 轉(zhuǎn)字符串
NSString *name = @(info.dli_sname);
// 區(qū)分函數(shù),block和OC方法的符號,函數(shù)與block是一樣的
NSString *symbolName = ([name hasPrefix:@"+["] || [name hasPrefix:@"-["])? name: [@"_" stringByAppendingString:name];
[sybleNames addObject:symbolName];
}
//反向遍歷數(shù)組
NSEnumerator *enumerator = [sybleNames reverseObjectEnumerator];
NSMutableArray *funArray = [NSMutableArray arrayWithCapacity:sizeof(sybleNames.count)];
// 遍歷去除重復(fù)的符號
NSString *name;
while (name = [enumerator nextObject]) {
if (![funArray containsObject:name]) {
[funArray addObject:name];
}
}
NSLog(@"%@",funArray);
}
6 生成order文件
最后一步,就把這些攔截到的符號寫入到文件中,代碼如下
// 定義數(shù)組
NSMutableArray<NSString *> *sybleNames = [NSMutableArray array];
while (YES) {
SymboNode *node =OSAtomicDequeue(&symbolList, offsetof(SymboNode, next));
if (node == NULL) {
break;
}
//獲取符號信息
Dl_info info;
dladdr(node->pc, &info);
// 轉(zhuǎn)字符串
NSString *name = @(info.dli_sname);
// 區(qū)分函數(shù),block和OC方法的符號,函數(shù)與block是一樣的
NSString *symbolName = ([name hasPrefix:@"+["] || [name hasPrefix:@"-["])? name: [@"_" stringByAppendingString:name];
[sybleNames addObject:symbolName];
}
//反向遍歷數(shù)組
NSEnumerator *enumerator = [sybleNames reverseObjectEnumerator];
NSMutableArray *funArray = [NSMutableArray arrayWithCapacity:sizeof(sybleNames.count)];
// 遍歷去除重復(fù)的符號
NSString *name;
while (name = [enumerator nextObject]) {
if (![funArray containsObject:name]) {
[funArray addObject:name];
}
}
NSLog(@"%@",funArray);
//去掉自己
[funArray removeObject:[NSString stringWithFormat:@"%s", __func__]];
// 寫入order文件
// 變成字符串
NSString *funcStr = [funArray componentsJoinedByString:@"\n"];
// 存儲路徑
NSString *filePath = [NSTemporaryDirectory() stringByAppendingString:@"/clangTrace.order"];
// 文件
NSData *file = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
// 創(chuàng)建文件
[[NSFileManager defaultManager] createFileAtPath:filePath contents:file attributes:nil];
NSLog(@"%@", funcStr);
運(yùn)行之后, 我們把真機(jī)上的文件download下來,找到這個clangTrace.order文件,如圖

這樣就保存下來了,我們可以使用下order文件,如圖

運(yùn)行,我們看下map文件的內(nèi)容,如圖

跟我們order文件的順序一模一樣的。
7 Swift符號覆蓋
我們創(chuàng)建一個swift文件,如下
import UIKit
class SwiftTest: NSObject {
@objc class public func swifttest () {
print("swifttest......")
}
}
然后在ViewController.m文件中的load方法加入
+ (void)load {
[SwiftTest swifttest];
}
運(yùn)行下,如圖

這是沒有攔截到swift的方法,這個時候需要怎么解決呢?
我們需要加一下配置,如圖

sanitize-coverage=func和-sanitize=undefined參數(shù),運(yùn)行,如圖

這時候可以看到swift的方法也攔截住了,這里的swift符號是經(jīng)過混淆的,這是編譯器自動添加的。
總結(jié)
這篇文章我們通過Clang的插樁實(shí)現(xiàn)了二進(jìn)制重排,并在此過程解決了很多坑,本人在這個過程中也學(xué)習(xí)到了很多知識,文章有很多不足之處,不過還是希望可以給大家?guī)碇R。
附完整的代碼
// OC插樁標(biāo)記
-fsanitize-coverage=func,trace-pc-guard
// swift插樁標(biāo)記
sanitize-coverage=func
-sanitize=undefined
ClangTrace.h代碼如下
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface ClangTrace : NSObject
/// 生成order文件
/// @param filePath 文件路徑
void generateOrderFile(NSString *filePath);
@end
NS_ASSUME_NONNULL_END
ClangTrace.m代碼如下
#import "ClangTrace.h"
#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>
#import <dlfcn.h>
#import <libkern/OSAtomic.h>
// 定義原子隊列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
// 定義符號的結(jié)構(gòu)
typedef struct {
void * pc; // 函數(shù)地址
void * next; // 下一個函數(shù)節(jié)點(diǎn)
}SymboNode;
@implementation ClangTrace
/// 生成order文件
/// @param filePath 文件路徑
void generateOrderFile(NSString *filePath) {
// 定義數(shù)組
NSMutableArray<NSString *> *sybleNames = [NSMutableArray array];
while (YES) {
SymboNode *node =OSAtomicDequeue(&symbolList, offsetof(SymboNode, next));
if (node == NULL) {
break;
}
//獲取符號信息
Dl_info info;
dladdr(node->pc, &info);
// 轉(zhuǎn)字符串
NSString *name = @(info.dli_sname);
// 區(qū)分函數(shù),block和OC方法的符號,函數(shù)與block是一樣的
NSString *symbolName = ([name hasPrefix:@"+["] || [name hasPrefix:@"-["])? name: [@"_" stringByAppendingString:name];
[sybleNames addObject:symbolName];
}
//反向遍歷數(shù)組
NSEnumerator *enumerator = [sybleNames reverseObjectEnumerator];
NSMutableArray *funArray = [NSMutableArray arrayWithCapacity:sizeof(sybleNames.count)];
// 遍歷去除重復(fù)的符號
NSString *name;
while (name = [enumerator nextObject]) {
if (![funArray containsObject:name]) {
[funArray addObject:name];
}
}
//去掉自己
[funArray removeObject:[@"_" stringByAppendingFormat:@"%s", __func__]];
// 寫入order文件
// 變成字符串
NSString *funcStr = [funArray componentsJoinedByString:@"\n"];
if ([ClangTrace isBlankString:filePath]) {
// 存儲路徑
filePath = [NSTemporaryDirectory() stringByAppendingString:@"/clangTrace.order"];
}
// 文件
NSData *file = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
// 創(chuàng)建文件
[[NSFileManager defaultManager] createFileAtPath:filePath contents:file attributes:nil];
NSLog(@"\n%@", funcStr);
}
/// 項目中的符號個數(shù)
/// @param start 起始位置
/// @param stop 結(jié)束位置
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop) {
// static uint64_t N; // Counter for the guards.
// if (start == stop || *start) return; // Initialize only once.
// printf("INIT: %p %p\n", start, stop);
// for (uint32_t *x = start; x < stop; x++)
// *x = ++N; // Guards should start from 1.
}
/// HOOK一切的回調(diào)函數(shù)
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
void *PC = __builtin_return_address(0);
// 創(chuàng)建結(jié)構(gòu)體
SymboNode *node = malloc(sizeof(SymboNode));
// 先給node賦值,下個節(jié)點(diǎn)暫時先為空
*node = (SymboNode){PC, NULL};
// 結(jié)構(gòu)體入棧,node存入symbolList,并把下一個地址給到node的next屬性
OSAtomicEnqueue(&symbolList, node, offsetof(SymboNode, next));
}
/// 判斷字符串是否為空,返回YES字符串為空,NO相反
/// @param str 字符串
+ (BOOL)isBlankString:(NSString *)str {
NSString *string = str;
if (string == nil || string == NULL) {
return YES;
}
if ([string isKindOfClass:[NSNull class]]) {
return YES;
}
if ([[string stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] length]==0) {
return YES;
}
return NO;
}
@end