Kevinlma的博客

物来顺应,未来不迎,当时不杂,既过不恋

0%

openUDID耗时调研

Problem

公司的某台测试机启动时间变的非常的慢,甚至达到10秒左右。但是其他大部分手机都没有问题。所以对这个手机进行了一番调查,发现是openUDID这个常用的第三方库拖慢了启动时间,于是做了一些优化,并做个总结。

History

ios5之前,开发者可以直接访问 UDID(unique device identifier)。比较准确

ios5系统,苹果禁止开发者获取UDID。各家公司开始自研相应的替代方案。所以出现了很几种方案。

初衷:Appsfire为了统一碎片化的状况,研发openUDID。鼓励大家都集成SGOpenUDID,是得不同产品之间可以共享同一个唯一标示符。

通过setPersistent来持久

1
[slotPB setPersistent:YES];

ios10之后,setPersistent被废弃

pasteboardWithName的说明:

App pasteboards returned by this method are not persistent, existing only until the app quits. Starting in iOS 10, persistent named pasteboards are deprecated. Instead use a shared container, as described in the overview for the UIPasteboard class.

Research

使用Instrument,对启动慢的设备进行检测,发现几个慢的方法中,都卡在了与openUDID的方法调用中。
主要是pasteboardWithNamedataForPasteboardType两个方法中。这个库使用比较普遍,且启动时的模块是在主线程进行,所以减慢了启动速度。

在代码中对两个方法进行swizzling,之后在系统方法前后统计时长以及调用次数。得出类似以下输出

1
2
UIPasteboard pasteboardWithName duration 1.709938 name:org.TTTrackerOpenUDID.slot.84
...
1
2
UIPasteboard dataForPasteboardType duration 15.424090 name:org.OpenUDID.slot.42 type:org.OpenUDID
...

duration单位ms. dataForPasteboardType的耗时更长。而统计调用次数可以发现,在启动缓慢的设备上该方法调用达到**500多次**, 而启动正常的总调用次数不到100次。

调用次数非常多,查看openUDID的代码,发现有循环操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
for (int n=0; n<kOpenUDIDRedundancySlots; n++) {
NSString* slotPBid = [NSString stringWithFormat:@"%@%d",kOpenUDIDSlotPBPrefix,n];
#if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR
UIPasteboard* slotPB = [UIPasteboard pasteboardWithName:slotPBid create:NO];
#else
NSPasteboard* slotPB = [NSPasteboard pasteboardWithName:slotPBid];
#endif
OpenUDIDLog(@"SlotPB name = %@",slotPBid);
if (slotPB==nil) {
// assign availableSlotPBid to be the first one available
if (availableSlotPBid==nil) availableSlotPBid = slotPBid;
} else {
NSDictionary* dict = [SGOpenUDID _getDictFromPasteboard:slotPB];

其中kOpenUDIDRedundancySlots的数值是100

设计思路:

  1. 先从UserDefault尝试读取出UDID, AppID。
  2. 每个app安装后,创建自己的AppID, 通过固定命名规则,并寻找一个没有被占用的slot,叫做availableSlotPBid
  3. 如果slot已经被占用,读取该slot粘贴板上存储的AppID,以及存储的UDID
  4. 如果有相同AppID的slot,将这个slot叫做myRedundancySlotPBid。如果有哪个slot上没有存储uuid,这个slotye 可以复用赋值给availableSlotPBid
  5. 统计已有的每个UDID被使用个数,使用个数最多的,叫做mostReliableUDID
  6. 遍历结束后,如果UserDefault里取出的UDID是空的,就使用mostReliableUDID。这个也没有就创建新的。之后存到UserDefault里。
  7. 如果有可用的availableSlotPBid, 且不存在相同AppID的slot,则将选的uuid数据也存储到availableSlotPBid粘贴板上

通过Demo验证,最新的iOS12系统上,

  1. 通过pasteboardWithName创建的粘贴板, app退出重新启动后,创建的粘贴板还在,并且同一Team里的粘贴板也可以共享。 但是不同Team之间不能共享。
  2. 调用setPersistent对结果无影响,即使设置页不能再不同Team间共享。
  3. removePasteboardWithName是有效的,可以删除Team内的粘贴板。team外的没有影响

从Api的官方描述看,这一变化是从iOS10开是的。

Why

  1. 随着测试的不断重装,每次卸载重装都会占用一个slot,并保存数据。导致每次启动后需要读取数据的pasteboard越来 越多,越来越慢
  2. 项目App中的openUDID, 只改了类名,slotKey还是原来库里的OpenUDID_slot。后果是如果有其他app也用了openUDID,而且也没有修改slotKey。其他app的安装也会增加已用粘贴板的数量。装的app阅读,启动越慢。

Case

  1. 自己App重新启动100次会怎样。
    占满了100个slot之后,会复用具有相同AppID的slot
  2. 自己App重装100次后会怎么处理?
    UserDefault被删除,所以每次启动新建一个AppID。然后获取mostReliableUDID,保存到userDefault. 100次之后后去不到可用slot,所以每次只会保存到UserDefault
  3. 自己的App新装时,发现100个slot都被其他公司app占用了怎么处理?
    这种情况和自己重装是表现是一样的。

Solution

目标:

  1. 保留UserDefault中的UDID
  2. 是众多粘贴板中只有一个跟内存中数据一致,其他的释放掉。

不会影响现有的UDID获取。也不影响mostAlibableUDID的获取。缺陷是其他某App打开后,不论升级还是新装,都有availableSlot可以用来保存

步骤:

  1. 修改slotKey.
    在升级不会改变原 openUDID获取值的情况下,尝试修改slotKey。保证只有自己的app重装才会自我影响,降低几率
  2. 获取粘贴板数据时,记录每个不同的UDID所在的第一个粘贴板slotName
  3. 增加一个阈值判断,当前使用的UDID,所使用的slot占用一定数目之后进行进行清理。
  4. 开始清理时,从Userdefault中读取使用的UDID, 之后查找其所在的第一个slotName. 找到后保留该slot,删除其他同UDID的粘贴板。
  5. 搜集项目中openUDID的不同SlotKey组,针对每组做一次冗余清理。
    目前发现的OpenUDID, TTTrackerOpenUDID, KATrackerOpenUDID, BUOpenUDID

对不同的UDID暂不处理,可能会影响其他同Team的App,虽然这种情况很少,代码中叫做isCompromised

Test

  1. 升级优化后,app自身的udid不会变
  2. 其他sdk的udid也不会变

Risk

  1. 粘贴板的名称是Team空间的。如果公司其他App也使用的是未改过名的openUDID, 那其他app所用到的粘贴板也会被清除。如果这个app只修改的关键常量,没有改逻辑,那么对该App其实也是有加速启动的效果。但如果该app修改了逻辑实现,那影响结果就不可预期了
  2. 同理,如果第三方sdk只修改了关键常量,那么是有加速效果,但如果第三方sdk中修改了实现逻辑,那结果也是不可预期。

Refer

https://techcrunch.com/2011/09/01/appsfire-announces-open-source-udid-replacement-for-ios-openudid/