zoukankan      html  css  js  c++  java
  • 开源库UITableView+FDTemplateLayoutCell学习

    摘自:优化UITableViewCell高度计算Swift版优化UITableViewCell高度计算的那些事

    本文带大家详细探索那篇文章所对应的库(1.2版),这个库就是利用缓存tableviewcell的高度提高滑动的流畅性。

    主要是利用Runloop在空闲状态时,后台计算tableviewcell的高度并缓存起来。然后在使用的时候就直接从缓存中去,这里都放在一个数组里存在内存。

    对Runloop以及几个mode不懂的可以看sunnyxx blog中的视频 视频可戳 , 文章的话可以看看 深入理解RunLoop、 【iOS程序启动与运转】- RunLoop个人小结

    其实就是在kCFRunLoopDefaultMode模式下BeforWaitting状态去执行计算的。

    下面来探究源码。首先在UITableView+FDTemplateLayoutCell 下载源码,下载1.2版本。

    然后你得到的库就只有两个文件:

    .m文件大概只有500行代码。

    下面看下作者的视线思路:

    1.  创建了一个_FDTemplateLayoutCellHeightCache类,就是管理Cache的一个类,里面有两个属性四个方法。

    属性:

    • sections 这个变量就是用来存储缓存的height的一个二维数组。(因为tableview有section和row组成所以必须二维)

    • _FDTemplateLayoutCellHeightCacheAbsentValue 这个是一个静态常量,就是用来标记没有缓存高度的row 。

    方法:

    • buildHeightCachesAtIndexPathsIfNeeded:indexPaths
      这个方法传入indexPaths数组来给sections中还没有初始化的元素进行初始化
    • hasCachedHeightAtIndexPath:indexPath 根据下标索引判断是否有缓存(其实就是判断是否等于上面那个静态常量)
    • cacheHeight:height:byIndexPath 根据indexPath给sections赋值。
    • cachedHeightAtIndexPath:indexPath 根据indexPath取值

    这个类主要是操作和存储缓存的。这个类的代码如下:

    @interface _FDTemplateLayoutCellHeightCache : NSObject
    @property (nonatomic, strong) NSMutableArray *sections;  
    @end
    
    static CGFloat const _FDTemplateLayoutCellHeightCacheAbsentValue = -1;
    
    @implementation _FDTemplateLayoutCellHeightCache
    
    - (void)buildHeightCachesAtIndexPathsIfNeeded:(NSArray *)indexPaths {
        if (indexPaths.count == 0) {
            return;
        }
        
        if (!self.sections) {
            self.sections = @[].mutableCopy;
        }
        
        // Build every section array or row array which is smaller than given index path.
        [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) {
            for (NSInteger section = 0; section <= indexPath.section; ++section) {
                if (section >= self.sections.count) {
                    self.sections[section] = @[].mutableCopy;
                }
            }
            NSMutableArray *rows = self.sections[indexPath.section];
            for (NSInteger row = 0; row <= indexPath.row; ++row) {
                if (row >= rows.count) {
                    rows[row] = @(_FDTemplateLayoutCellHeightCacheAbsentValue);
                }
            }
        }];
    }
    
    - (BOOL)hasCachedHeightAtIndexPath:(NSIndexPath *)indexPath
    {
        [self buildHeightCachesAtIndexPathsIfNeeded:@[indexPath]];
        NSNumber *cachedNumber = self.sections[indexPath.section][indexPath.row];
        return ![cachedNumber isEqualToNumber:@(_FDTemplateLayoutCellHeightCacheAbsentValue)];
    }
    
    - (void)cacheHeight:(CGFloat)height byIndexPath:(NSIndexPath *)indexPath
    {
        [self buildHeightCachesAtIndexPathsIfNeeded:@[indexPath]];
        self.sections[indexPath.section][indexPath.row] = @(height);
    }
    
    - (CGFloat)cachedHeightAtIndexPath:(NSIndexPath *)indexPath
    {
        [self buildHeightCachesAtIndexPathsIfNeeded:@[indexPath]];
    #if CGFLOAT_IS_DOUBLE
        return [self.sections[indexPath.section][indexPath.row] doubleValue];
    #else
        return [self.sections[indexPath.section][indexPath.row] floatValue];
    #endif
    }
    
    @end

    2. 接下来是UITableView的一个扩展UITableView + FDTemplateLayoutCellPrivate

    • 第一个方法fd_templateCellForReuseIdentifier:identifier,这个方法主要是通过你传入的一个identifier(就是复用的id)获取cell。

        第一句是这样的 NSMutableDictionary *templateCellsByIdentifiers = objc_getAssociatedObject(self, _cmd);

        OC中的 _cmd 代表的就是本方法,objc_getAssociatedObject 获取一个关联对象的属性。

    • 接下来提供了一个方法来获取管理Cache的_FDTemplateLayoutCellHeightCache的对象fd_cellHeightCache。
    • 属性:fd_autoCacheInvalidationEnabled 记录是否自动缓存高度

    • 属性:fd_precacheEnabled

    这是一个私有类,下面给出这个类的完整代码:

    @interface UITableView (FDTemplateLayoutCellPrivate)
    
    /// Returns a template cell created by reuse identifier, it has to be registered to table view.
    /// Lazy getter, and associated to table view.
    - (id)fd_templateCellForReuseIdentifier:(NSString *)identifier;
    
    /// A private height cache data structure.
    @property (nonatomic, strong, readonly) _FDTemplateLayoutCellHeightCache *fd_cellHeightCache;
    
    /// This is a private switch that I don't think caller should concern.
    /// Auto turn on when you use "-fd_heightForCellWithIdentifier:cacheByIndexPath:configuration".
    @property (nonatomic, assign) BOOL fd_autoCacheInvalidationEnabled;
    
    /// It helps to improve scroll performance by "pre-cache" height of cells that have not
    /// been displayed on screen. These calculation tasks are collected and performed only
    /// when "RunLoop" is in "idle" time.
    ///
    /// Auto turn on when you use "-fd_heightForCellWithIdentifier:cacheByIndexPath:configuration".
    @property (nonatomic, assign) BOOL fd_precacheEnabled;
    
    /// Debug log controlled by "fd_debugLogEnabled".
    - (void)fd_debugLog:(NSString *)message;
    
    @end
    
    @implementation UITableView (FDTemplateLayoutCellPrivate)
    
    - (id)fd_templateCellForReuseIdentifier:(NSString *)identifier
    {
        NSAssert(identifier.length > 0, @"Expects a valid identifier - %@", identifier);
        
        NSMutableDictionary *templateCellsByIdentifiers = objc_getAssociatedObject(self, _cmd);
        if (!templateCellsByIdentifiers) {
            templateCellsByIdentifiers = @{}.mutableCopy;
            objc_setAssociatedObject(self, _cmd, templateCellsByIdentifiers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        }
        
        UITableViewCell *templateCell = templateCellsByIdentifiers[identifier];
        
        if (!templateCell) {
            templateCell = [self dequeueReusableCellWithIdentifier:identifier];
            NSAssert(templateCell != nil, @"Cell must be registered to table view for identifier - %@", identifier);
            templateCell.fd_isTemplateLayoutCell = YES;
            templateCellsByIdentifiers[identifier] = templateCell;
            [self fd_debugLog:[NSString stringWithFormat:@"layout cell created - %@", identifier]];
        }
        
        return templateCell;
    }
    
    - (_FDTemplateLayoutCellHeightCache *)fd_cellHeightCache {
        _FDTemplateLayoutCellHeightCache *cache = objc_getAssociatedObject(self, _cmd);
        if (!cache) {
            cache = [_FDTemplateLayoutCellHeightCache new];
            objc_setAssociatedObject(self, _cmd, cache, OBJC_ASSOCIATION_RETAIN);
        }
        return cache;
    }
    
    - (BOOL)fd_autoCacheInvalidationEnabled
    {
        return [objc_getAssociatedObject(self, _cmd) boolValue];
    }
    
    - (void)setFd_autoCacheInvalidationEnabled:(BOOL)enabled
    {
        objc_setAssociatedObject(self, @selector(fd_autoCacheInvalidationEnabled), @(enabled), OBJC_ASSOCIATION_RETAIN);
    }
    
    - (BOOL)fd_precacheEnabled
    {
        return [objc_getAssociatedObject(self, _cmd) boolValue];
    }
    
    - (void)setFd_precacheEnabled:(BOOL)precacheEnabled
    {
        objc_setAssociatedObject(self, @selector(fd_precacheEnabled), @(precacheEnabled), OBJC_ASSOCIATION_RETAIN);
    }
    
    - (void)fd_debugLog:(NSString *)message
    {
        if (!self.fd_debugLogEnabled) {
            return;
        }
        NSLog(@"** FDTemplateLayoutCell ** %@", message);
    }
    
    @end

    3. 下面又是一个分类,(这个是重点计算高度,调用缓存管理方法的分类)UITableView + FDTemplateLayoutCellPrecache

    这个里面的方法在他blog中也有提到就是在NSDefaultRunLoopMode下当状态将要进入休眠的时候把计算方法分解成多个RunLoop Source任务(source0) 

    - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array;

    这个方法将创建一个 Source 0 任务,分发到指定线程的 RunLoop 中,在给定的 Mode 下执行,若指定的 RunLoop 处于休眠状态,则唤醒它处理事件.

    主要逻辑就是先通过遍历所有section和row找到还没有缓存的row,然后加入到待缓存数组 ,创建一个observer去监听Runloop的状态 ,如果空闲了去创建source0任务,执行计算方法并缓存起来。如果预缓存任务完成了就把监听的Observer移除了。

    下面给出这个类的代码:

    @implementation UITableView (FDTemplateLayoutCellPrecache)
    
    - (void)fd_precacheIfNeeded
    {
        if (!self.fd_precacheEnabled) {
            return;
        }
        
        // Delegate could use "rowHeight" rather than implements this method.
        if (![self.delegate respondsToSelector:@selector(tableView:heightForRowAtIndexPath:)]) {
            return;
        }
        
        CFRunLoopRef runLoop = CFRunLoopGetCurrent();
        
        // This is a idle mode of RunLoop, when UIScrollView scrolls, it jumps into "UITrackingRunLoopMode"
        // and won't perform any cache task to keep a smooth scroll.
        CFStringRef runLoopMode = kCFRunLoopDefaultMode;
        
        // Collect all index paths to be precached.
        NSMutableArray *mutableIndexPathsToBePrecached = self.fd_allIndexPathsToBePrecached.mutableCopy;
        
        // Setup a observer to get a perfect moment for precaching tasks.
        // We use a "kCFRunLoopBeforeWaiting" state to keep RunLoop has done everything and about to sleep
        // (mach_msg_trap), when all tasks finish, it will remove itself.
        CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler
        (kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity _) {
            // Remove observer when all precache tasks are done.
            if (mutableIndexPathsToBePrecached.count == 0) {
                CFRunLoopRemoveObserver(runLoop, observer, runLoopMode);
                return;
            }
            // Pop first index path record as this RunLoop iteration's task.
            NSIndexPath *indexPath = mutableIndexPathsToBePrecached.firstObject;
            [mutableIndexPathsToBePrecached removeObject:indexPath];
            
            // This method creates a "source 0" task in "idle" mode of RunLoop, and will be
            // performed in a future RunLoop iteration only when user is not scrolling.
            [self performSelector:@selector(fd_precacheIndexPathIfNeeded:)
                         onThread:[NSThread mainThread]
                       withObject:indexPath
                    waitUntilDone:NO
                            modes:@[NSDefaultRunLoopMode]];
        });
        
        CFRunLoopAddObserver(runLoop, observer, runLoopMode);
    }
    
    - (void)fd_precacheIndexPathIfNeeded:(NSIndexPath *)indexPath
    {
        if (![self.fd_cellHeightCache hasCachedHeightAtIndexPath:indexPath]) {
            CGFloat height = [self.delegate tableView:self heightForRowAtIndexPath:indexPath];
            [self.fd_cellHeightCache cacheHeight:height byIndexPath:indexPath];
            [self fd_debugLog:[NSString stringWithFormat:
                               @"precached - [%@:%@] %@",
                               @(indexPath.section),
                               @(indexPath.row),
                               @(height)]];
        }
    }
    
    - (NSArray *)fd_allIndexPathsToBePrecached
    {
        NSMutableArray *allIndexPaths = @[].mutableCopy;
        for (NSInteger section = 0; section < [self numberOfSections]; ++section) {
            for (NSInteger row = 0; row < [self numberOfRowsInSection:section]; ++row) {
                NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section];
                if (![self.fd_cellHeightCache hasCachedHeightAtIndexPath:indexPath]) {
                    [allIndexPaths addObject:indexPath];
                }
            }
        }
        return allIndexPaths.copy;
    }
    
    @end

    4. 下面又是一个分类UITableView + FDTemplateLayoutCellAutomaticallyCacheInvalidation 

      因为我们会有一些操作导致cell的改变,所以这里作者要保证在每次cell改变的时候把sections数组改掉,然后如果新增或者修改了 需要重新计算高度。用到了methodSwizzle 黑魔法。这里作者把swizzle放在了UITableView的load类方法中。需要使用methodSwizzle的方法有:

      SEL selectors[] = {
            @selector(reloadData),
            @selector(insertSections:withRowAnimation:),
            @selector(deleteSections:withRowAnimation:),
            @selector(reloadSections:withRowAnimation:),
            @selector(moveSection:toSection:),
            @selector(insertRowsAtIndexPaths:withRowAnimation:),
            @selector(deleteRowsAtIndexPaths:withRowAnimation:),
            @selector(reloadRowsAtIndexPaths:withRowAnimation:),
            @selector(moveRowAtIndexPath:toIndexPath:)
        };

    这个类的代码:

    @implementation UITableView (FDTemplateLayoutCellAutomaticallyCacheInvalidation)
    
    + (void)load
    {
        // All methods that trigger height cache's invalidation
        SEL selectors[] = {
            @selector(reloadData),
            @selector(insertSections:withRowAnimation:),
            @selector(deleteSections:withRowAnimation:),
            @selector(reloadSections:withRowAnimation:),
            @selector(moveSection:toSection:),
            @selector(insertRowsAtIndexPaths:withRowAnimation:),
            @selector(deleteRowsAtIndexPaths:withRowAnimation:),
            @selector(reloadRowsAtIndexPaths:withRowAnimation:),
            @selector(moveRowAtIndexPath:toIndexPath:)
        };
        
        for (NSUInteger index = 0; index < sizeof(selectors) / sizeof(SEL); ++index) {
            SEL originalSelector = selectors[index];
            SEL swizzledSelector = NSSelectorFromString([@"fd_" stringByAppendingString:NSStringFromSelector(originalSelector)]);
            
            Method originalMethod = class_getInstanceMethod(self, originalSelector);
            Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector);
            
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    }
    
    - (void)fd_reloadData
    {
        if (self.fd_autoCacheInvalidationEnabled) {
            [self.fd_cellHeightCache.sections removeAllObjects];
        }
        [self fd_reloadData]; // Primary call
        [self fd_precacheIfNeeded];
    }
    
    - (void)fd_insertSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation
    {
        if (self.fd_autoCacheInvalidationEnabled) {
            [sections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
                [self.fd_cellHeightCache.sections insertObject:@[].mutableCopy atIndex:idx];
            }];
        }
        [self fd_insertSections:sections withRowAnimation:animation]; // Primary call
        [self fd_precacheIfNeeded];
    }
    
    - (void)fd_deleteSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation
    {
        if (self.fd_autoCacheInvalidationEnabled) {
            [sections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
                [self.fd_cellHeightCache.sections removeObjectAtIndex:idx];
            }];
        }
        [self fd_deleteSections:sections withRowAnimation:animation]; // Primary call
    }
    
    - (void)fd_reloadSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation
    {
        if (self.fd_autoCacheInvalidationEnabled) {
            [sections enumerateIndexesUsingBlock: ^(NSUInteger idx, BOOL *stop) {
                NSMutableArray *rows = self.fd_cellHeightCache.sections[idx];
                for (NSInteger row = 0; row < rows.count; ++row) {
                    rows[row] = @(_FDTemplateLayoutCellHeightCacheAbsentValue);
                }
            }];
        }
        [self fd_reloadSections:sections withRowAnimation:animation]; // Primary call
        [self fd_precacheIfNeeded];
    }
    
    - (void)fd_moveSection:(NSInteger)section toSection:(NSInteger)newSection
    {
        if (self.fd_autoCacheInvalidationEnabled) {
            [self.fd_cellHeightCache.sections exchangeObjectAtIndex:section withObjectAtIndex:newSection];
        }
        [self fd_moveSection:section toSection:newSection]; // Primary call
    }
    
    - (void)fd_insertRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
    {
        if (self.fd_autoCacheInvalidationEnabled) {
            [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) {
                NSMutableArray *rows = self.fd_cellHeightCache.sections[indexPath.section];
                [rows insertObject:@(_FDTemplateLayoutCellHeightCacheAbsentValue) atIndex:indexPath.row];
            }];
        }
        [self fd_insertRowsAtIndexPaths:indexPaths withRowAnimation:animation]; // Primary call
        [self fd_precacheIfNeeded];
    }
    
    - (void)fd_deleteRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
    {
        if (self.fd_autoCacheInvalidationEnabled) {
            [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) {
                [self.fd_cellHeightCache.sections[indexPath.section] removeObjectAtIndex:indexPath.row];
            }];
        }
        [self fd_deleteRowsAtIndexPaths:indexPaths withRowAnimation:animation]; // Primary call
    }
    
    - (void)fd_reloadRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
    {
        if (self.fd_autoCacheInvalidationEnabled) {
            [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) {
                NSMutableArray *rows = self.fd_cellHeightCache.sections[indexPath.section];
                rows[indexPath.row] = @(_FDTemplateLayoutCellHeightCacheAbsentValue);
            }];
        }
        [self fd_reloadRowsAtIndexPaths:indexPaths withRowAnimation:animation]; // Primary call
        [self fd_precacheIfNeeded];
    }
    
    - (void)fd_moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath
    {
        if (self.fd_autoCacheInvalidationEnabled) {
            NSMutableArray *sourceRows = self.fd_cellHeightCache.sections[sourceIndexPath.section];
            NSMutableArray *destinationRows = self.fd_cellHeightCache.sections[destinationIndexPath.section];
            
            NSNumber *sourceValue = sourceRows[sourceIndexPath.row];
            NSNumber *destinationValue = destinationRows[destinationIndexPath.row];
            
            sourceRows[sourceIndexPath.row] = destinationValue;
            destinationRows[destinationIndexPath.row] = sourceValue;
        }
        [self fd_moveRowAtIndexPath:sourceIndexPath toIndexPath:destinationIndexPath]; // Primary call
    }
    
    @end

    5. 下面还有一个分类UITableView + FDTemplateLayoutCell,这个类提供外界获取cell高度的方法

    • fd_heightForCellWithIdentifier:configuration:configuration
    • fd_heightForCellWithIdentifier:cacheByIndexPath:configuration:configuration

      这个类的方法如下:

    @implementation UITableView (FDTemplateLayoutCell)
    
    - (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier configuration:(void (^)(id))configuration
    {
        if (!identifier) {
            return 0;
        }
        
        // Fetch a cached template cell for `identifier`.
        UITableViewCell *cell = [self fd_templateCellForReuseIdentifier:identifier];
        
        // Manually calls to ensure consistent behavior with actual cells (that are displayed on screen).
        [cell prepareForReuse];
        
        // Customize and provide content for our template cell.
        if (configuration) {
            configuration(cell);
        }
        
        // Add a hard width constraint to make dynamic content views (like labels) expand vertically instead
        // of growing horizontally, in a flow-layout manner.
        NSLayoutConstraint *tempWidthConstraint =
        [NSLayoutConstraint constraintWithItem:cell.contentView
                                     attribute:NSLayoutAttributeWidth
                                     relatedBy:NSLayoutRelationEqual
                                        toItem:nil
                                     attribute:NSLayoutAttributeNotAnAttribute
                                    multiplier:1.0
                                      constant:CGRectGetWidth(self.frame)];
        [cell.contentView addConstraint:tempWidthConstraint];
        
        // Auto layout engine does its math
        CGSize fittingSize = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
        
        [cell.contentView removeConstraint:tempWidthConstraint];
        
        // Add 1px extra space for separator line if needed, simulating default UITableViewCell.
        if (self.separatorStyle != UITableViewCellSeparatorStyleNone) {
            fittingSize.height += 1.0 / [UIScreen mainScreen].scale;
        }
        
        return fittingSize.height;
    }
    
    - (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier cacheByIndexPath:(NSIndexPath *)indexPath configuration:(void (^)(id))configuration
    {
        if (!identifier || !indexPath) {
            return 0;
        }
        
        // Enable auto cache invalidation if you use this "cacheByIndexPath" API.
        if (!self.fd_autoCacheInvalidationEnabled) {
            self.fd_autoCacheInvalidationEnabled = YES;
        }
        // Enable precache if you use this "cacheByIndexPath" API.
        if (!self.fd_precacheEnabled) {
            self.fd_precacheEnabled = YES;
            // Manually trigger precache only for the first time.
            [self fd_precacheIfNeeded];
        }
        
        // Hit the cache
        if ([self.fd_cellHeightCache hasCachedHeightAtIndexPath:indexPath]) {
            [self fd_debugLog:[NSString stringWithFormat:
                               @"hit cache - [%@:%@] %@",
                               @(indexPath.section),
                               @(indexPath.row),
                               @([self.fd_cellHeightCache cachedHeightAtIndexPath:indexPath])]];
            return [self.fd_cellHeightCache cachedHeightAtIndexPath:indexPath];
        }
        
        // Do calculations
        CGFloat height = [self fd_heightForCellWithIdentifier:identifier configuration:configuration];
        [self fd_debugLog:[NSString stringWithFormat:
                           @"calculate - [%@:%@] %@",
                           @(indexPath.section),
                           @(indexPath.row),
                           @(height)]];
        
        // Cache it
        [self.fd_cellHeightCache cacheHeight:height byIndexPath:indexPath];
        
        return height;
    }
    
    - (BOOL)fd_debugLogEnabled
    {
        return [objc_getAssociatedObject(self, _cmd) boolValue];
    }
    
    - (void)setFd_debugLogEnabled:(BOOL)debugLogEnabled
    {
        objc_setAssociatedObject(self, @selector(fd_debugLogEnabled), @(debugLogEnabled), OBJC_ASSOCIATION_RETAIN);
    }
    
    @end
  • 相关阅读:
    RN-Android构建失败:Caused by: org.gradle.api.ProjectConfigurationException: A problem occurred configuring root project 'AwesomeProject'.
    Android更新包下载成功后不出现安装界面
    真机调试: The application could not be installed: INSTALL_FAILED_TEST_ONLY
    react native 屏幕尺寸转换
    Android Studio生成签名文件,自动签名,以及获取SHA1和MD5值
    React Native安卓真机调试
    git提交代码报错Permission denied, please try again
    The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.
    命令行设置快捷命令
    Linux 常用指令
  • 原文地址:https://www.cnblogs.com/muzijie/p/7391012.html
Copyright © 2011-2022 走看看