背景
关于无用代码检测
在越狱设备上获取从App Store下载的包可以准确查看当台设备上的包构成(个人认为这是最准确的测算方式)。58APP的资源占比较小是因为我们主要使用xcassert存储图片,这可以充分利用分片下发的能力。如果你的图片存储依旧使用bundle存储,那么可能资源的比例会相对高一些,在这种情况下建议先将资源转存到xcassert。
混编项目无用代码检测的几种手段
OC是如何实现无用代码检测的?
Swfit的类调用
在OC的检测方案中,很大程度上是依赖classlist和classrefs做差集来实现的。其他技术手段不过是作为补充技术手段。如果没有classrefs这样一个section为我们提供主要信息,那么整个方案的技术基础就会受到动摇。那我们首先要弄清楚类如何使用会被存储到classrefs中。首先我们来看个示例:
WBBladesClass *b = nil;
id c = [WBBladesClass new];
Class d = NSClassFromString(@"WBBladesClass");
那Swift的类是不是也存在这样的特性呢?
class TestClass0: NSObject {
dynamic func hello() {
let obj = TestClass0.init()
}
}
class TestClass1 {
func hello() {
let obj = TestClass1.init()
}
}
class TestClass2 : NSObject{}
//在OC环境中调用则会被加入到classrefs中
+ (void)load{
id obj = [TestClass2 new];
}
因此可以说明classrefs只适用于OC的语言环境,即使刨除Struct、enum等类型不谈,classlist和classrefs做差集的方案也不适用于Swift的无用代码检测。
那如何才能识别出来一个Swift类型被调用呢?
struct ClassContextDescriptor{
uint32_t Flag;
uint32_t Parent;
int32_t Name;
int32_t AccessFunction;
int32_t FieldDescriptor;
int32_t SuperclassType;
uint32_t MetadataNegativeSizeInWords;
uint32_t MetadataPositiveSizeInWords;
uint32_t NumImmediateMembers;
uint32_t NumFields;
uint32_t FieldOffsetVectorOffset;
<泛型签名> //字节数与泛型的参数和约束数量有关
<MaybeAddResilientSuperclass>//有则添加4字节
<MaybeAddMetadataInitialization>//有则添加4*3字节
VTableList[]//先用4字节存储offset/pointerSize,再用4字节描述数量,随后N个4+4字节描述函数类型及函数地址。
OverrideTableList[]//先用4字节描述数量,随后N个4+4+4字节描述当前被重写的类、被重写的函数描述、当前重写函数地址。
}
struct SwiftMetadataClass {NSInteger kind;
id superclass;
NSInteger reserveword1;
NSInteger reserveword2;
NSUInteger rodataPointer;
UInt32 classFlags;
UInt32 instanceAddressPoint;
UInt32 instanceSize;
UInt16 instanceAlignmentMask;
UInt16 runtimeReservedField;
UInt32 classObjectSize;
UInt32 classObjectAddressPoint;
NSInteger nominalTypeDescriptor;
NSInteger ivarDestroyer;
...//N个函数地址
};
//通过这样的强制转换能清楚的发现TestClass2的supperclass等信息
struct SwiftMetadataClass* swiftClass =
(__bridge struct SwiftMetadataClass * )(TestClass2.self);
let tclass = TestClass1.self
bl 0x100a3b32c ; type metadata accessor for BBB.TestClass1
怎么才能知道每个函数的函数指令区间?
/*
* This is the symbol table entry structure for 64-bit architectures.
*/
struct nlist_64 {
union {
uint32_t n_strx; /* index into the string table */
} n_un;
uint8_t n_type; /* type flag, see below */
uint8_t n_sect; /* section number or NO_SECT */
uint16_t n_desc; /* see <mach-o/stab.h> */
uint64_t n_value; /* value of this symbol (or stab offset) */
};
具体做法是,首先对符号表按地址进行排序,然后把下个符号的起始地址当做当前函数的截止点。这样就实现了函数指令区间的切割。
遇到的问题
在为WBBlades做Swift适配时发现了很多有意思的问题,也是开发过程中踩到的一系列坑。
section判断不严谨。
之前字节跳动发过一篇文章《今日头条优化实践:iOS 包大小二进制优化,一行代码减少 60 MB 下载大小》,可能有些APP做了section迁移。如果APP做了section迁移的话,原本处于一个段的两个section变成了不同segment中的两个section。由于不同的段的base address可能不同,因此一旦地址计算出现跨段的时候,都需要做base address地址修正,否则文件的偏移地址可能会取错。另外,由于段名可能存在自定义的情况,因此也不能通过段名+节名的方式来确定唯一的一个section。需要通过段的权限+节名来确定section。
if ((segmentCommand.maxprot & (VM_PROT_WRITE | VM_PROT_READ)) ==
(VM_PROT_WRITE | VM_PROT_READ)) {
//能够具有读写权限的的段,即可认为为__DATA,__CONST_DATA,__AUTH_CONST等
}
当然,这种判断方式也不是完全准确,因为section迁移后,新增的段默认是读写权限,这也意味着原先的TEXT中的数据,迁移后可能变成了VM_PROT_WRITE | VM_PROT_READ。这也是段迁移后需要重新设置权限的原因。
-
获取类名循环遍历Parent可能发生异常
//类似这样的代码(Type的Parent可能不属于Type)
func extensions(of value: Any) {
struct Extensions : AnyExtensions {}
return
}
-
复杂的泛型结构
-
Anonymous布局
/// This context descriptor represents an anonymous possibly-generic context
/// such as a function body.
Anonymous = 2,
Flag(4Byte) + Parent(4Byte) + 泛型签名(不定长)+ mangleName(4Byte)
/// Flags for anonymous type context descriptors. These values are used as the
/// kindSpecificFlags of the ContextDescriptorFlags for the anonymous context.
class AnonymousContextDescriptorFlags : public FlagSet<uint16_t> {
enum {
/// Whether this anonymous context descriptor is followed by its
/// mangled name, which can be used to match the descriptor at runtime.
HasMangledName = 0,
};
...
};
-
其他
demangling cache variable for type metadata for
开头的符号。
支持范围
WBBlades做二进制扫描检测时,对APP中包含以下情况的代码作了测试。示例中的✅ 的代码能被识别为被使用到。其中V1.1是在适配Swift二进制之前,V2.0是经过适配之后。
导读
需要检测的APP需要在Debug环境下打出一个arm64真机包。
-
编译WBBlades,生成WBBlades可执行文件。https://github.com/wuba/WBBlades -
将WBBlades可执行文件拖入系统终端,并输入-unused ,再将真机包拖入终端。 -
Enter,等待几分钟,会在桌面输出结果文件。如果Swift代码较多,可能耗时较长。
应用情况及展望
总结
https://mp.weixin.qq.com/s/egrQxxJSympB-L6BdVDQVA
本文分享自微信公众号 - 58技术(architects_58)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。