iOS 瘦身

经过多次版本迭代,产生不少冗余代码和无用资源。而苹果规定今年6月提交给Appstore的应用必须支持64位,32位和64位两个架构的存在使得可执行文件增加了一倍多。安装包大小优化迫在眉睫。

一、资源瘦身

资源瘦身主要是去掉无用资源和压缩资源,资源包括图片、音视频文件、配置文件以及多语言wording。无用资源是指资源在工程文件里,但没有被代码引用。
检查方法思路:用资源关键字(通常是文件名,图片资源需要去掉@2x @3x),搜索代码(一般是m\xib\sb文件),搜不到就是没有被引用。
这里需要注意的是,如果资源是在xcassets中,其代码引用的资源名就不一定是图片名称,而是imageset后缀的文件夹名称
当然,有些资源在使用过程中是拼接而成的(如loading_xxx.png),需要手工过滤,
脚本实例(py):

suffix = ".imageset"
scanImagePath = "/Users/xxx/Documents/xxx/Pro/Images.xcassets" #扫描路径
imageNamelist = []
print("开始扫描【{0}】".format(scanImagePath))

#找出所有资源名(imageset的资源都是目录,不是文件,取dirs)
for root, dirs, files in os.walk(scanImagePath):
    for file in dirs:
        if file.endswith(suffix):
            imageName = file.replace(suffix,'')
            # imagePathDir = os.path.join(root, file)
            imageNamelist.append(imageName)

#扫描代码文件
scanTargetDir = "/Users/xxx/Documents/Pro"
scanSuffix_m = ".m"
scanSuffix_xib = ".xib"
scanSuffix_sb = ".storyboard"
invalidResList = []
for name in imageNamelist :
    isExist = False
    for root, dirs, files in os.walk(scanTargetDir):
        for file in files:
            currentFilePath = os.path.join(root,file)
            if (not os.path.isdir(currentFilePath)) and (file.endswith(scanSuffix_m) or file.endswith(scanSuffix_xib) or file.endswith(scanSuffix_sb)):
                try:
                    f = open(currentFilePath, "r")
                    fileContent = f.read()
                    isFind = fileContent.find(name,0,len(fileContent))
                    if isFind != -1:
                        isExist = True
                        break
                except:
                    print ("读取失败%s",currentFilePath)
                finally:
                    f.close()
        if isExist:
            break

    if not isExist:
        print ("找到无用资源:",name)
        invalidResList.append(name)

output = open("result.txt", 'w') 
output.write("{0}".format(invalidResList))   
output.close()

当然,你也可以用一下工具来扫,https://github.com/tinymind/LSUnusedResources

二、可执行文件瘦身

1.重复代码

当一个项目在不断开发迭代、功能累加的过程中,因为业务轮转、新人加入等原因可能产生重复造轮子的问题,造成冗余代码。
一般代码的重复检查,就是扫描代码中指定行数范围内是否有相同的代码。对于客户端代码而言,由于有iOS和Android两个平台,所以需要考虑工具的通用性,必须支持objective-C和java两种语言。
基于以上原因,最后选择的工具是PMD-CPD(PMD’s Copy/Paste Detector)。此工具使用的是Karp-Rabin字符串匹配算法,支持gui,支持命令行,输出格式支持text、xml、csv等,可以很好的配合脚本语言进行二次开发,对重复率数据进行统计。

  1. 先从官网下载pmd工具包 https://sourceforge.net/projects/pmd/files/pmd/ 并解压
  2. cd进入其bin目录,执行./run.sh cpd --language ObjectiveC --minimum-tokens
    120 --files /Users/xxx/Documents/项目目录

ps:指定输出格式

./run.sh cpd  --language ObjectiveC --minimum-tokens 120 --format csv_with_linecount_per_file  --files /Users/xxx/Documents/项目目录 > codeCheck.csv

使用./run.sh cpdgui启用gui界面工具

详细参数用法可参考官网教程:https://pmd.sourceforge.io/pmd-5.5.1/usage/cpd-usage.html

参数 说明
cpd 重复代码扫描的批处理脚本
–language ObjectiveC 指定语言为OC
–minimum-tokens 100 指定被判定为重复代码的最少匹配的token数,数值100 ~ 150比较合适,越小则筛选强度越宽松
–files 指定搜索文件目录
> ~/Desktop/codeCheck.txt 将数据导出到 txt 文件

PS:
除此之外,还有很多其他检测工具如Simian

2.查找无用方法

LinkMap
查找无用selector,首先先了解下LinkMap,LinkMap文件是Xcode产生可执行文件的同时生成的链接信息,用来描述可执行文件的构造成分,包括代码段(__TEXT)和数据段(__DATA)的分布情况。只要设置Project->Build Settings->Write Link Map File为YES,并设置Path to Link Map File,build完后就可以在设置的路径看到LinkMap文件了。
每个LinkMap由3个部分组成:
1. Object files:列举可执行文件里所有.obj文件,以及每个文件的编号,如 [ 1]
/Users/luph/Library/Developer/Xcode/DerivedData/YYMobile-fpkgufbaoaunujctjgrwtzbylsll/Build/Intermediates.noindex/YYMobile.build/Debug-iphoneos/YYMobile.build/Objects-normal/arm64/YYBootingProtection.o

2. Sections:是可执行文件的段表,描述各个段在可执行文件中的偏移位置和大小。第一列是段的偏移量,第二列是段占用大小,Address(n)=Address(n-1)+Size(n-1);第三列是段类型,代码段和数据段;第四列是段名字,如__text是可执行机器码,__cstring是字符串常量。如下:

# Sections:
# Address Size Segment Section
0x100004FE0 0x03683FEC  __TEXT __text
0x103688FCC 0x0000EE74  __TEXT __stubs
0x103697E40 0x000043B0  __TEXT __stub_helper
0x10369C1F0 0x00004A08  __TEXT __const
0x1036A0BF8 0x00218EF0  __TEXT __gcc_except_tab
0x1038B9AE8 0x0001BB58  __TEXT __ustring
0x1038D5640 0x0007E908  __TEXT __unwind_info
0x103953F48 0x000000AC  __TEXT __eh_frame
0x103954000 0x00002280  __DATA __got
0x103956280 0x00009EF8  __DATA __la_symbol_ptr
0x103960178 0x00000840  __DATA __mod_init_func
0x1039609C0 0x000FD6E8  __DATA __const
0x103A5E0A8 0x00100C60  __DATA __cfstring
0x103B5ED08 0x0000F9C0  __DATA __objc_classlist
0x103B6E6C8 0x000001C8  __DATA __objc_nlclslist
  1. Symbols:
    详细描述每个obj文件在每个段的分布情况,按第二部分Sections顺序展示,例如序号1的YYBootingProtection.o文件,+[YYBootingProtection isRepaired]方法在__TEXT.__text地址是0x100004FE0,占用大小是36字节。根据序号累加每个obj文件在每个段的占用大小,从而计算出每个obj文件在可执行文件的占用大小,进而算出每个静态库、每个功能模块代码占用大小。这里要注意的地方是,由于__DATA.__bbs是代表未初始化的静态变量,Size表示应用运行时占用的堆大小,并不占用可执行文件,所以计算obj占用大小时,要排除这个段的Size
# Symbols:
# Address   Size        File  Name
0x100004FE0 0x00000024  [ 1] +[YYBootingProtection isRepaired]
0x100005004 0x0000002C  [ 1] +[YYBootingProtection setIsRepaired:]
0x100005030 0x00000024  [ 1] +[YYBootingProtection needForceUpdate]
0x100005054 0x0000002C  [ 1] +[YYBootingProtection setNeedForceUpdate:]
0x100005080 0x00000024  [ 1] +[YYBootingProtection isFixing]
0x1000050A4 0x0000002C  [ 1] +[YYBootingProtection setIsFixing:]

无用方法检测思路
以往C++在链接时,没有被用到的类和方法是不会编进可执行文件里。但Objctive-C不同,由于它的动态性,它可以通过类名和方法名获取这个类和方法进行调用,所以编译器会把项目里所有OC源文件编进可执行文件里,哪怕该类和方法没有被使用到。
结合LinkMap文件的__TEXT.__text,通过正则表达式[+|-]\[\w+ \w+\],我们可以提取当前可执行文件里所有objc类方法和实例方法(SelectorsAll)。再使用otool命令otool -v -s __DATA __objc_selrefs逆向__DATA.__objc_selrefs段,提取可执行文件里引用到的方法名(UsedSelectorsAll),我们可以大致分析出SelectorsAll里哪些方法是没有被引用的(SelectorsAll-UsedSelectorsAll),
扫描脚本(py):

import os
import re

outPath = "/Users/luph/Documents/sizetj/" #输出目录
mathoFilePaht = "/Users/luph/Documents/sizetj/Pro" #可执行文件
linkmapPath = "/Users/luph/Documents/sizetj/Pro-LinkMap-normal-arm64.txt"
selrefsFile =  outPath+"/selrefs.txt" #引用sel文件
cmd = "otool -v -s __DATA __objc_selrefs "+ mathoFilePaht +" >> "+selrefsFile
os.system(cmd) #逆向selrefs段

linkmapContent = open(linkmapPath,encoding="utf8", errors='ignore').read()
pattern = re.compile(r'[+|-]\[\w+ \w+\]') 
selall = pattern.findall(linkmapContent)

selrefsF = open(selrefsFile,encoding="utf8", errors='ignore')
selrefsList = []
for line in selrefsF.readlines():
    if '__objc_methname' in line:
        line = line.strip("\n");
        lineSplit = line.split(":")
        if  len(lineSplit)  > 0:
            selrefs = ""
            lineSplit.reverse()
            for subStr in lineSplit:
                if len(subStr) > 0:
                    selrefs = subStr
                    break
            if len(selrefs) > 0:
                selrefsList.append(selrefs)
selrefsF.close()   

output = open(outPath+"result.txt", 'w')
for sel in selall:
    print("正在扫描【{0}】".format(sel))
    selMth = sel.replace("+",'')
    selMth = selMth.replace("-",'')
    selMth = selMth.replace("[",'')
    selMth = selMth.replace("]",'')
    selL = selMth.split(" ")
    selMth = selL[1]
    isUse = False
    for selref in selrefsList:
        if  selref == selMth:
            isUse = True
            break 
    if not isUse:
        print("发现无用方法【{0}】".format(sel))
        output.write("{0}\n".format(sel))  

output.close()
print("扫描结束")
相关文章
相关标签/搜索