文章

iOS15线上问题总结

iOS15升级可以关注IT支架,获取最新的下载配置文件。以下下载链接从IT之家获取。

iOS/iPadOS升级文件:https://down.ruanmei.com/upload/iOS_iPadOS_15_Beta_Profile.mobileconfig

macOS升级文件:https://down.ruanmei.com/upload/macOSDeveloperBetaAccessUtility_121.dmg

watchOS 升级文件:https://down.ruanmei.com/upload/watchOS_8_Beta_Profile.mobileconfig

在浏览器中打开并下载,下载成功后系统会自动提示安装。

RestKit导致的崩溃问题解决

结论

UIImage和UIColor的RKObjectMapping占用内存太大。

分析过程

6.8号早上,就收到用户反馈

用户ID:262**21表示手机和平板更新了系统ios15,app一直打不开,APP也是刚下载的,最新版本 其他APP正常就是的不行 手机是iPhone xs max 平板是iPad air 3 辛苦核实

收到反馈之后,立即开始找iOS15系统的机子,下载Xcode13beta,一切就绪之后开始定位,分析崩溃日志,真机调试(后发现模拟器也能必现)。

  1. 崩溃日志是在组单接口的初始化上面

    /usr/bin/atos -o **.app.dSYM/.app.dSYM/Contents/Resources/DWARF/** -arch arm64 -l 0x100df8000 0x0000000100e02bfc

    [Y**rInitRequest responseDescriptor]

  2. 是内存暴涨导致的

    202169-152243

  3. 模拟器调试崩溃堆栈,和接口定义也有关系

    202169-15299

最后基本定位在,接口引起的内存暴涨,而且是在 [H****Request initialize]中执行的代码有问题,通过对关键行前后对比内存占比的方式,来过滤下,哪些接口有问题。

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
40
@implementation UIDevice(Utils)
// 获取当前任务所占用的内存(单位:MB)
- (double)usedMemory{
    struct task_vm_info info;
    mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
    
    kern_return_t kernReturn = task_info(mach_task_self(),
                                         TASK_VM_INFO,
                                         (task_info_t)&info,
                                         &count);
    if (kernReturn == KERN_SUCCESS){
        return info.phys_footprint/1024.0/1024.0;
    }
    else{
        return NSNotFound;
    }
}
@end
  
// 所有接口请求的基类
@implementation H****Request 

+ (void)initialize {
    RKObjectManager *manager = [RKObjectManager sharedManager];
    NSAssert(nil != manager, @"manager必须在使用所有网络请求创建前创建完毕");
    
    RKRequestDescriptor *requestDescriptor = [self requestDescriptor];
    if (nil != requestDescriptor) {
        [manager addRequestDescriptor:requestDescriptor];
    }
    double mem1 = [[UIDevice currentDevice] usedMemory];
    // 初始化RestKit中的,数据模型和类字段的映射
    NSArray *responseDesscriptors = [self responseDescriptors];
    double mem2 = [[UIDevice currentDevice] usedMemory];
    NSLog(@"%@ %f", mem2 - mem1);
    for (RKResponseDescriptor *responseDescriptor in responseDesscriptors) {
        [manager addResponseDescriptor:responseDescriptor];
    }
}
@end

用iOS15和iOS14.5的模拟器启动对比日志,如下

WechatIMG22

分析发现,部分接口其实在iOS14.5上面也比其他接口的占内存要大的,只是在iOS15上被放大了,增加一个占内存大于1m的条件,然后输出全部的接口占内存情况

screenshot-20210609-155110

查找发现和不占内存的接口进行对比,使用排除法去除一些字段对比,无法发现差异处。

继续深入跟踪,盯住一个接口Y****yPolicyRequest 101.417969M[self responseDescriptors]的执行逻辑,指向的是里面的,NSObject的分类方法ht_modelMapping

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
40
41
42
43
44
45
// 增加excludeModelList是为了避免递归过程中死锁的出现. 例如: Model A中有个成员变量类型仍然是Model A, 如果在取该成员变量的类型Model A的Mapping中,不添加参数excludeModelList, 就会无限递归.
+ (RKObjectMapping *)ht_modelMapping:(NSMutableArray *)excludeModelList blackPropertyList:(NSArray *)blackPropertyList hasCycle:(BOOL)hasCycle {
    RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[self class]];
    NSArray *attributesArray = [self ht_mappingAttributesArrayWithBlackList:blackPropertyList];
    if ([attributesArray count] > 0) {
        [mapping addAttributeMappingsFromArray:attributesArray];
    }
    
    // 如果出现了循环,则限制最多层数为kMaxRelationshipMappingLevel.
    if (hasCycle && [excludeModelList count] > kMaxRelationshipMappingLevel) {
        return mapping;
    }
    
    NSDictionary *customTypePropertyDic = [self ht_customTypePropertyDic];
    for (NSString *propertyName in customTypePropertyDic) {
        if (0 == [propertyName length] || [blackPropertyList containsObject:propertyName]) {
            // 属性名为空或者被显式排除,则不需要添加到Mapping中.
            continue;
        }
        
        NSString *propertyType = [customTypePropertyDic objectForKey:propertyName];
        Class modelClass = NSClassFromString(propertyType);
        if (!hasCycle && [excludeModelList containsObject:propertyType]) {
            // 如果属性已被排除,则表明出现了循环.
            // 例如,ClassA含有类型为ClassB的属性,ClassB又含有类型为ClassA的属性,那么必须控制循环的曾经,否则会在添加RelationshipMapping时无限循环.
            hasCycle = YES;
        }
        
        NSMutableArray *itemExcludeModeList = [NSMutableArray arrayWithObject:NSStringFromClass([self class])];
        if ([excludeModelList count] > 0) {
            [itemExcludeModeList addObjectsFromArray:excludeModelList];
        }
        
        // Note: 这里不需要传递blackPropertyList.
        RKObjectMapping *relationMapping = [modelClass ht_modelMapping:itemExcludeModeList hasCycle:hasCycle];
        if (nil == relationMapping) {
            continue;
        }
        
        [mapping addRelationshipMappingWithSourceKeyPath:propertyName mapping:relationMapping];
    }
    
    return mapping;
}

断点执行,发现内存占用情况挺OK的,嵌套递归查询所有类的属性,过程太长,中间在NSDictionary *customTypePropertyDic = [self ht_customTypePropertyDic];发现出现一些陌生的属性

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
{
    CIColor = CIColor;
    "PG_wantsVibrancyEffect" = "__NSCFBoolean";
    "_accessibilityNameWithLuma" = NSString;
    "_pkaxCachedApproximateColorDescription" = NSString;
    accessibilityName = NSString;
    alpha = NSNumber;
    "asc_highlightedColor" = UIColor;
    blue = NSNumber;
    debugDescription = NSString;
    description = NSString;
    dynamic = "__NSCFBoolean";
    green = NSNumber;
    hash = NSNumber;
    invert = UIColor;
    isDynamicTintColor = "__NSCFBoolean";
    pkaxApproximateColorDescription = NSString;
    pkaxDescriptionWithLuma = NSString;
    pkaxLuma = NSNumber;
    red = NSNumber;
    "safari_colorDataForSerialization" = NSData;
    "safari_grayscaleComponent" = NSNumber;
    "safari_luminance" = NSNumber;
    "safari_meetsThresholdForDarkAppearance" = "__NSCFBoolean";
    "safari_rgbColorComponents" = NSArray;
    "sf_darkenedColor" = UIColor;
    "sf_isDarkColor" = "__NSCFBoolean";
    systemColorName = NSString;
    "vk_colorWith20PercentOpacity" = UIColor;
    "vk_colorWith40PercentOpacity" = UIColor;
    "vk_colorWith60PercentOpacity" = UIColor;
    "vk_colorWith80PercentOpacity" = UIColor;
    "vk_colorWithMaxSaturation" = UIColor;
    writableTypeIdentifiersForItemProvider = NSArray;
}

马上去iOS14上看了下,也有,定位跟踪,发现这些其实是UIColor中的系统字段,然后就大胆猜想,后端接口返回数据类型基本都是基础类型和NSString,所以如果接口Model中有系统类型,比如UIColor,会不会导致问题?,马上仔细查看Y****cyPolicyRequest的返回数据,发现了有一个类的属性中有UIColor

screenshot-20210609-161102

马上进行改造,将属性改成get方法,执行,对了,占内存下来了,但是还有部分接口很占内存,最后使用回想加猜测的方式,发现另一个类中有UIImage,改造成了objc_getAssociatedObjectobjc_setAssociatedObject的方式,内存占用问题也解决了。

所以基本定位:是UIImage和UIColor导致的。

最后的解决方法是在上面的代码段方法ht_modelMapping:blackPropertyList:hasCycle:中增加过滤条件,判断如果是UIImage和UIColor就不进行获取RKObjectMapping

具体为什么系统类型的RKObjectMapping,在iOS14和iOS15上表现差异这么大,还没有深究,后续会查看下

最近24小时的OOM崩溃系统分布

202169-162526

线上问题修复效果

数据提取的是6.1号至6.15号的数据(15号全天数据还未累计完整)

screenshot-20210615-101733

screenshot-20210615-102214

screenshot-20210615-102334

导航栏背景图透明问题

202169-162631

导航栏的底图设计如下

1
[self.navigationController.navigationBar setBackgroundImage:[UIImage imageWithColor:[UIColor whiteColor] size:CGSizeMake(1, 1)] forBarMetrics:UIBarMetricsDefault];

查看层级结构图,如下

screenshot-20210609-163131

202169-163210

发现导航栏的底图的alpha是0,导致的看不见问题,而且用subviews的方式还遍历不到这个UIImageView,最后发现是_UIBarBackground_colorAndImageView1,通过将alpha设置回1来尝试解决问题

1
2
3
4
5
6
7
8
9
if (@available(iOS 15.0, *)) {
    dispatch_async(dispatch_get_main_queue(), ^{
        UIImageView *imageView = [self.navigationController.navigationBar.subviews.firstObject valueForKey:@"colorAndImageView1"];
        UIView *testView = self.navigationController.navigationBar.subviews.firstObject;
        [self addObserver:testView forKeyPath:@"colorAndImageView1" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
        [self addObserver:imageView forKeyPath:@"alpha" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
        imageView.alpha = 1;
    });
}

但是进入下一个页面再后退回来,返现问题又出现了,通过hook,[UIImageView setAlpha:]方法,来看看是哪里又给他设置回来了,发现是系统的转场动画里面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@implementation UIImageView (Test)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self nes_swizzleInstanceMethod:@selector(setAlpha:) to:@selector(nes_setAlpha:)];
    });
}

- (void)nes_setAlpha:(CGFloat)alpha {
    [self nes_setAlpha:alpha];// 条件断点:alpha == 0
}

@end

202169-163717

1
2
3
4
[_UIBarBackground updateBackground]
[UIView(Internal) _addSubview:positioned:relativeTo:]
[UIView(NEHeimdall) hmd_addSubview:]
[_UINavigationParallaxTransition animateTransition:]

对比执行iOS14系统的,并没有走到设置alpha = 0的逻辑里面来

使用runtime查看下_UIBarBackground的方法列表

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
unsigned int outCount;
Method *methodList = class_copyMethodList(NSClassFromString(@"_UIBarBackground"), &outCount);
// 遍历所有属性列表
for (int i = 0; i<outCount; i++) {
    SEL name = method_getName(methodList[i]);
    NSLog(@"%@", NSStringFromSelector(name));
}
/**
iOS15、iOS14.5都存在updateBackground方法
encodeWithCoder:
initWithCoder:
.cxx_destruct
groupName
setGroupName:
layout
setLayout:
initWithFrame:
layoutSubviews
_shadowView
_encodableSubviews
_setupBackgroundValues
_updateBackgroundViewVisiblity
frameForYOrigin:height:
_orderSubviews
_setupShadowView:effect:image:shadowColor:shadowTint:alpha:
cleanupBackgroundViews
prepareBackgroundViews
updateBackground
setCustomBackgroundView:
transition:toLayout:
transitionBackgroundViews
_backgroundEffectView
set_backgroundEffectView:
set_shadowView:
setTopAligned: 
customBackgroundView
*/

以此推断,iOS15的updateBackground方法中,新增了alpha = 1的逻辑导致的

尝试解决方案,在viewDidAppear:中将UIImageView的alpha设置回1。

本文由作者按照 CC BY 4.0 进行授权