iOS分類 同名方法自動(dòng)檢測(cè)腳本

零. 前言

在OC的開發(fā)過程中,要盡量避免分類方法同名,否則會(huì)發(fā)生什么線上問題也很難被察覺出來(一個(gè)踩過坑的菜雞流淚打下這句話),很多開發(fā)的同學(xué)即使知道這個(gè)原理,但在實(shí)際開發(fā)過程中還是難免會(huì)產(chǎn)生腦子一抽寫下分類同名方法的情況。

特別是對(duì)于大文件的重構(gòu),一不小心就在多個(gè)分類上面都寫下了系統(tǒng)方法,什么deallocviewDidAppear,輕則影響到了原有的邏輯,重則因?yàn)?code>dealloc等被重寫,沒有及時(shí)釋放對(duì)象,引發(fā)系統(tǒng)崩潰。

所以寫個(gè)自動(dòng)檢測(cè)腳本還是有必要的,起碼能降低影響產(chǎn)品功能的不確定因素= =,這個(gè)腳本將會(huì)在編譯時(shí)自動(dòng)調(diào)用,如果檢測(cè)到分類有同名方法的話,就會(huì)編譯時(shí)報(bào)錯(cuò),將bug扼殺在開發(fā)階段中。

一. 思路

思路很簡(jiǎn)單,無非就是:

讀取工程文件.m、.mm的內(nèi)容和名字 => 取出所有文件的所有方法名 => 以基礎(chǔ)文件名(分類則會(huì)去除+xxx)為key、該文件下所有方法名的數(shù)組為Value存進(jìn)字典中 => 如果分類的方法名已經(jīng)在字典中,則視為同名方法,報(bào)錯(cuò)

二. 匹配出工程的所有方法

1. 匹配單個(gè)文件的多個(gè)實(shí)現(xiàn)

一個(gè)文件內(nèi)可以有多個(gè)implementation,不同的implementation可以有相同名字的方法,所以即便讀取到單個(gè)文件的內(nèi)容,也有取出多個(gè)實(shí)現(xiàn):

# 一個(gè)文件內(nèi)可能有多個(gè)實(shí)現(xiàn)方法
imple_regex = r'@implementation(.*?)@end'
for imple_string in re.findall(imple_regex, file_string, re.S):
    imple_name = imple_string.split('\n')[0]
    find_implementation_method_replaced(file, imple_name, imple_string)

對(duì)于一些情況,還需過濾一下,以確保獲得@implementation xxx (Category)中的xxx,還要去除一下空格:

# @implementation xxx { ... }
if imple_name.find('{') != -1:
    imple_name = imple_name[:imple_name.find('{')]

# @implementation xxx (Category)
if imple_name.find('(') != -1:
    imple_name = imple_name[:imple_name.find('(')]

# 去除空格
imple_name = imple_name.strip()
imple_name = imple_name.rstrip()

2. 匹配出完整方法

有些內(nèi)容比較簡(jiǎn)單就不說了,關(guān)鍵在于用Python如何取出所有文件的所有方法名:

對(duì)于OC來說,方法名的情況有以下幾種:

- (void)func1 {
    
}

- (NSString *)func2 {
    return nil;
}

- (void)     func3:(NSString *)params3 {

}

- (void)func4:(NSString *)param4 func4:(NSString *)param4 {

}

- (void)    func5:(NSString *)param5
        func5:(NSString *)param5 {

}

- (void)func6:(NSString *)param6
             func6:(void(^)(NSString *))param6 {

}

- (void(^)(NSString *))func7:(NSString *)param7
        func7:(void(^)(NSString *))param7
        func7:(void(^)(NSString *))param7
        func7:(void(^)(NSString *))param7
        func7:(void(^)(NSString *))param7
        func7:(void(^)(NSString *))param7 {
    return nil;
}

-(xxx)func:(xxx)param func:(xxx)param ... {},中間可能會(huì)穿插若干個(gè)空格或者回車鍵,而且還有可能有block等帶括號(hào)的變量,如何準(zhǔn)確地忽略空格、回車、block里面的括號(hào)等干擾因素,多個(gè)參數(shù)時(shí)準(zhǔn)確取出參數(shù)方法名,還有一些把+ -當(dāng)作減號(hào)加號(hào)的情況,是解決這個(gè)問題的關(guān)鍵所在。

另外,對(duì)于OC的方法,我們只需關(guān)心幾個(gè)重點(diǎn):

  • 類方法(+)還是實(shí)例方法(-)

  • 方法名字是啥(- func1:func2)

而對(duì)于參數(shù)名、返回類型,我們無需關(guān)心,因?yàn)閷?duì)于有且僅有一個(gè)名字的類方法/實(shí)例方法。

這時(shí)候,正則表達(dá)式這個(gè)大神器又得祭出來了!真是一個(gè)一行頂百行的好東西!(雖然這一行可能要想一天= =)

首先匹配到所有的類方法或者實(shí)例方法,以+或者-開頭,且以{結(jié)束,中間可能有若干個(gè)括號(hào)、空格、換行的若干情況的一段文本:

(\+|\-)\s*\([^;<>=\+\-]*?\)\s*([^;<>=\+\-]*?)\s*\{

上面的正則即可匹配到-(xxx)func:(xxx)param func:(xxx)param ... {}類似的格式,且把一些可能引起混淆的運(yùn)算字符踢了出去,以避免加減號(hào)干擾到正則了。(注意:+ load方法要排除在外)

3. 對(duì)方法進(jìn)行拆分

方法可能沒有參數(shù),也可能有多個(gè)參數(shù),這時(shí)候我們需要匹配出所有帶參數(shù)的前綴:

用下面的正則表達(dá)式,如果不匹配,則說明這個(gè)方法沒有參數(shù),如果匹配了,則取每一個(gè)匹配的參數(shù)進(jìn)行拼接即可。

(\w*?)\s*:\s*\(.*?\)

這樣我們就可以得到所有的方法了

-func1
-func2
-func3:
-func4:func4:
-func5:func5:
-func6:func6:
-func7:func7:func7:func7:func7:func7:

最后以imple_name為key,該實(shí)現(xiàn)下所有func的數(shù)組為value,即可檢查是否有分類重名方法。

三. 匹配出庫(kù)文件的所有方法

雖然在上面的步驟中,我們提取到了所有.m和.mm文件的方法,并進(jìn)行了一次檢查,但是在工程中還有一些需要檢查的地方:.a文件和.framework文件,他們是外部提供的不暴露源代碼、只暴露部分頭文件的庫(kù)文件,有些是外部提供的SDK,有些則是自身為提高編譯速度和維護(hù)性而生成的模塊化文件。

這里就有難點(diǎn)了:既然我們獲取不到里面的源代碼,那我們?cè)趺粗拦こ讨惺欠裼兄貙憥?kù)文件的方法呢?幸好,Mac平臺(tái)提供了一個(gè)命令,可以讓我們查看一個(gè)文件的符號(hào)表信息,而符號(hào)表信息中就包含了該二進(jìn)制文件里面的所有方法,所以只要我們執(zhí)行這個(gè)命令,再對(duì)生成的符號(hào)表進(jìn)行篩選,即可獲取到該庫(kù)的所有方法。

這個(gè)指令就是nm指令,具體介紹可以參考這篇文章

1. 對(duì).a文件的方法提取

當(dāng)我們對(duì)微信的SDK執(zhí)行這個(gè)指令時(shí):

nm xxx/Libs/WeChat/libWeChatSDK.a

就可以得到以下的符號(hào)表,這里截取其中一小段講解

.......

xxx/Libs/WeChat/libWeChatSDK.a(WXLogUtil.o):
---------------- t +[WXLogUtil sharedInstance]
---------------- t -[WXLogUtil .cxx_destruct]
---------------- t -[WXLogUtil configLogBlock:level:]
---------------- t -[WXLogUtil configLogDelegate:level:]
---------------- t -[WXLogUtil logBlock]
---------------- t -[WXLogUtil logDelegate]
---------------- t -[WXLogUtil logLevel]
---------------- t -[WXLogUtil printLog:level:]
---------------- t -[WXLogUtil removeLog]
---------------- t -[WXLogUtil setLogBlock:]
---------------- t -[WXLogUtil setLogDelegate:]
---------------- t -[WXLogUtil setLogLevel:]
                 U _OBJC_CLASS_$_NSObject
---------------- D _OBJC_CLASS_$_WXLogUtil
---------------- S _OBJC_IVAR_$_WXLogUtil._logBlock
---------------- S _OBJC_IVAR_$_WXLogUtil._logDelegate
---------------- S _OBJC_IVAR_$_WXLogUtil._logLevel
                 U _OBJC_METACLASS_$_NSObject
---------------- D _OBJC_METACLASS_$_WXLogUtil
                 U __NSConcreteStackBlock
---------------- t ___27+[WXLogUtil sharedInstance]_block_invoke
                 U ___CFConstantStringClassReference
---------------- W ___block_descriptor_40_e8__e5_v8?0l
---------------- W ___copy_helper_block_e8_
---------------- W ___destroy_helper_block_e8_
                 U __objc_empty_cache
                 U _dispatch_once
---------------- d _instance
                 U _objc_getProperty
                 U _objc_msgSend
                 U _objc_setProperty

......

上面的格式是“符號(hào)值 符號(hào)類型 符號(hào)名”,符號(hào)類型的意義在剛剛提到的那篇文章也講得很清楚了,在這里我就直接照抄了。


1)U,未定義符號(hào)

表示這個(gè)符號(hào)沒有在本文件中定義,需要解析別的文件從而找出對(duì)應(yīng)符號(hào)的定義。

例如,當(dāng)前文件調(diào)用另一個(gè)文件中定義的函數(shù)或者全局變量,這個(gè)被調(diào)用的函數(shù)或全局變量在當(dāng)前文件中就是未定義的。(但是,在定義它的文件中,如果是函數(shù)則對(duì)應(yīng)的類型是T,而如果是全局變量則其符號(hào)類型為C)。

2)A,絕對(duì)符號(hào)

表示該符號(hào)的值是絕對(duì)的,在以后的鏈接過程中,不允許進(jìn)行改變。這種類型的符號(hào)常常出現(xiàn)在中斷向量表中,例如用符號(hào)來表示各個(gè)中斷向量函數(shù)在中斷向量表中的位置。

3)T,定義在__TEXT段__text區(qū)(代碼區(qū))中的符號(hào)

表示該符號(hào)位于代碼區(qū)中,其值表示該符號(hào)在整個(gè)文件當(dāng)中的所處的位置。

有點(diǎn)奇怪的是符號(hào)“__mh_execute_header”竟然類型也為T,算作在代碼區(qū)定義的符號(hào)。

4)D,定義在__DATA段__data區(qū)中的符號(hào)

表明該符號(hào)位于初始化數(shù)據(jù)區(qū)中,其值表示該符號(hào)在整個(gè)文件當(dāng)中的所處的位置。

5)B,定義在__DATA段__bss區(qū)中的符號(hào)

表明該符號(hào)位于非初始化數(shù)據(jù)區(qū)中,其值表示該符號(hào)在bss段中的偏移。

6)C,所謂的普通(Common)符號(hào),定義在__DATA段__common區(qū)中的符號(hào)

普通符號(hào)是定義在一個(gè)未初始化數(shù)據(jù)段內(nèi)的符號(hào)。該符號(hào)沒有包含于一個(gè)普通的區(qū)中,只有在鏈接過程中才進(jìn)行分配,符號(hào)的值表示該符號(hào)需要的字節(jié)數(shù)。例如在一個(gè)C文件中,定義int test,并且該符號(hào)在別的地方會(huì)被引用,則該符號(hào)類型即為C,否則其類型為B。

7)I,間接符號(hào)

說明這個(gè)符號(hào)是僅僅是對(duì)另一個(gè)符號(hào)的間接引用。

8)S,其它符號(hào)

定義在除前所述其它地方的符號(hào),例如出現(xiàn)在__TEXT段__const區(qū)中的符號(hào)。


但其實(shí)這些在這次的需求中都不重要,我們只需關(guān)注第三列:符號(hào)名,相信在上面的符號(hào)表中我們也能看出點(diǎn)端倪出來了,那么,怎么排除其他干擾因素,只要第三列呢?我們只需加個(gè)參數(shù)就可以了:

nm -j xxx/Libs/WeChat/libWeChatSDK.a

于是乎,我們就得到了純符號(hào)名,可以獲取到庫(kù)的符號(hào)了!

/xxx/Libs/WeChat/libWeChatSDK.a(WXLogUtil.o):
+[WXLogUtil sharedInstance]
-[WXLogUtil .cxx_destruct]
-[WXLogUtil configLogBlock:level:]
-[WXLogUtil configLogDelegate:level:]
-[WXLogUtil logBlock]
-[WXLogUtil logDelegate]
-[WXLogUtil logLevel]
-[WXLogUtil printLog:level:]
-[WXLogUtil removeLog]
-[WXLogUtil setLogBlock:]
-[WXLogUtil setLogDelegate:]
-[WXLogUtil setLogLevel:]
_OBJC_CLASS_$_NSObject
_OBJC_CLASS_$_WXLogUtil
_OBJC_IVAR_$_WXLogUtil._logBlock
_OBJC_IVAR_$_WXLogUtil._logDelegate
_OBJC_IVAR_$_WXLogUtil._logLevel
_OBJC_METACLASS_$_NSObject
_OBJC_METACLASS_$_WXLogUtil
__NSConcreteStackBlock
___27+[WXLogUtil sharedInstance]_block_invoke
___CFConstantStringClassReference
___block_descriptor_40_e8__e5_v8?0l
___copy_helper_block_e8_
___destroy_helper_block_e8_
__objc_empty_cache
_dispatch_once
_instance
_objc_getProperty
_objc_msgSend
_objc_setProperty
_sharedInstance.onceToken

再觀察一下,純方法的格式無非就是以+/-開頭,[Class method]形式的文本(在這里要注意一下,一定是本行以+/-開頭,因?yàn)?code>___27+[WXLogUtil sharedInstance]_block_invoke雖然也符合符號(hào)表的格式,但他代表的是該方法被調(diào)用的回調(diào),并不是寫在代碼里面的方法。

好了,這就是很簡(jiǎn)單的一個(gè)文本處理了:讀取每一行,看到+/-開頭的再提取,且需要符合[Class method]形式。

另外需要注意的一個(gè)方法是.cxx_destruct方法,有些符號(hào)表會(huì)有如-[WXLogUtil .cxx_destruct]這樣的方法,這個(gè)并不是我們代碼文件寫出來的,而是ARC下編譯器插入的.cxx_desctruct方法,目的是為了自動(dòng)釋放ARC下對(duì)象的成員變量。所以這個(gè)需要我們?nèi)ゼ尤敕椒ǖ陌酌麊危寵z查器不去檢查這個(gè)方法,如果有興趣研究的可以看看Sunny大神的這篇詳解。

2. 對(duì).framework文件的方法提取

現(xiàn)在我們提取到了.a文件的所有方法,但如果我們需要對(duì).framework文件進(jìn)行這個(gè)指令的話,應(yīng)該怎么辦呢?你可能想到同樣直接nm -j完事:

nm -j xxx/xxxLib.framework

那么就會(huì)有報(bào)錯(cuò)產(chǎn)生:

error: xxx/xxxLib.framework: Is a directory.

系統(tǒng)提示:.framework文件是個(gè)文件夾來的,報(bào)錯(cuò)了。

之所以.a可以直接用這個(gè)指令,是因?yàn)?a 是單純的二進(jìn)制文件,.framework是二進(jìn)制文件+資源文件。

其中.a 不能直接使用,需要 .h文件配合,而.framework則可以直接使用。
.framework = .a + .h + sourceFile(資源文件)

右鍵.framework文件=》顯示包內(nèi)容,可以看到下面的結(jié)構(gòu):

其中,xxLib和xx.framework同名,他就是我們想要的二進(jìn)制文件,Headers是這個(gè)庫(kù)要暴露的頭文件,.bundle是一些資源文件(如圖片文件、鏈接的一些其他庫(kù)等等)。

現(xiàn)在,我們就可以知道怎么獲得.framework文件的符號(hào)表了,就是在.framework文件后面加個(gè)同名的路徑就可以了:

nm -j xxx/xxxLib.framework/xxxLib

至此,我們就可以讀取到庫(kù)里面的所有方法了,再也不用擔(dān)心覆蓋了庫(kù)文件的方法了!

def find_file(rootDir):
    for lists in os.listdir(rootDir):
        if (lists == "Third-Party"):
            continue
        path = os.path.join(rootDir, lists)
        file_name = path.split('/')[-1]
        file_type = file_name.split('.')[-1]
        if file_type == 'm' or file_type == 'mm':
            handle_file(path)
        elif file_type == 'framework':
            handle_framework(path, file_name)
        elif file_type == 'a':
            handle_lib(path)
        if os.path.isdir(path):
            find_file(path)


# .framework文件
def handle_framework(path, framework_name):
    framework_path = path + '/' + framework_name.split('.')[0]
    handle_lib(framework_path)
    

# .a文件或者是.framework文件
def handle_lib(path):
    cmd = 'nm -j ' + path
    output_string = commands.getoutput(cmd)

    for line in output_string.split('\n'):
        find_lib_method_replaced(path, line)


def find_lib_method_replaced(path, line):
    if len(line) < 6:
        #-[A b]
        return
    if line[0] == '-' or line[0] == '+':
        if line[1] == '[' and line[-1] == ']':
            space_index = line.find(' ')
            if space_index != -1:
                imple_name = line[2 : space_index]
                method_name = line[space_index + 1 : -1]
                method_name = line[0] + method_name
                handle_method(path, imple_name, method_name)

四. 編譯時(shí)自動(dòng)運(yùn)行

其他步驟因?yàn)楸容^簡(jiǎn)單就不多說了,最后講一下怎么在編譯的時(shí)候自動(dòng)跑這個(gè)腳本

我們?cè)?code>Build Phase加一個(gè).sh腳本,這個(gè).sh腳本用于運(yùn)行.py腳本,如果python腳本有輸出,則會(huì)終止編譯

# find_error_property.py
ret=$(python "${SRCROOT}/find_category_method_replaced.py")
if [ -n "$ret" ] ; then
    echo "error: property declaration" >&2
    echo "${ret}"
    exit 1
fi

最后把上面提到的幾個(gè)方法加在分類上面試試:

成功啦!

五. 拓展

如果你實(shí)在想在不同分類上面實(shí)現(xiàn)同樣的方法(比如都想用到dealloc等方法搞事情),不妨看看這篇文章,大概原理就是自動(dòng)在同名方法前加個(gè)前綴來實(shí)現(xiàn)。

六. 成果

通過這個(gè)腳本,成功找到了工程中10處被覆蓋的分類方法,16個(gè)遷移時(shí)未被及時(shí)移除的工程文件,以及6個(gè)已經(jīng)失效的無用文件。

七. 注意事項(xiàng)

該腳本對(duì)系統(tǒng)控件的分類方法覆蓋檢測(cè)無效,不過如果重寫了系統(tǒng)的方法會(huì)有警告,這個(gè)需要留意一下。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1、隨機(jī)數(shù) 不需要隨機(jī)數(shù)種子 arc4random()%N + begin:產(chǎn)生begin~begin+N的隨機(jī)數(shù)...
    我是小胡胡123閱讀 4,407評(píng)論 0 2
  • 官網(wǎng) 中文版本 好的網(wǎng)站 Content-type: text/htmlBASH Section: User ...
    不排版閱讀 4,707評(píng)論 0 5
  • 寫在前面的話 代碼中的# > 表示的是輸出結(jié)果 輸入 使用input()函數(shù) 用法 注意input函數(shù)輸出的均是字...
    FlyingLittlePG閱讀 3,208評(píng)論 0 9
  • 這篇文章是手冊(cè)的中文譯版整理而來(英文看著太慢了,感謝前人鋪路Orz...),vim的markdown插件和實(shí)時(shí)預(yù)...
    Himryang閱讀 7,378評(píng)論 0 20
  • 2014年的蘋果全球開發(fā)者大會(huì)(WWDC),當(dāng)Craig Federighi向全世界宣布“We have new ...
    yeshenlong520閱讀 2,397評(píng)論 0 9

友情鏈接更多精彩內(nèi)容