iOS代碼瘦身實踐:刪除無用的方法

本文將提供一種靜態分析的方式,用於查找可執行文件中未使用的方法,源碼連接:xuezhulian/selectorsunrefgit

核心思路

分析Mach-o文件中的__DATA __objc_selrefs段獲得使用到的方法,經過otool找出實現的全部方法。取差集獲得未使用的方法。而後過濾setter和getter,過濾協議方法,再加上一些其它的過濾規則獲得最終的結果。github

def unref_selectors(path):
    ref_sels = ref_selectors(path)
    imp_sels = imp_selectors(path)
    protocol_sels = protocol_selectors(path)
    unref_sels = set()
    for sel in imp_sels:
        if ignore_selectors(sel):
            continue
        #protocol sels will not apppear in selrefs section
        if sel not in ref_sels and sel not in protocol_sels:
            unref_sels = unref_sels.union(filter_selectors(imp_sels[sel]))
    return unref_sels
複製代碼

使用到的方法

使用otool -v -s輸出__DATA __objc_selrefs段的信息:bash

def ref_selectors(path):
    re_selrefs = re.compile('__TEXT:__objc_methname:(.+)')
    ref_sels = set()
    lines = os.popen('/usr/bin/otool -v -s __DATA __objc_selrefs %s' % path).readlines()
    for line in lines:
        results = re_selrefs.findall(line)
        if results:
            ref_sels.add(results[0])
    return ref_sels
複製代碼

輸出示例:數據結構

00000001030f7ce8  __TEXT:__objc_methname:getMessageRequestFromQQ:
00000001030f7cf0  __TEXT:__objc_methname:SendMessageToQQRequest:
00000001030f7cf8  __TEXT:__objc_methname:responseToGetMessageFromQQ:
00000001030f7d00  __TEXT:__objc_methname:responseToShowMessageFromQQ:
複製代碼

匹配__TEXT:__objc_methname:(.+)獲得使用到的方法。app

實現的全部方法

使用otool -oV輸出可執行文件的詳細信息, 在__DATA,__objc_classlist這個段裏面記錄了類實現的方法的相關信息:post

Contents of (__DATA,__objc_classlist) section
0000000102bdc190 0x103117798 _OBJC_CLASS_$_EpisodeDetailStatusCell
           isa 0x103117770 _OBJC_METACLASS_$_EpisodeDetailStatusCell
    superclass 0x103152988 _OBJC_CLASS_$_TableViewCell
         cache 0x0 __objc_empty_cache
        vtable 0x0
          data 0x102be84c0 (struct class_ro_t *)
                    flags 0x184 RO_HAS_CXX_STRUCTORS
            instanceStart 8
             instanceSize 16
                 reserved 0x0
               ivarLayout 0x102a2a78f
                layout map: 0x01 
                     name 0x102a2a775 TTEpisodeDetailStatusCell
              baseMethods 0x102be83d0 (struct method_list_t *)
		   entsize 24
		     count 7
		      name 0x1028606b7 setupConstraintsAdditional
		     types 0x102a489fe v16@0:8
		       imp 0x10000c1a8 -[TTEpisodeDetailStatusCell setupConstraintsAdditional]
		      name 0x1028606d2 setupUpdateConstraintsAdditional
		     types 0x102a489fe v16@0:8
		       imp 0x10000c7b8 -[TTEpisodeDetailStatusCell setupUpdateConstraintsAdditional]
		      name 0x1028606f3 bindDataWithEpisode:replayInfo:
		     types 0x102a48a20 v32@0:8@16@24
		       imp 0x10000d014 -[TTEpisodeDetailStatusCell bindDataWithEpisode:replayInfo:]
		       ... ...
複製代碼

經過匹配\s*imp 0x\w+ ([+|-]\[.+\s(.+)\])獲得實現的方法,存儲的數據結構{sel:set("-[class sel]","-[class sel]")}ui

for line in os.popen('/usr/bin/otool -oV %s' % path).xreadlines():
    results = re_sel_imp.findall(line)
    if results:
        (class_sel, sel) = results[0]
        if sel in imp_sels:
            imp_sels[sel].add(class_sel)
        else:
            imp_sels[sel] = set([class_sel])
複製代碼

過濾setter和getter

直接對ivar賦值,不會觸發propertysettergetter,這些方法即便不被調用,也不可以刪除。 otool -oV能夠輸出類的protertieslistspa

baseProperties 0x102be84a8
            entsize 16
              count 1
	     name 0x10293aaa5 pinkPointView
	attributes 0x10293aab3 T@"UIView",&,N,V_pinkPointView
複製代碼

匹配baseProperties區間,經過\s*name 0x\w+ (.+)匹配類的屬性,此時也就獲得了對應的setter和getter方法。3d

#delete setter and getter methods as ivar assignment will not trigger them
if re_properties_start.findall(line):
    is_properties_area = True
if re_properties_end.findall(line):
    is_properties_area = False
if is_properties_area:
    property_result = re_property.findall(line)
    if property_result:
        property_name = property_result[0]
        if property_name and property_name in imp_sels:
            #properties layout in mach-o is after func imp
            imp_sels.pop(property_name)
            setter = 'set' + property_name[0].upper() + property_name[1:] + ':'
            if setter in imp_sels:
                imp_sels.pop(setter)
複製代碼

過濾protocol方法

協議調用的方法不會出如今__DATA __objc_selrefs這個段裏面,過濾協議方法採用的策略是找到相應的.h文件,正則匹配文件中包含的協議方法。code

def header_protocol_selectors(file_path):
    protocol_sels = set()
    file = open(file_path, 'r')
    is_protocol_area = False
    for line in file.readlines():
        #delete description
        line = re.sub('\".*\"', '', line)
        #delete annotation
        line = re.sub('//.*', '', line)
        #match @protocol
        if re.compile('\s*@protocol\s*\w+').findall(line):
            is_protocol_area = True
        #match @end
        if re.compile('\s*@end').findall(line):
            is_protocol_area = False
        #match sel
        if is_protocol_area and re.compile('\s*[-|+]\s*\(').findall(line):
            sel_content_match_result = None
            if ':' in line:
                #match sel with parameters
                sel_content_match_result = re.compile('\w+\s*:').findall(line)
            else:
                #match sel without parameters
                sel_content_match_result = re.compile('\w+\s*;').findall(line)
            if sel_content_match_result:
                protocol_sels.add(''.join(sel_content_match_result).replace(';', ''))
    file.close()
    return protocol_sels
複製代碼

系統.h文件

otool -L能夠打印可執行文件引用到的library,加上公共前綴/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk',獲得絕對路徑。使用find命令遞歸查找該目錄下全部的.h文件。

#get system librareis
lines = os.popen('otool -L ' + path).readlines()
for line in lines:
    line = line.strip()
    #delete description
    line = re.sub('\(.*\)', '', line).strip()
    if line.startswith('/System/Library/'):
        library_dir = system_base_dir + '/'.join(line.split('/')[0:-1])
        if os.path.isdir(library_dir):
            header_files = header_files.union(os.popen('find %s -name \"*.h\"' % library_dir).readlines())
複製代碼

自定義.h文件

otool -oV的輸出來看,baseProtocols會包含協議的方法,可是一些pod倉庫經過.a文件導入到宿主工程,這個時候拿不到方法的符號。最終過濾自定義協議方法的時候採用的策略和系統協議方法相同。遞歸遍歷工程目錄(腳本須要輸入的第二個參數)下的.h文件,匹配協議方法。

header_files = header_files.union(os.popen('find %s -name \"*.h\"' % project_dir).readlines())
for header_path in header_files:
    header_protocol_sels = header_protocol_selectors(header_path)
    if header_protocol_sels:
        protocol_sels = protocol_sels.union(header_protocol_sels)
複製代碼

其餘的過濾規則

根據輸出的結果,對一些系統方法進行了過濾。

def ignore_selectors(sel):
    if sel == '.cxx_destruct':
        return True
    if sel == 'load':
        return True
    return False
複製代碼

爲了過濾第三方庫的方法,只保留了帶有某些前綴的類的方法,這裏須要根據實際狀況自行修改reserved_prefixs

def filter_selectors(sels):
    filter_sels = set()
    for sel in sels:
        for prefix in reserved_prefixs:
            if sel.startswith(prefix):    
                filter_sels.add(sel)
    return filter_sels
複製代碼

最終結果保存在腳本路徑下的selectorunref.txt文件中。和以前整理過的iOS代碼瘦身實踐:刪除無用的類 同樣,這個方式只能作靜態分析,對動態調用無效,最終是否須要刪除,還須要手動確認。

相關文章
相關標籤/搜索