iOS分享世界iOS开发iOS开发进阶

iOS开发进阶:性能优化与稳定性优化实践

2022-01-03  本文已影响0人  __Null

优化实践主要包括UI界面的优化、稳定性的优化两部分,是在开发过程中对于相关问题的认知和解决方案,仅代表个人观点,如有疑问,欢迎一起探讨学习。

一、UI界面优化

在渲染流程中GPU、CPU、显示器协同工作。CPU计算好显示的内容(包括视图的创建、布局计算、图片解码、文本绘制等),再提交打给GPU进行变换、图层合成、纹理渲染,并将渲染的结果提交到帧缓冲区,等带下一次VSync信号显示到屏幕上。

针对这个问题,可以分别对CPU、GPU做一些方面的优化:

1.界面布局优化之预排版

UITableViewUICollectionView中单元格的现实需要提供给代理方法对应的高度),以快速决定后续单元格布局的位置,而单元格高度与实际渲染的数据相关。我们可以在heightForRow(...)cellForRow(...)方法中通过临时布局计算单元格的高度和实际数据的渲染,但是这样一来就进行了多次布局计算,如果界面非常复杂,这里势必会出现卡顿。

2、界面渲染优化之预解码/预渲染

图像的现实需要网络获取到图片的Data-Buffer,再解码生成Image-Buffer,而整个解码的过程是比较耗费性能的。如果有大量的网络图片需要加载,这里可能就会造成一定程度的卡顿。

关于这一点在创建的图片加载网络框架中都是有迹可循的,比如在SDWebImage框架中SDWebImageDownloaderOperation中:

//@property (strong, nonatomic, nonnull) NSOperationQueue *coderQueue; // the serial operation queue to do image decoding

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    
    [self.coderQueue addOperationWithBlock:^{
        // decode the image in coder queue, cancel all previous decoding process
        UIImage *image = SDImageLoaderDecodeImageData(imageData, self.request.URL, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context); 
    }]; 
}
3、界面渲染优化之异步绘制

根据CPU、GPU渲染原理,由于垂直同步的机制,如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。从上图中可以看到,CPU 和 GPU 不论哪个阻碍了显示流程,都会造成掉帧现象。

整个流程可以通过如下代码进行验证:

@interface NXAsyncableLayer : CALayer
@end
@implementation NXAsyncableLayer
- (void)setNeedsDisplay {
    NSLog(@"%s", __func__);
    [super setNeedsDisplay];
}

- (void)display {
    NSLog(@"%s", __func__);
    [super display];
}

- (void)drawInContext:(CGContextRef)ctx {
    NSLog(@"%s", __func__);
    [super drawInContext:ctx];
}

- (void)renderInContext:(CGContextRef)ctx{
    NSLog(@"%s", __func__);
    [super renderInContext:ctx];
}
@end

@interface NXAsyncableLabel : UILabel
@end
@implementation NXAsyncableLabel
- (void)setNeedsDisplay {
    NSLog(@"%s", __func__);
    [super setNeedsDisplay];
}

- (void)setNeedsDisplayInRect:(CGRect)rect{
    NSLog(@"%s", __func__);
    [super setNeedsDisplayInRect:rect];
}

- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
    NSLog(@"%s", __func__);
    [super drawLayer:layer inContext:ctx];
}

- (void)drawRect:(CGRect)rect {
    NSLog(@"%s", __func__);
    [super drawRect:rect];
}

- (void)displayLayer:(CALayer *)layer {
    NSLog(@"%s", __func__);

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
       //1.异步绘制,切换至子线程
       UIGraphicsBeginImageContextWithOptions(size, NO, scale);
       //2.获取当前上下文
       CGContextRef context = UIGraphicsGetCurrentContext();
       //3.进行异步绘制(略)
       //4.生成位图
       UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
       UIGraphicsEndImageContext();

       dispatch_async(dispatch_get_main_queue(), ^{
           //5.子线程完成工作,切换至主线程
           self.layer.contents = (__bridge id)image.CGImage;
        });
   });
}

+ (Class)layerClass {
    return [NXAsyncableLayer class];
}
@end
NXAsyncableLabel *label = [[NXAsyncableLabel alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
label.text = @"NXAsyncableLabel";
[self.view addSubview:label];
-[NXAsyncableLabel setNeedsDisplay]
-[NXAsyncableLayer setNeedsDisplay] 
-[NXAsyncableLayer display]
-[NXAsyncableLabel displayLayer:]
-[NXAsyncableLabel setNeedsDisplay]
-[NXAsyncableLayer setNeedsDisplay]
-[NXAsyncableLayer display]
-[NXAsyncableLayer drawInContext:]
-[NXAsyncableLabel drawLayer:inContext:]
-[NXAsyncableLabel drawRect:]
-[NXAsyncableLabel setNeedsDisplay]
-[NXAsyncableLayer setNeedsDisplay]
-[NXAsyncableLayer display]
-[NXAsyncableLayer drawInContext:]
如上归纳总结如下:
绘制流程

假如视图非常复杂(子视图较多、布局相互依赖、有大量图片需要解码),那么这个CPU+GPU的工作就可能超过1帧的时间,这样在快速滑动的过程中就会造成卡顿,接口给我们提供了优化的空间,也就是在[UIView displayLayer:]中,自己进行计算布局和绘制,整个过程中我们可以放在子线程中进行,不影响主线程处理滑动等其他的UI事务,这样就不会卡顿。需要补充一点,如果有大量的计算在整个滑动的过程中有时候会出现局部的空白,这是正常的,毕竟计算布局是在子线程中异步操作的,如果没有计算完毕则渲染出来的就会没有内容。

详细的绘制流程和原理可以参考YYKit开源框架YYAsyncLayerYYLabel的实现流程。

4、界面渲染优化之离屏渲染

离屏渲染的检测方式:选中模拟器 ->Debug -> Off Off-screen Rendered,如果使用离屏渲染会有黄色的背景,比如系统的电池。

GPU的渲染分为当前屏幕渲染(On-Screen Rendering)和离屏渲染(Off-Screen Rendering)。当前屏幕渲染的原理上面已经介绍了,在一次Vsync信号周期内CPU计算好布局等,然后将计好的内容交给GPU渲染。GPU渲染好之后就会放入帧缓冲区。所谓离屏渲染就是指GPU在当前屏幕的帧缓冲区意外开辟一个新的缓冲区进行渲染操作。

那为什么说离屏渲染耗费性能的呢?

造成离屏渲染的原因有很多,比如shouldRasterize(光栅化)、mask(遮罩层)、shadows(阴影)、EdgeAnntialiasing(抗锯齿)、cornerRadius(圆角)等等

针对以上问题我们针对性的做出优化方案:

UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
imageView.image = [UIImage imageNamed:@"test.png"];

UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:imageView.bounds byRoundingCorners:UIRectCornerAllCorners cornerRadii:imageView.bounds.size];
    
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
maskLayer.frame = imageView.bounds;
maskLayer.path = maskPath.CGPath;
imageView.layer.mask = maskLayer;
[self.view addSubview:imageView];

或者采用YYImage中对图片圆角边框的处理方式,内部处理了圆角和边框(边框宽度、颜色)等多种需求,内部使用CoreGraphics+UIBezierPath的方案,绘制圆角、绘制边框,生成新的图片。

- (UIImage *)imageByRoundCornerRadius:(CGFloat)radius
                              corners:(UIRectCorner)corners
                          borderWidth:(CGFloat)borderWidth
                          borderColor:(UIColor *)borderColor
                       borderLineJoin:(CGLineJoin)borderLineJoin{}

在实际开发中,需要注意:

二、稳定性优化

App Crash的常见类型主要包括以下几种:

1.unrecognized selector crash没有找到方法的实现

在解决这个问题的,我们先了解一下方法调用的流程:

推荐一种较为优雅的做法:我们可以创建一个"傀儡"类,动态为该类添加无法执行的Selector方法,然后用一个通用的方法作为该Selector的实现,将消息转发到该傀儡类的实例上。

2.KVO crash

KVO导致的Crash主要原因有两方面:

那么解决这个问题,就是保证KVO的观察者dealloc的时候,移除观察者,并且保证不重复添加移除观察者。所以内部维护一个观察者的关系映射是十分有必要的。
这里可以参考我的开源框架NXKitNXKVOObserver类,它的原理很简单:

open class NXKVOObserver : NSObject {
    //弱引用观察者
    public fileprivate(set) weak var observer : NSObject? = nil
    //内部维护一个被观察信息的列表[NXKVOObserver.Observation]
    public fileprivate(set) var observations = [NXKVOObserver.Observation]()
    
    //被观察者的信息实体
    open class Observation : NSObject {
        //弱引用被观察者
        weak open var object : NSObject? = nil
        open var key = ""
        open var options: NSKeyValueObservingOptions = []
        open var context: UnsafeMutableRawPointer? = nil
        open var completion : NX.Completion<String, [NSKeyValueChangeKey : Any]?>? = nil
        
        public init(object:NSObject, key:String, options:NSKeyValueObservingOptions, context:UnsafeMutableRawPointer?, completion:NX.Completion<String, [NSKeyValueChangeKey : Any]?>?) {
            self.object = object
            self.key = key
            self.options = options
            self.context = context
            self.completion = completion
        }
    }
    
    //初始化
    public init(observer:NSObject) {
        self.observer = observer
    }
    
    //添加观察者和观察者的属性:判断重复?!只有不重复的才会真正添加
    open func add(object: NSObject, key: String, options: NSKeyValueObservingOptions = [], context: UnsafeMutableRawPointer? = nil, completion:NX.Completion<String, [NSKeyValueChangeKey : Any]?>? = nil){
        if self.observations.contains(where: { kvo in return kvo.object == object && kvo.key == key}) {
            
        }
        else {
            let observation = NXKVOObserver.Observation(object: object, key: key, options:options, context: context, completion: completion)
            self.observations.append(observation)
            object.addObserver(self, forKeyPath: key, options: options, context: context)
        }
    }
    
    //移除观察者
    open func remove(object: NSObject, key: String) {
        if let index = self.observations.firstIndex(where: { kvo in return kvo.object == object && kvo.key == key}){
            self.observations.remove(at: index)
            object.removeObserver(self, forKeyPath: key)
        }
    }
    
    //移除所有观察者
    open func removeAll() {
        for observation in self.observations {
            observation.object?.removeObserver(self, forKeyPath: observation.key)
        }
        self.observations.removeAll()
    }
    
    //拦截回调:可以通过闭包回调或者通过observeValue(...)方法回调
    open override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if let __object = object as? NSObject, let observation = self.observations.first(where: { kvo in return kvo.object == __object && kvo.key == keyPath}){
            if observation.completion != nil {
                observation.completion?(observation.key, change)
            }
            else if let __observer = self.observer,__observer.responds(to: #selector(NSObject.observeValue(forKeyPath:of:change:context:))) == true {
                __observer.observeValue(forKeyPath: observation.key, of: observation.object, change: change, context: context)
            }
        }
    }
    
    deinit {
        NX.print(NSStringFromClass(self.classForCoder))
    }
}

如何使用?以WebViewController观察WebView为例:

open class NXWebViewController: NXViewController {
    ...
    open var webView = NXWebView(frame: CGRect.zero)
    //初始化
    lazy var observer : NXKVOObserver = {
        return NXKVOObserver(observer: self)
    }()

    override open func viewDidLoad() {
       super.viewDidLoad()
       ....
       //添加被观察者和属性
       self.observer.add(object:self.webView, key: "title", options: [.new, .old], context: nil, completion: nil)
       self.observer.add(object:self.webView, key: "estimatedProgress", options: [.new, .old], context: nil, completion: nil)
    }

    override open func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        
    }
    
    deinit {
        self.webView.stopLoading()
        //移除全部的被观察者
        self.observer.removeAll()
    }
}

整个结构非常轻巧,特别注意这里边的观察者observer和被观察者object都采用了弱引用,不会有循环引用的问题,那么在观察者dealloc中调用一下removeAll()即可;并且内部维护了观察者信息的列表,所有的添加、移除、回调都会查找这个列表的数据,所以不存在重复添加移除的问题了。

如果觉得在多线程中操作不安全,可以在add(...)remove(...)removeAll(...)observeValue(...)位置添加一把锁。

3.NSTimer crash

我们一般使用[NSTimer scheduledTimerWithTimeInterval:target:selector:userInfo:repeats: ]做重复性的定时任务,但是这个API会强引用target实例,默认形成循环引用。为此我们需要在合适的时机invalidate定时器,断开引用环,否则就会因为循环引用双发都无法释放,导致内存泄露,甚至无限重复调用会导致资源的浪费。
解决这个问题的关键就是在合适的时机断开引用环,这里推荐如下方案:

4.Container/NSString crash(数组越界,插nil等)

Container Crash是指NSArray/NSMutableArray/NSDictionary/NSMutableDictionary/NSCache等类的越界访问或者插入nil等错误操作造成的。
解决方案:可以swizzle对应的插值和访问方法,在swizzle的方法中做好空值和下标越界的判断即可,也可以自己定义一套C API可以更简洁的插值和读取,同时内部做好空值和越界的判断。两种方式各有利弊,前者有一定的侵入性,后者拦截不是特别彻底。
NSString和NSMutableString的崩溃闪退问题,通常是越界操作引起的,处理方式同上。

5.Bad Access crash (野指针))

野指针造成的crash是我们开发中占比较高的一个问题,Xcode提供了检测僵尸对象Zombie的机制,能够在发生野指针的时候提示出现野指针的类,从而解决开发阶段出现的野指针问题,对于线上发生的野指针问题依旧不好排查。

6.NSNotification crash

这个问题主要是由于在NSNotificationCenter添加一个对象为observer之后,如果在observer dealloc的时候,没有调用[[NSNotificationCenter defaultCenter] removeObserver:self]会导致崩溃。这个问题出现在iOS 9.0之前,高版本苹果对此做了优化,不会再有这个问题了。这个推荐在控制器等基类的dealloc方法中添加[[NSNotificationCenter defaultCenter] removeObserver:self]的调用即可。网上也有一些说法说是hook add方法,hook dealloc方法,这些都是方法,个人感觉太重了~~。

三、异常的收集

在应用启动之后会对objc运行时异常回调进行初始化,异常回调用到_objc_terminate函数:

static void _objc_terminate(void){
    if (! __cxa_current_exception_type()) {
        // No current exception.
        (*old_terminate)();
    }
    else {
        // There is a current exception. Check if it's an objc exception.
        @try {
            __cxa_rethrow();
        } @catch (id e) {
            // It's an objc object. Call Foundation's handler, if any.
            (*uncaught_handler)((id)e);
            (*old_terminate)();
        } @catch (...) {
            // It's not an objc object. Continue to C++ terminate.
            (*old_terminate)();
        }
    }
}

如果捕获到objc异常,回调用uncaught_handler(e),并将异常信息传递回去。uncaught_handler有个默认值是_objc_default_uncaught_exception_handler,该函数是空实现。在该文件中可以找到另外一个地方给uncaught_handler赋值:

/***********************************************************************
* objc_setUncaughtExceptionHandler
* Set a handler for uncaught Objective-C exceptions. 
* Returns the previous handler. 
**********************************************************************/
objc_uncaught_exception_handler objc_setUncaughtExceptionHandler(objc_uncaught_exception_handler fn){
    objc_uncaught_exception_handler result = uncaught_handler;
    uncaught_handler = fn;
    return result;
}

在Foundation层有一个

typedef void NSUncaughtExceptionHandler(NSException *exception);
FOUNDATION_EXPORT void NSSetUncaughtExceptionHandler(NSUncaughtExceptionHandler * _Nullable);

我们可以在应用启动完成后调用该函数,然后捕获异常信息,并将该信息先保存到本地,等下一次应用启动的时候再将该信息通过接口提交给服务器。

@implementation EXExceptionHandler
+ (instancetype)center {
    static dispatch_once_t t;
    static EXExceptionHandler *center = nil;
    dispatch_once(&t, ^{
        center = [[self alloc] init];
    });
    return center;
}

- (void)start{
    NSSetUncaughtExceptionHandler(&ExceptionHandler);
}

void ExceptionHandler(NSException *exception) {    
    NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:exception.userInfo];
    [userInfo setObject:exception.name forKey:EXExceptionHandlerExceptionName];
    [userInfo setObject:exception.reason forKey:EXExceptionHandlerExceptionReason];
    [userInfo setObject:exception.callStackSymbols forKey:EXExceptionHandlerExceptionCallStackSymbols];
    [userInfo setObject:@"EXException" forKey:EXExceptionHandlerExceptionFileKey];
    NSException *e = [[NSException alloc] initWithName:exception.name reason:exception.reason userInfo:userInfo];
    [EXExceptionHandler.center handleException:e];
}

- (void)handleException:(NSException *)exception{
    NSLog(@"将异常信息/设备信息/时间信息保存到本地;合适时提交到服务器:%@", exception.userInfo);
}
@end
上一篇下一篇

猜你喜欢

热点阅读