iOS 瘦身

版权声明:未经博主同意不得转载 https://blog.csdn.net/bluefish89/article/details/80818126

前言

App 的瘦身主要是针对于安装包,而在 iOS 中安装包就是一个以 .ipa 结尾的压缩包。我们可以通过 ipa 来分析,将ipa解压后可得到.app文件,右键可查看包内容(可执行文件、nib、storyboardc、car资源包等等)
包瘦身,大致可以从以下几类入手:

资源层面:
Assets.car:项目中所有 .xcassets 的压缩包
image: 图片资源文件
Video && Audio :音频 或者 视频。
Xib && Storyboard:Xib 和 Storyboard 编译后的文件。

代码层面:
项目可执行文件
Frameworks:Embedded Frameworks,项目中使用的动态库

一、资源瘦身

资源瘦身我们可以分为远程资源(Remote)、本地资源(Local)
Remote : 将资源文件放在服务器上,当用户下载完 App 后根据需要再下载。
Local : 将资源文件集成到安装包中的。

Remote
我们可以把 非必须的资源文件 都放到服务器上,按需加载

Local
1.压缩大图片,通过用tinypng等工具来压缩,应该是以最小的占用量达到了最适合的效果。
图片资源统一使用.xcassets 也会为你做一部分的压缩,查找大图:

查找指定类型文件,超过10k的文件,并cp到指定目录下
 find ./ -type f -size +10k -name "*.png" -exec cp {}  /Users/xxx/Documents/png \;

2.检查重复资源,这里指名字不同内容相同/相似,可以使用fdupes工具来扫描,在指定的目录及子目录中查找重复的文件。fdupes通过对比文件的MD5签名,以及逐字节比较文件来识别重复内容

brew install fdupes
fdupes -Sr 目录[ 目录...] > 输出文件.txt

3.清理本地无用资源,无用资源是指资源在工程文件里,但没有被代码引用。通过去掉无用资源和压缩资源,资源包括图片、音视频等文件来优化。
检查方法思路:用资源关键字(通常是文件名,图片资源需要去掉@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("扫描结束")

3.无用类

使用fui工具扫描:
https://github.com/dblock/fui

4.其他

**语言选择:**不推荐使用 Swift,不论纯 Swift 还是 混编,任何一个包含有 Swift 代码的 App 都有的一个为了支持 Swift 的动态库集合,在10M 左右。如果你使用 Objective - C 完全不用这个东西

三.更多相关

1.App Thinning

据Apple官方文档的介绍,App Thinning主要有三个机制

Slicing

开发者把App安装包上传到AppStore后,Apple服务会自动对安装包切割为不同的应用变体(App variant),当用户下载安装包时,系统会根据设备型号下载安装对应的单个应用变体。(你不需要做什么,iOS9.0.2以上就支持)
在这里插入图片描述

Bitcode

开启Bitcode编译后,可以使得开发者上传App时只需上传Intermediate Representation(中间件),为二进制数据表示的格式的中间码,而非最终的可执行二进制文件。 在用户下载App之前,AppStore会自动编译中间件,产生设备所需的执行文件供用户下载安装。也就是当我们提交程序到 App Store上时, Xcode 会将程序编译为一个中间表现形式( bitcode )。然后 App store 会再将这个 Bitcode 编译为可执行的64位或32位程序。苹果会根据下载应用的用户的手机指令集类型生成只有该指令集的二进制,进行下发
在这里插入图片描述

所以,通过这个方式,我们可以做到架构级别的App Slicing。

然而,一个很常见的误区是认为使用 bitcode 能优化包大小,其实启用 bitcode 作用并不大。实际上 bitcode 和包大小半毛钱关系都没有,它仅仅是把编译的最后一步留给苹果,这样苹果就可以在优化编译器后,再次将我们的应用打包,从而让历史应用也能享受到新技术
https://www.appcoda.com/app-thinning/
在文档里可看到
In fact, app slicing handles the majority of the app thinning process. ‘App Slicing’ feature finally switched on in iOS 9.0.2
说明slicing才是主要处理 app thinning的而且该功能需要在iOS9.0.2以上才支持(iOS9.0中被关闭了,因为一个iCloud的bug)。实际上Bitcode,做的事情是指令集优化。根据你设备的状态去做编译优化,进而提升性能。所以Bitcode对包的大小优化起不到什么本质上的作用。

这就好比饭店原来是把菜做好了,等顾客来了以后直接上菜。现在厨师长说:“大家买好原材料”,万一哪天我们有了新的菜谱,同样的原材料就能做出更好吃的菜,用户就经常光顾我们这里了

注意点
1.开启 Bitcode 编译后,编译产生的 .app 体积会变大(中间代码,不是用户下载的包),且 .dSYM 文件不能用来崩溃日志的符号化(用户下载的包是 Apple 服务重新编译产生的,有产生新的符号文件)
2.通过 Archive 方式上传 AppStore 的包,可以在Xcode的Organizer工具中下载对应安装包的新的dSYM符号文件。或者iTunes Connect上下载对应构建包的dSYM(需消除混淆)
详情查看:https://developer.apple.com/library/archive/technotes/tn2151/_index.html

On-Demand Resources

ORD(随需资源)是指开发者对资源添加标签上传后,系统会根据App运行的情况,动态下载并加载所需资源,而在存储空间不足时,自动删除这类资源。
这可能在游戏中应用场景会多一些。你可以用 tag 来组织像图像或者声音这样的资源,比如把它们标记为 level1,level2 这样。然后一开始只需要下载 level1 的内容,在玩的过程中再去下载 level2。或者也可以通过这个来推后下载那些需要内购才能获得的资源文件。
在这里插入图片描述
这种机制对于大多数APP来讲,看起来更像是按需加载网络图片,并作缓存处理。而On-Demand Resources只是将这个服务交由苹果来处理, 个人觉得多少显得鸡肋

2.矢量图可行性分析

PDF矢量图
一开始,大家都以为,使用矢量图就可以不需要使用1x、2x、3x图了,毕竟人家不会有失真问题。那么,使用矢量图能不能帮助iOS App减少空间呢?
试验:
1.使用pdf原始文件编译生成通用IPA
2.从生成的IPA文件中提取Asset.car文件
3.利用iOS Image Extractor提取Asset.car文件

可发现,解压后,除了PDF,还有对应的1-3x图,xcode并非直接使用PDF,而是以PDF大小为1x,生成了对应的2x、3x图,我们将解压出来的三张png图提取出来,重新打包ipa,结果对比如下:

仅PDF 3张图 PDF大小
115K 86KB 19KB

结论:
iOS对矢量图的支持其实只是一种方便开发者的选择, 本质上在XCode编译的阶段矢量图会自动生成对应Target的@1x,@2x和@3x的png格式图像,自动生成的@1x图会和矢量图的原始尺寸保持一致。在iOS实际运行中使用的图片实际上已经是png格式的图片了。所以,
PDF矢量图对App减少空间是没什么实质上的帮助的,同时iOS9后,app Slicing的作用下,最终下载到手机的资源只有对应倍率的资源。因此, 严格意义下, 利用矢量图并不能帮助App节省空间,但从便利角度来说还是有好处的,设计不需要给开发多个尺寸的图,也就只有这点好处吧- -

题外:
这里解压car使用到了iOS Image Extractor工具,我们知道xcasset的格式应该是封闭不开放的, 该工具是怎么从Asset.car中提取图片的, 难道该工具破解了Asset.car的格式?
通过浏览工程源码,我们发现 iOS Image Extrator其实是基于开源库iOS Asset Extrator开发实现的,核心提取的功能是在iOS Asset Extrator库下提取的, 笔者通过阅读其源码, 找到两个核心方法exportToDirectory:exportThemeRendition:
通过阅读这两个方法的源代码可以了解到这个库的基本实现。exportToDirectory:方法有该库核心的提取图片的所有逻辑代码。而exportThemeRendition:可以看出该库支持的所有格式, 并且通过苹果内置的各个格式的Rendition类提取导出。
iOS Asset Extrator库本质上调用的是苹果的私有API。在该系列API中, CUICommonAssetStorage负责存储Asset资源的关键key, CUICatalog是承载了具体资源图片信息的登记目录。
开源库底层既然是苹果API, 那么就基本是一个黑盒子了。既不能从暴露的API中分析出car的格式, 又不能判断iOS设备是否在执行中解压, 只好放弃~
PS:Xcode 默认自动使用 PNGCRUSH 压缩 .png 图片

摘录:
http://blog.startry.com/2016/06/15/vector-apply-to-iOS-Project/
https://juejin.im/post/5800ef71a0bb9f0058736caa

猜你喜欢

转载自blog.csdn.net/bluefish89/article/details/80818126