iOS灵异事件集锦

前言

JavaScriptCore引擎的分析由于其复杂性,耗时性,暂时先告一段落,后续会进行回归,本篇将总结一些日常开发中遇到的问题

问题列表

启动图

之前在做视频闪屏的时候,遇到了一个比较棘手的问题:每次加载本地视频都会有一段黑屏的时间(大概0.2s左右),这本身和AVPlayer有很大的关系,猜测和视频的解码有关,无奈由于闭源,无计可施。

- (void)setupVideoPlayer
{
    NSURL* fileUrl = [NSURL fileURLWithPath:@"本地视频url"];
    AVAsset *movieAsset = [AVURLAsset URLAssetWithURL:fileUrl options:nil];
    AVPlayerItem * playerItem = [AVPlayerItem playerItemWithAsset:movieAsset];
    self.player = [AVPlayer  playerWithPlayerItem:playerItem];

    self.moviePlayer = [[AVPlayerViewController alloc] init];
    self.moviePlayer.showsPlaybackControls = NO;
    self.moviePlayer.player = self.player;
    self.moviePlayer.view.backgroundColor = self.view.backgroundColor;
    [self.view addSubview:self.moviePlayer.view];
    self.moviePlayer.view.frame = self.view.frame;
    self.moviePlayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
}

后来有童鞋想了个办法:因为闪屏是在启动图之后,所以人为地在这段黑屏的时间里,在闪屏的VC上面盖一个UIImageView,其image就是启动图,等过了这段黑屏时间,再把这个UIImageView移除,一个不错的解决方案,尽管这里的黑屏时间还是无法得到精确的控制!

然而又碰到了问题:
日常开发天天见的启动图,这个时候竟然找不到!!!在mainBundle里面找不到任何启动图资源!!!
难道要将所有的启动图像添加资源又往项目中添加了一遍吗???
我的感觉是:好残忍!每一张启动图都很大!并且有2x和3x之分,关键是还有很多尺寸…

经过调研,发现我们还是有办法拿到启动图的,只是这个时候不再是url,而是直接从内存中获取UIImage:

+ (NSString *)getLaunchImageName
{
    CGSize viewSize = [UIApplication sharedApplication].delegate.window.bounds.size;

    NSString *viewOrientation =  @"Portrait";
    if(UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation)){
        viewOrientation = @"Landscape";
    }    

    NSString *launchImageName = nil;
    NSArray* imagesDict = [[[NSBundle mainBundle] infoDictionary] valueForKey:@"UILaunchImages"];
    for (NSDictionary* dict in imagesDict)
    {
        CGSize imageSize = CGSizeFromString(dict[@"UILaunchImageSize"]);
        if (CGSizeEqualToSize(imageSize, viewSize) && [viewOrientation isEqualToString:dict[@"UILaunchImageOrientation"]])
        {
            launchImageName = dict[@"UILaunchImageName"];
        }
    }
    return launchImageName;
}

你真的在复用cell吗?

iOS开发中,cell复用是一件再正常不过的事情了,然而有许多比较复杂的cell,里面的元素个数不确定,每次拿到复用的cell都要进行各种逻辑判断,得到一个合适的cell高度,并进行cell内部的重新布局,这使得cell的复用大打折扣。有的童鞋为了避免这种麻烦,干脆在拿到cell之后,首先把内部的子视图全部移除,然后重新按照模型数据生成新的子视图,这样固然是省事多了,但是随之也带来了性能问题。
以QQ阅读的漫画书城为例:
在每次给cell设置数据的时候是这样的:

- (void)setViewModel:(id<XXXViewModelDelegate>)viewModel
{
    // 每次拿到cell的时候都会把cell上的子视图全部清理掉
    [self.subviews enumerateObjectsUsingBlock:^(XXXSubView * _Nonnull obj,
                                                        NSUInteger idx,
                                                        BOOL * _Nonnull stop) {
        [obj removeFromSuperview];
    }];

    // 然后重新初始化子视图,并设置数据
    ...
}

通过intrument测量发现,cellForRow的时间占比高达17.6%
优化前

秉着以下原则,对漫画书城的cell的逻辑进行了修改:

  • 复用cell,且cell中的每个子视图创建且仅应该创建1次;
  • 每个子视图采用懒加载的方式进行创建,即只在需要的时候进行加载;
  • 当复用的cell的子视图对于当前的数据”过剩”时,采取隐藏的方式;
  • 每次网络请求到的数据,每个数据模型的高度计算且只应该计算1次,计算之后缓存下来复用;当重新刷新后,模型数据及其高度应该全部抛弃;

之后通过intrument测量发现,发现cellForRow的时间占比已经下降到7.5%
优化后

所以不要小觑创建视图带来的性能消耗!

会说谎的屏幕尺寸

之前项目中是这样判定iPhoneX设备的

#define IS_IPHONEX                      ([UIScreen instancesRespondToSelector:@selector(currentMode)] ? CGSizeEqualToSize(CGSizeMake(1125, 2436), [[UIScreen mainScreen] currentMode].size) : NO)

这段代码看上去倒是没有什么问题,适配以来在iPhoneX上也并未出过什么问题,然而直到今年苹果爸爸又推出了新的3款iPhone设备,iPhoneXS、iPhoneXR、iPhoneXSMax,问题来了:

这3款新的设备都是刘海屏,刘海高度都是一致的,这样其UI适配和iPhoneX的适配无二异,所以按照搬砖惯性,不自觉地就会往这个宏里面加点东西:

#define IS_IPHONEX                      ([UIScreen instancesRespondToSelector:@selector(currentMode)] ?\
    (CGSizeEqualToSize(CGSizeMake(1125, 2436),  [[UIScreen mainScreen] currentMode].size) ||\
     CGSizeEqualToSize(CGSizeMake(1242, 2688),  [[UIScreen mainScreen] currentMode].size) ||\
     CGSizeEqualToSize(CGSizeMake(828, 1792) ,  [[UIScreen mainScreen] currentMode].size)  )\
     : NO)

自然这个时候IS_IPHONEX不仅仅代表iPhoneX设备,而是一系列刘海屏设备。

然而在一款新的设备iPhoneXR上,该宏总是返回NO,上了断点,才发现该机器返回的屏幕尺寸
(width = 750, height = 1624)
瞬间有一种颠覆人生观的感觉。难道买了个假的iPhoneXR ?
纠结了半天,才有同学想起来是 放大模式 的问题,去设置里面看了下,果然是!

问题找到了,所以分别针对iPhoneXR和iPhoneXSMax,继续加了两个尺寸(经过确认和其它的机型不会重复)

 (width = 750, height = 1624) iphonexr 放大模式
 (width = 1242, height = 2688) iphonexmax 正常模式

然而,这样的解决方式有很大的缺陷:

  • 代码很臃肿,判定很繁琐,并且可能考虑的不够周到,目前支持放大模式的设备还不少:iphone6/6s、iphone7/7s、iphone8/8s、iphonexr、iphonexmax都有放大模式;
  • 如果正常设备A的放大尺寸正好是设备B的正常尺寸,会导致我们的宏判定失效,即使当前没有,如果以后出了一款正常屏幕的设备,但是尺寸却和某款刘海屏的放大模式一致,也会导致判定失效

有同学给出了这样的解决方案:

#define IS_IPHONEX \
({BOOL isPhoneX = NO;\
if (@available(iOS 11.0, *)) {\
isPhoneX = [[UIApplication sharedApplication] delegate].window.safeAreaInsets.bottom > 0.0;\
}\
(isPhoneX);})

相对来说就比较简单了,用到了异形屏的安全区的概念,比较漂亮的解法!

不过也有一个缺陷:在window创建之前,这个宏是无法使用的,有的情况下,例如在load里面对IS_IPHONEX做判定,这个宏是无法做到的

实际上,屏幕会说谎,设备却不会,用下面的方法更简单准确:

- (NSString *)platform {

    size_t size;
    sysctlbyname("hw.machine", NULL, &size, NULL, 0);
    char *machine = malloc(size);
    sysctlbyname("hw.machine", machine, &size, NULL, 0);
    NSString *platform = [NSString stringWithUTF8String:machine];
    free(machine);
    return platform;
}
- (BOOL)isPhoneX
{
    NSString* plat = [self platform];

    if ([platform isEqualToString:@"iPhone10,3"] || 
        [platform isEqualToString:@"iPhone10,6"]) {
         //  modelType = DeviceModelTypeIPhoneX
        return YES;
    }
    else if([platform isEqualToString:@"iPhone11,2"]) {
        // modelType = DeviceModelTypeIPhoneXS
        return YES;
    }
    else if ([platform isEqualToString:@"iPhone11,4"] || 
             [platform isEqualToString:@"iPhone11,6"]){
        // modelType = DeviceModelTypeIPhoneXSMAX
        return YES;
    }      
    else if([platform isEqualToString:@"iPhone11,8"]){
        // modelType = DeviceModelTypeIPhoneXR;
        return YES;
    }
    return NO; 
}

再也不用担心老年机模式了,打完收工。

躺着的tableview

平常情况下,UITableView可以通过简单的设置实现SectionHeader悬浮问题,然而想要一个躺着的UITableView来实现悬浮就没那么容易了,需要借助于UICollectionView,悬浮要靠自己来实现。



这里主要实现了一个QRHorizontalFloatingHeaderLayout,它继承自UICollectionViewLayout,使用方式如下,之后需要把QRHorizontalFloatingHeaderLayoutDelegate中的代理方法实现一下就可以了:

QRHorizontalFloatingHeaderLayout *layout = [[QRHorizontalFloatingHeaderLayout alloc] init];

_collectionView = [[UICollectionView alloc]initWithFrame:frame
                                        collectionViewLayout:layout];

QRHorizontalFloatingHeaderLayout是用OC实现的,主要参照与这里的Swift版本:
https://github.com/cruzdiego/HorizontalFloatingHeaderLayout,代码的核心在于在滚动的过程中去定位FloatingHeader的位置坐标,具体细节请查看代码

实现的OC版本代码如下

@protocol QRHorizontalFloatingHeaderLayoutDelegate <NSObject>

//Item size
- (CGSize) collectionView:(UICollectionView*)collectionView horizontalFloatingHeaderItemSizeAt:(NSIndexPath*)indexPath;

//Header size
- (CGSize) collectionView:(UICollectionView*)collectionView horizontalFloatingHeaderSizeAt:(NSInteger)section;

//Section Inset
- (CGFloat) collectionView:(UICollectionView*)collectionView horizontalFloatingHeaderItemSpacingForSectionAt:(NSInteger)section;

//Item Spacing
- (CGFloat) collectionView:(UICollectionView *)collectionView horizontalFloatingHeaderColumnSpacingForSectionAt:(NSInteger)section;

// Line Spacing
- (UIEdgeInsets) collectionView:(UICollectionView*)collectionView horizontalFloatingHeaderSectionInsetAt:(NSInteger)section;

// Left Margin
- (CGFloat) collectionView:(UICollectionView*)collectionView horizontalFloatingHeaderLeftMarginForSectionAt:(NSInteger)section;
@end

@interface QRHorizontalFloatingHeaderLayout : UICollectionViewLayout

@end
#import "QRHorizontalFloatingHeaderLayout.h"

@interface QRHorizontalFloatingHeaderLayout()
@property(nonatomic,strong)NSMutableDictionary* itemsAttributes;

@property(nonatomic,assign)CGFloat currentMinX;
@property(nonatomic,assign)CGFloat currentMinY;
@property(nonatomic,assign)CGFloat currentMaxX;
@end

@implementation QRHorizontalFloatingHeaderLayout

- (instancetype)init
{
    if(self = [super init])
    {
        _itemsAttributes = [NSMutableDictionary dictionary];
        _currentMaxX = 0;
        _currentMinX = 0;
        _currentMinY = 0;
    }
    return self;
}

- (CGSize)collectionViewContentSize
{
    if(!self.collectionView){
        return CGSizeZero;
    }



    UICollectionView* collectionView = self.collectionView;
    NSInteger lastSection = collectionView.numberOfSections - 1;

    if(lastSection < 0){
        return CGSizeZero;
    }

    CGFloat contentWidth = self.lastItemMaxX + [self insetForSection:lastSection].right;
    CGFloat contentHeight =
    collectionView.bounds.size.height -
    collectionView.contentInset.top -
    collectionView.contentInset.bottom;

    return CGSizeMake(contentWidth,contentHeight);
}

- (CGFloat)lastItemMaxX
{
    UICollectionView* collectionView = self.collectionView;
    NSInteger lastSection = collectionView.numberOfSections - 1;
    NSInteger lastRow = [collectionView numberOfItemsInSection:lastSection] -1;
    UICollectionViewLayoutAttributes* lastItemAttributes =
    [self layoutAttributesForItemAtIndexPath:[NSIndexPath
                                              indexPathForRow:lastRow inSection:lastSection]];
    return lastItemAttributes ? CGRectGetMaxX(lastItemAttributes.frame) : 0;
}

- (UICollectionViewLayoutAttributes*)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
    NSIndexPath* aIndexPath = [NSIndexPath indexPathForRow:indexPath.row inSection:indexPath.section];
    return self.itemsAttributes[aIndexPath];
}

- (UICollectionViewLayoutAttributes*)layoutAttributesForSupplementaryViewOfKind:(NSString *)elementKind
                                                                    atIndexPath:(NSIndexPath *)indexPath
{
    if([elementKind isEqualToString:UICollectionElementKindSectionHeader])
    {
        return [self.sectionHeadersAttributes qr_safeObjectForKey:indexPath];
    }
    return nil;
}

- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
    return true;
}

- (UICollectionViewLayoutInvalidationContext*)invalidationContextForBoundsChange:(CGRect)newBounds
{
    UICollectionViewLayoutInvalidationContext* context = [super invalidationContextForBoundsChange:newBounds];
    CGRect oldBounds = self.collectionView.bounds;

    BOOL isSizeChanged =
    oldBounds.size.width != newBounds.size.width ||
    oldBounds.size.height != newBounds.size.height;
    if(!isSizeChanged)
    {
        NSArray* headersIndexPaths = [self.sectionHeadersAttributes allKeys];
        [context invalidateSupplementaryElementsOfKind:UICollectionElementKindSectionHeader
                                          atIndexPaths:headersIndexPaths];
    }
    return context;
}

- (void)prepareLayout
{
    [self prepareItemsAttributes];
}

- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect
{
    NSArray<UICollectionViewLayoutAttributes *> * itemsA =
    [self attributes:self.itemsAttributes containedInRect:rect];
    NSArray<UICollectionViewLayoutAttributes *> * itemsB =
    [self.sectionHeadersAttributes allValues];
    return [itemsA arrayByAddingObjectsFromArray:itemsB];
}

#pragma mark - Private


- (NSMutableDictionary<NSIndexPath*,UICollectionViewLayoutAttributes*>*)sectionHeadersAttributes
{
    UICollectionView* collectionView = self.collectionView;
    NSInteger sectionCount = collectionView.numberOfSections;
    if(!collectionView || sectionCount < 0)
    {
        return [NSMutableDictionary dictionary];
    }

    NSMutableDictionary<NSIndexPath*,UICollectionViewLayoutAttributes*>* attributes =
    [NSMutableDictionary dictionary];
    for (NSInteger section = 0; section < sectionCount; section++) {
        NSIndexPath* indexPath = [NSIndexPath indexPathForRow:0 inSection:section];
        UICollectionViewLayoutAttributes* attribute = [self attributeForSectionHeaderAtIndexPath:indexPath];
        [attributes qr_safeSetObject:attribute forKey:indexPath];
    }
    return attributes;
}

- (UICollectionViewLayoutAttributes*)attributeForSectionHeaderAtIndexPath:(NSIndexPath*)indexPath
{
    UICollectionViewLayoutAttributes* attribute =
    [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader
                                                                   withIndexPath:indexPath];

    UICollectionView* collectionView = self.collectionView;
    CGSize mySize = [self headerSizeforSection:indexPath.section];

    NSInteger itemsCount = [collectionView numberOfItemsInSection:indexPath.section];

    UICollectionViewLayoutAttributes* firstItemAttribute = [self layoutAttributesForItemAtIndexPath:indexPath];
    UICollectionViewLayoutAttributes* lastItemAttribute = [self layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForRow:itemsCount-1 inSection:indexPath.section]];

    CGPoint myPosition;
    CGFloat leftMargin = [self leftMarginForSection:indexPath.section];
    if(itemsCount > 0 && firstItemAttribute && lastItemAttribute)
    {
        CGFloat edgeX = collectionView.contentOffset.x + collectionView.contentInset.left;
        CGFloat xByLeftBoundary = MAX(edgeX+leftMargin, CGRectGetMinX(firstItemAttribute.frame));
        CGFloat xByRightBoundary = CGRectGetMaxX(lastItemAttribute.frame) - mySize.width;
        CGFloat x = MIN(xByLeftBoundary, xByRightBoundary);
        myPosition = CGPointMake(x, 0);
    }
    else
    {
        CGFloat x = [self insetForSection:indexPath.section].left;
        myPosition = CGPointMake(x, 0);
    }

    CGRect frame = CGRectMake(myPosition.x, myPosition.y, mySize.width, mySize.height);
    attribute.frame = frame;
    return attribute;
}

- (NSArray<UICollectionViewLayoutAttributes*>*)attributes:(NSDictionary*)attributes
                                          containedInRect:(CGRect)rect
{
    NSMutableArray<UICollectionViewLayoutAttributes*>* finalAttributes =
    [NSMutableArray array];
    for (UICollectionViewLayoutAttributes* attribute in attributes.allValues) {
        if(CGRectIntersectsRect(rect, attribute.frame))
        {
            [finalAttributes qr_safeAddObject:attribute];
        }
    }
    return finalAttributes;
}

- (void)prepareItemsAttributes
{

    UICollectionView* collectionView =  self.collectionView;
    NSInteger sectionCount = collectionView.numberOfSections;
    if(!collectionView || sectionCount <= 0){return;}

    [self resetAttributes];

    for (NSInteger section = 0; section < sectionCount; section++) {
        [self configureVariablesforSection:section];
        NSInteger itemCount = [collectionView numberOfItemsInSection:section];
        if(itemCount <= 0){ continue;}
        for (NSInteger row = 0; row < itemCount; row++) {
            NSIndexPath* indexPath = [NSIndexPath indexPathForRow:row inSection:section];
            UICollectionViewLayoutAttributes* attribute = [self itemAttributeAtIndexPath:indexPath];
            [self.itemsAttributes qr_safeSetObject:attribute forKey:indexPath];
        }
    }

}

- (void)resetAttributes
{
    [self.itemsAttributes removeAllObjects];
    self.currentMinX = 0;
    self.currentMaxX = 0;
    self.currentMinY = 0;
}

- (void)configureVariablesforSection:(NSInteger)section
{
    UIEdgeInsets sectionInset = [self insetForSection:section];
    UIEdgeInsets lastSectionInset = [self insetForSection:section-1];
    self.currentMinX = (self.currentMaxX + sectionInset.left + lastSectionInset.right);
    self.currentMinY = sectionInset.top + [self headerSizeforSection:section].height;
    self.currentMaxX = self.currentMinX;
}

- (UICollectionViewLayoutAttributes*)itemAttributeAtIndexPath:(NSIndexPath*)indexPath
{
    CGSize size = [self itemSizeForIndexPath:indexPath];
    CGFloat newMaxY = self.currentMinY + size.height;
    CGPoint origin = CGPointZero;
    if(newMaxY >= [self availableHeightAtSection:indexPath.section])
    {
        origin.x = self.currentMaxX + [self columnSpacingForSection:indexPath.section];
        origin.y = [self insetForSection:indexPath.section].top + [self headerSizeforSection:indexPath.section].height;
    }
    else
    {
        origin.x = self.currentMinX;
        origin.y = self.currentMinY;
    }

    CGRect frame = CGRectMake(origin.x, origin.y, size.width, size.height);
    UICollectionViewLayoutAttributes* attribute =
    [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
    attribute.frame = frame;

    // updateVariables
    self.currentMaxX = MAX(self.currentMaxX,CGRectGetMaxX(frame));
    self.currentMinX = CGRectGetMinX(frame);
    self.currentMinY = CGRectGetMaxY(frame) + [self itemSpacingForSection:indexPath.section];

    return attribute;
}

- (CGFloat)availableHeightAtSection:(NSInteger)section
{
    if(!self.collectionView || section < 0){
        return 0.0f;
    }
    UIEdgeInsets sectionInset = [self insetForSection:section];
    UIEdgeInsets contentInset = self.collectionView.contentInset;
    CGFloat totalInset = sectionInset.top + sectionInset.bottom + contentInset.top + contentInset.bottom;
    return self.collectionView.bounds.size.height - totalInset;
}

- (CGSize)headerSizeforSection:(NSInteger)section
{
    if(section < 0 || !self.collectionView)
    {
        return CGSizeZero;
    }

    id<QRHorizontalFloatingHeaderLayoutDelegate> delegate =
    (id<QRHorizontalFloatingHeaderLayoutDelegate>)(self.collectionView.delegate);
    if([delegate respondsToSelector:@selector(collectionView:horizontalFloatingHeaderSizeAt:)])
    {
        return [delegate collectionView:self.collectionView horizontalFloatingHeaderSizeAt:section];
    }

    return CGSizeZero;

}

- (CGFloat)itemSpacingForSection:(NSInteger)section
{
    if(!self.collectionView || section < 0)
    {
        return 0.0f;
    }

    id<QRHorizontalFloatingHeaderLayoutDelegate> delegate =
    (id<QRHorizontalFloatingHeaderLayoutDelegate>)(self.collectionView.delegate);
    if([delegate respondsToSelector:@selector(collectionView:horizontalFloatingHeaderItemSpacingForSectionAt:)])
    {
        return [delegate collectionView:self.collectionView horizontalFloatingHeaderItemSpacingForSectionAt:section];
    }

    return 0.0f;
}


- (CGFloat)leftMarginForSection:(NSInteger)section
{
    if(!self.collectionView || section < 0)
    {
        return 0.0f;
    }

    id<QRHorizontalFloatingHeaderLayoutDelegate> delegate =
    (id<QRHorizontalFloatingHeaderLayoutDelegate>)(self.collectionView.delegate);
    if([delegate respondsToSelector:@selector(collectionView:horizontalFloatingHeaderLeftMarginForSectionAt:)])
    {
        return [delegate collectionView:self.collectionView horizontalFloatingHeaderLeftMarginForSectionAt:section];
    }

    return 0.0f;
}

- (CGFloat)columnSpacingForSection:(NSInteger)section
{
    if(!self.collectionView || section < 0)
    {
        return 0.0f;
    }

    id<QRHorizontalFloatingHeaderLayoutDelegate> delegate =
    (id<QRHorizontalFloatingHeaderLayoutDelegate>)(self.collectionView.delegate);
    if([delegate respondsToSelector:@selector(collectionView:horizontalFloatingHeaderColumnSpacingForSectionAt:)])
    {
        return [delegate collectionView:self.collectionView horizontalFloatingHeaderColumnSpacingForSectionAt:section];
    }

    return 0.0f;
}

- (CGSize)itemSizeForIndexPath:(NSIndexPath*)indexPath
{
    if(!self.collectionView)
    {
        return CGSizeZero;
    }

    id<QRHorizontalFloatingHeaderLayoutDelegate> delegate =
    (id<QRHorizontalFloatingHeaderLayoutDelegate>)(self.collectionView.delegate);
    if([delegate respondsToSelector:@selector(collectionView:horizontalFloatingHeaderItemSizeAt:)])
    {
        return [delegate collectionView:self.collectionView horizontalFloatingHeaderItemSizeAt:indexPath];
    }

    return CGSizeZero;

}

- (UIEdgeInsets)insetForSection:(NSInteger)section
{
    if(section < 0 || !self.collectionView)
    {
        return UIEdgeInsetsZero;
    }

    id<QRHorizontalFloatingHeaderLayoutDelegate> delegate =
    (id<QRHorizontalFloatingHeaderLayoutDelegate>)(self.collectionView.delegate);
    if([delegate respondsToSelector:@selector(collectionView:horizontalFloatingHeaderSectionInsetAt:)])
    {
        return [delegate collectionView:self.collectionView horizontalFloatingHeaderSectionInsetAt:section];
    }

    return UIEdgeInsetsZero;
}

@end

C++的容器如何存放OC对象

日常的iOS开发中最常用的容器莫过于NSArrayNSDictionary了,然而在一些对性能要求比较场合下这两个容器明显不给力,很多人选择objective-c++这种混编模式来开发,从而使用强大的C++ STL等类库。但是objc的对象内存管理相对而言比c++对象麻烦很多,比如将objc的对象直接保存在STL容器中时,默认的并不会对该对象进行任何管理,我们需要手动的retain和release

这里直接引用大神duboleon在MRC下的解法

class ns_handle
{
private:
      typedef enum
      {
            _retain,
            _assign,
            _copy,
      }_mem_type;

public:
      static const _mem_type retain = _retain;
      static const _mem_type assign = _assign;
      static const _mem_type copy = _copy;

public:
      ns_handle(id nsObj,_mem_type mem_type = retain);
      ns_handle(const ns_handle & handle);
      ns_handle & operator=(const ns_handle & handle);
      id & operator*();
      ~ns_handle();

private:
      id m_nsObj;
};


inline ns_handle::ns_handle(id nsObj,_mem_type mem_type):m_nsObj(nsObj)
{
      switch (mem_type)
      {
            case retain:
                  [m_nsObj retain];
                  break;
            case assign:
                  break;
            case copy:
                  [m_nsObj copy];
                  break;            
            default:
                  break;
      }   
}

inline ns_handle::ns_handle(const ns_handle & handle):m_nsObj(handle.m_nsObj)
{
    [m_nsObj retain];
}

inline ns_handle & ns_handle::operator=(const ns_handle & handle)
{
    [m_nsObj release];
    m_nsObj = handle.m_nsObj;
    [m_nsObj retain];
    return *this;
}

inline id & ns_handle::operator*()
{
    return m_nsObj;
}

inline ns_handle::~ns_handle()
{
    [m_nsObj release];
}
  • 对于retain方式,即构建handle的时候原objc对象引用加一,析构的时候引用减一。
  • 对于copy方式,首先该objc对象必须继承NSCopying协议并实现CopyWithZone的方法,才可以正常使用。
  • 对于assign方式,则默认仅做赋值处理。
  • 通过重载解运算符,在调用handle的时候可以直接用handle来获取句柄中保存的objc对象。

使用如下:

NSString * s = @"Test";
ns_handle handle(s,ns_handle::copy);
std::vector<ns_handle> vec;
vec.push_back(handle);
NSLog(@"%@",*handle);

那么问题来了,如果是在ARC下,又该如何来解决呢?问题的关键在于解决在ARC如何搞定retain和release,这是个棘手的问题

  • ARC如何retain和release呢?
    首先想到了objc_retain和objc_release,然而runtime并没有暴露这两个方法的接口,所以否决了,接下来考虑到的就是修饰符:__bridge_retained__bridge_transfer,以及__bridge

ARC下,retain一个对象obj:

void *retainedThing = (__bridge_retained void *)obj; 
retainedThing = retainedThing;

ARC下,release一个对象obj:

void *retainedThing = (__bridge void *)obj; 
id unretainedThing = (__bridge_transfer id)retainedThing; 
unretainedThing = nil;

这里直接利用宏的形式统一处理ARC和MRC下的情况

#if __has_feature(objc_arc)
#define UniversalRetain(...) void *retainedThing = (__bridge_retained void *)__VA_ARGS__; retainedThing = retainedThing
#define UniversalRelease(...) void *retainedThing = (__bridge void *) __VA_ARGS__; id unretainedThing = (__bridge_transfer id)retainedThing; unretainedThing = nil
#else
#define UniversalRetain(...) [__VA_ARGS__ retain];
#define UniversalRelease(...) [__VA_ARGS__ release];
#endif
  • ARC下不能很好地处理C++里面的引用&,所以直接将下面的方法的返回值从id&修改为id
// 修改前
inline id & ns_handle::operator*()
{
    return m_nsObj;
}

// 修改后
inline id ns_handle::operator*()
{
    return m_nsObj;
}

解决了上面的两个问题,给出修改后的代码:

#ifndef NSHandle_h
#define NSHandle_h

class ns_handle
{
private:
    typedef enum
    {
        _retain,
        _assign,
        _copy,
    }_mem_type;
public:
    static const _mem_type retain = _retain;
    static const _mem_type assign = _assign;
    static const _mem_type copy = _copy;
public:
    ns_handle(id nsObj,_mem_type mem_type = retain);
    ns_handle(const ns_handle & handle);
    ns_handle & operator=(const ns_handle & handle);

    inline id operator*();

    ~ns_handle();
private:
    id m_nsObj;
};
inline ns_handle::ns_handle(id nsObj,_mem_type mem_type):m_nsObj(nsObj)
{
    switch (mem_type)
    {
        case retain:
        {
            UniversalRetain(m_nsObj);
        }
            break;
        case assign:
            break;
        case copy:
            [m_nsObj copy];
            break;
        default:
            break;
    }
}
inline ns_handle::ns_handle(const ns_handle & handle):m_nsObj(handle.m_nsObj)
{
    UniversalRetain(m_nsObj);
}
inline ns_handle & ns_handle::operator=(const ns_handle & handle)
{
#if __has_feature(objc_arc)
    m_nsObj = handle.m_nsObj;
#else
    [m_nsObj release];
    m_nsObj = handle.m_nsObj;
    [m_nsObj retain];
#endif
    return *this;
}

inline id ns_handle::operator*()
{
    return m_nsObj;
}

inline ns_handle::~ns_handle()
{
     UniversalRelease(m_nsObj);
}

#endif /* NSHandle_h */

UIScrollView中的UIButton延迟高亮问题

之前在开发的过程中,就发现UIButton的点击在UITableViewCell中会出现高亮延迟的现象,究其原因是:
当手指触摸到UIScrollView内容的一瞬间,会产生下面的动作:

1、拦截触摸事件,tracking属性变为YES
2、一个内置的计时器开始生效(默认时间间隔是150ms),用来监控在极短的事件间隔内是否发生了手指移动
3、当检测到时间间隔内手指发生了移动,UIScrollView自己触发滚动,tracking属性变为NO,手指触摸下即使有(可以响应触摸事件的)内部控件也不会再响应触摸事件。
4、当检测到时间间隔内手指没有移动,tracking属性保持YES,手指触摸下如果有(可以响应触摸事件的)内部控件,则将触摸事件传递给控件进行处理。

可以通过delaysContentTouches属性来搞定该问题

self.tableView.delaysContentTouches = NO;
for (id obj in self.tableView.subviews) {
    if ([obj respondsToSelector:@selector(setDelaysContentTouches:)]) {
        [obj setDelaysContentTouches:NO];
    }
}

惹不起的UITableViewCell

最近在做个颜色标签,感觉用UILabel很省事,想不到在UITableViewCell却遇到了麻烦,究其原因是UITableViewCell在高亮的时候会改变子视图的背景色:
优化前

但是有时候,并不想要这样的效果。

通过查阅资料,找到一种解决方案

主要思想:通过swizzle方式,分别hook UITableViewCellsetHighlighted:animated:方法,和UILablesetBackgroundColor:方法。然后在UITableViewCell高亮的时候,修改UILabelforbidSetBackgroundColor属性,来进行控制

// UITableViewCell+CellClick.m

- (void)qr_setHighlighted:(BOOL)highlighted animated:(BOOL)animated
{
    if (highlighted)
    {
        [self setupInnerLabelForbidState:YES superView:self.contentView];
    }
    else
    {
        [self setupInnerLabelForbidState:NO superView:self.contentView];
    }
    [self qr_setHighlighted:highlighted animated:animated];
}

- (void)setupInnerLabelForbidState:(BOOL)forbid superView:(UIView*)superView
{
    for (UIView *subview in superView.subviews)
    {
        if ([subview isKindOfClass:[UILabel class]])
        {
            // 如果设置了firbidSetBackgroundColorWhenHighlighted属性,则不允许改变
            // UILabel的背景色值
            UILabel *label = (UILabel *)subview;
            if(label.firbidSetBackgroundColorWhenHighlighted)
                label.forbidSetBackgroundColor = forbid;
        }
        else if(subview.class.isCustomClass)
        {
            // 递归遍历子视图
            [self setupInnerLabelForbidState:forbid superView:subview];
        }
    }
}
//  UILabel+CellClick.m
- (void)qr_setBackgroundColor:(UIColor *)backgroundColor
{
    // 如果没有禁止,就可以调用原来的改变颜色方法
    if (!self.forbidSetBackgroundColor){
        [self qr_setBackgroundColor:backgroundColor];
    }
}

这样的解决方案略显沉重,为了一个小小的背景色,竟然要付出这么高的代价,实在有些得不偿失:要知道每个UITableViewCell初始化的时候,都要调用setBackgroundColor:方法,此外还带着入侵性属性:firbidSetBackgroundColorWhenHighlighted,外部必须传这个属性值来告知是否要执行上面的这一套逻辑。

最关键的是这个方案没有彻底解决问题,虽然躲过了UITableViewCell的高亮逻辑,然而工程项目中有些自定义的控件(也有自己的一套高亮逻辑),却不能幸免,只能说是一个不是很完美的解决方案。

该问题最根本的原因就是根本控制不住别人来调用视图的setBackgroundColor:方法
如何化解这个大招呢?

如果实现一个自定义控件,设置好setBackgroundColor:方法的调用权限,不就好了吗?

QRCellLabel为例:

1、定义一个内部类:QRCellInnerLabel(继承自UILabel),设置shouldSetBackgroundColor属性,来控制setBackgroundColor:的调用权限

@interface QRCellInnerLabel : UILabel
@property(nonatomic,assign) BOOL shouldSetBackgroundColor;
@end

@implementation QRCellInnerLabel
- (void)setBackgroundColor:(UIColor *)backgroundColor
{
    if(_shouldSetBackgroundColor)
    {
       [super setBackgroundColor:backgroundColor];
    }
}

@end

2、定义一个QRCellLabel类,和UILabel的各种属性保持一致,方便调用,编译级别禁止调用setBackgroundColor:方法,并暴露自己的颜色设置方法qr_setBackgroundColor:

@interface QRCellLabel : UIView

@property(nullable, nonatomic,copy)                 NSString *text;
@property(null_resettable, nonatomic,strong)        UIFont *font;
@property(null_resettable, nonatomic,strong)        UIColor *textColor;
@property(nonatomic,assign)                         NSTextAlignment textAlignment;
@property(nonatomic,assign)                         NSLineBreakMode lineBreakMode;

// 注意NS_UNAVAILABLE,编译级别禁止调用该方法
- (void)setBackgroundColor:(UIColor *_Nullable)backgroundColor NS_UNAVAILABLE;

// 暴露自己的设置背景颜色方法
- (void)qr_setBackgroundColor:(UIColor *_Nullable)backgroundColor;

// 如果有不够用的属性,在下面添加,然后在.m文件中重新set方法
@end

3、QRCellLabel包括一个子控件QRCellInnerLabel,分别实现QRCellLabelsetBackgroundColor:方法和qr_setBackgroundColor:方法:

  • setBackgroundColor:的实现为空方法,这样即使外侧调进来,也无法改变控件的背景色;
  • qr_setBackgroundColor:的实现如下:每次调用的时候,将shouldSetBackgroundColor的属性打开,一旦设置完毕QRCellInnerLabel的背景色,立刻将shouldSetBackgroundColor的属性关闭,所以该控件的背景色只能由自己暴露的qr_setBackgroundColor:方法来改变
@implementation QRCellLabel
{
    QRCellInnerLabel* _label;
}

- (void)setBackgroundColor:(UIColor *)backgroundColor
{
    // 什么都不做,成功躲过大招
}


- (void)qr_setBackgroundColor:(UIColor *)backgroundColor
{
    _label.shouldSetBackgroundColor = YES;
    _label.backgroundColor = UIColor.clearColor;
    _label.shouldSetBackgroundColor = NO;
}


// 各种setter方法的重写

- (void)setFont:(UIFont *)font{
    _label.font = font;
}

- (void)setText:(NSString *)text{
    _label.text = text;
}

- (void)setLineBreakMode:(NSLineBreakMode)lineBreakMode{
    _label.lineBreakMode = lineBreakMode;
}

- (void)setTextAlignment:(NSTextAlignment)textAlignment{
    _label.textAlignment = textAlignment;
}

- (void)setTextColor:(UIColor *)textColor{
    _label.textColor = textColor;
}

...

这样,无论这个控件放到哪个父视图里面,也不会被修改背景色,因为没有权限!

总结

先写到这里,后续会更新添加内容

-------------本文结束 感谢您的阅读-------------

本文标题:iOS灵异事件集锦

文章作者:lingyun

发布时间:2018年11月13日 - 22:11

最后更新:2018年12月09日 - 01:12

原始链接:https://tsuijunxi.github.io/2018/11/13/iOS灵异事件集锦/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。