走近静态库(异常错误处理)

0x00 进击的.a/.framework

那一天,人們回想起了
在它们支配下的恐懼…
以及被它们蹂躏屈辱……

0x01 Do u remember these?

1.0 Undefined symbols for architecture

1
2
3
4
5
6
7
8
9
10
11
12
Undefined symbols for architecture i386/x86_64:
"_OBJC_CLASS_$_FuckingClazz", referenced from:
objc-class-ref in ViewController.o
...
...


Undefined symbols for architecture armv7/armv7s/arm64:
"_OBJC_CLASS_$_FuckingClazz", referenced from:
objc-class-ref in ViewController.o
...
...

这TM是什么鬼东西?

要想解决这个Error,我们首先要明白为什么会出现这个错误。先来科普一下iOS设备CPU指令集:

  • i386: 对应32位模拟器的CPU
  • x86_64: 对应64位模拟器的CPU
  • armv7/armv7s: 对应32位真机设备的CPU
  • arm64: 对应64位真机设备的CPU

  知道了上面的知识,我们再来看看上面的错误描述, 意思就是说在某种CPU指令集下没有找到对应的符号(“_OBJC_CLASS_$_FuckingClazz”),出现这种错误一般是你引用的第三方库不支持某种CPU指令集,那我们就在工程中搜索一下 ‘FuckingClazz’ 看看这个东西是对应那个库。经过搜索后发现他对应libFuckingLibray.a这个库。这个时候你的心里就会飘过千万只羊驼,问候了千万次库提供方。

骂完之后平静下来,生活还得继续,代码还得写。那我们该怎么办呢?

  1. 去找库提供方让他提供支持多指令集的库(如果库提供方有着良好的程序员自我修养的话,那下面的内容你可以不用看了)。
  2. 自己动手解决

下面我们就来讲讲怎么动手解决

首先先来个进阶知识,打开终端输入以下命令:

1
$: nm a/b/libFuckingLibray.a

这个时候你会发现一堆花花绿绿的看不懂的东西,别急,找到”_OBJC_CLASS_$_FuckingClazz” 复制,在上面命令的执行结果里搜索一发。
你会看到:

1
2
3
4
libFuckingLibray.a(FuckingLibray.o) (for architecture armv7):
...
...
libFuckingLibray.a(FuckingLibray.o) (for architecture arm64):

这个时候你会发现,哦原来这个库只支持armv7和arm64两种指令集。

具体nm命令和更高级的用法,在这里就不细说了自己去问男人:

1
$: man nm

当然还有更简单的一个命令可以直接查看一个库支持哪几种CPU指令集:

1
2
$: lipo -info a/b/libFuckingLibray.a
Architectures in the fat file: libFuckingLibray.a are: armv7 arm64

好了,找到原因了,那我们来手动为这个FuckingLibray来增加i386和x86_64的支持吧。

  1. 新建一个EunuchFuckLibray的静态库工程
  2. 将所有和FuckingLibray暴露出来的头文件(.h)导入到静态库工程,并设置为public的。然后创建每个.h对应的.m文件。
  3. 为了消除一些方法没实现之类的警告,我们在.m里实现所有.h的声明的方法,方法具体实现,随便写(一般返回值为void的 可以在方法体内 NSLog(@”[FuckingLibray]:<%s> SDK不支持模块器! “,FUNCTION);,有返回值的返回0/NO/nil之类的);
  4. 将此静态库编译成支持FuckingLibray缺少的那一部分CPU指令集的库
  5. 最后我们将这个编译好的库与之前的太监库合并:

    1
    2
    $: lipo -create a/b/libFuckingLibray.a c/d/libEunuchFuckLibray.a -output ~/Desktop/libFuckingLibray.a
    #执行完这条命令后,你会发现你的桌面上多了一个libFuckingLibray.a文件
  6. 将工程中的libFuckingLibray.a替换成你刚才生成的

  7. command + R

1.2 duplicate symbol _OBJC_IVAR_$_XXXXX._opacity in

出现这种链接错误,一种的是工程中的确存在重复的文件或者全局变量定义,这种情况手动查找到删除解决,本文不再多说.另外一工程中的文件与第三方静态库中的文件重复,本文主要针对这种情况给予解决方案

1
2
3
4
5
6
duplicate symbol _OBJC_IVAR_$_MBProgressHUD._opacity in:
/Users/findertiwk/Library/Developer/Xcode/DerivedData/FuckLibrayProject-gyeissovcremddekzpznuxjsevom/Build/Intermediates.noindex/FuckLibrayProject.build/Debug-iphonesimulator/FuckLibrayProject.build/Objects-normal/x86_64/MBProgressHUD.o
/Users/findertiwk/Workspace/Github/静态库博客/FuckLibrayProject/FuckLibrayProject/Vendor/FuckLibray/libFuckingLibray.a(MBProgressHUD.o)
duplicate symbol _OBJC_IVAR_$_MBBackgroundView._effectView in:
...
...

看到这个不要慌,细心看一下错误,意思是说 _OBJC_IVAR_$_XXXXX._opacity这个符号 重复定义了.

那我们来在工程中找一下XXXXX 这个类,结果发现是一个我们引用的第三方库,再看一下,我们的工程中只有一份啊,怎么会重复定义,把这个库放到其它的工程中就没有问题.这个时候开始怀疑人生了,到底哪里出了错.

那我们只好慢慢排查,当发现我们把之前添加的某个FuckingLibray库从工程中去掉时,就能编译过了,就正常了.这下可以确定是FuckingLibray这个库在搞事情了,那我们还是通过nm来查看一下

1
$: nm libFuckingLibray.a

在结果中查找一下 XXXXX”,结果发现找到了,好了,现在可以下结论了: FuckingLibray这个库把我们工程中使用的一个第三方库也编译进去了.

如果一时找不到其它库替换这个FuckingLibray,那这下该怎么办呢,场面一度陷入江局.
下面我来告诉我怎么做:

  • 方法一: 将你工程中的 XXXX 这个库去掉, 编译+运行,完美解决
  • 方法二: 将编译进第三方库中的 XXXX这个库去掉,具体方法下面附上一个脚本:
1
2
3
4
5
6
7
8

#用法: 将要处理的第三方库放到指定文件夹下,并在此文件夹下执行下面的脚本
# 参数1: .a文件 或者xxx.framework/xxx
# 参数2: 需要删除的文件前缀

$: ./xxx.sh libFuckingLibray.a MB
#如果执行脚本没有权限,则给脚本增加权限
#$: chmod 777 xxx.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

#--------------------------------------------
# 将静态库按CPU指令集解压并删除指定前缀的.o文件,再重新打包
# param1: 静态库文件路径
# param2: CPU指令集
# param3: 要删除的文件前缀
#--------------------------------------------
function unpack {
if [ -d "${2}" ]; then
rm -rf ${2}
mkdir ${2}
else
mkdir ${2}
fi
cd ${2}
lipo ../${1} -thin ${2} -output lib-${2}.a
ar -x lib-${2}.a
rm -rf lib-${2}.a
find ./ -name "${3}*.o" -exec rm -rf {} \;
libtool -static -o ../lib-${2}.a *.o
cd ..
rm -rf ${2}
}

#$1 第一个参数,查看库所支持的CPU指令集,并将查看结果字符串赋值给var
var=$(lipo -info $1)
if [ $? != 0 ] ; then
exit 1
fi

#从结果字符串中获取CPU指令集数组
for element in ${var#*are:}
do
unpack $1 $element $2
done

#将删除指定前缀后重新打包的各指令集的库合并成
lipo -create lib-*.a -output $1
rm -rf lib-*.a

到此,问题算是解决了,大部分情况下工程也能正常编译了,但也会有一些隐患的,比如工程中使用了某个库的1.0.0版本, 而那个该死的不得不用的库把共用的库的2.0.0版本编译进去了,些时可能会出现一些API不兼容的情况,遇到这情况,要么工程升级共用的库到2.0.0版本,要么联系FuckingLibray的作者来更改实现.

1.3 unrecognized selector sent to class 0x1009beda8’

1
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '+[FuckingLibray anythingOtherOperation]: unrecognized selector sent to class 0x1009beda8'

这TM又是怎么了?

当你使用第三方库时,调用一些第三方库中的分类中的方法时,会造成这种崩溃.
具体原因参考:苹果官方文档

总结一下就是说OC没有为方法定义链接符号,只为类定义了链接符号.而分类只是一些方法的集合,链接器没有将分类中的对象加载进来,所以链接器不知道去哪找这个方法,所以就在链接阶段报错了.

知道了原因,同时苹果告诉我们解决办法是在工程Build Setting中Other Linker Flags,添加 -ObjC. 但是这个-ObjC有一个BUG,就是当第三方库中只有分类而没有分类所属的类时,链接器还是不能将分类中的方法实现加载进来.所以就轮到-all_laod出场了,这个标识符可以让链接器把目标文件都加载进来.但是苹果也说了,加入这些链接标识符号会使生成的可执行文件变大,有时候我们只想针对某个库来添加,那么就又有了-force-load出场,这个标识符号的用法和前面两个不同,要在后面跟一个路径,-force-load ${SDKPath}

好了,这下问题解决了,command + R