優(yōu)化UITableViewCell高度計(jì)算Swift版

最近在想著怎么提高自己的iOS技能低零,不斷的閱讀別人的blog。發(fā)現(xiàn)讀完了不練手很快又忘掉了,練手吧又不知道從何處下手岳悟。所以就想著看看別人寫的庫(kù)是怎么實(shí)現(xiàn)的,體量比較大的看起來(lái)很費(fèi)勁泼差,影響士氣。那就從體量比較小的看起呵俏。

UITableView應(yīng)該是iOS非常常用的一個(gè)組件堆缘,學(xué)過(guò)iOS都會(huì)用,但是用好它卻并不是那么easy 普碎。不恰當(dāng)?shù)氖褂猛鶗?huì)造成掉幀引起界面卡頓吼肥。

關(guān)于界面上圖像的顯示原理,卡頓造成的原因之類的可以看這篇麻车,iOS 保持界面流暢的技巧 , YY大神是用異步渲染的技術(shù)使界面流程缀皱,目前還看不懂。动猬。??啤斗,但是那些成像和卡頓的原理還是值得好好看看的硬梁。

如果你是因?yàn)閳A角陰影太多等問(wèn)題引起的镰烧,可以看iOS 高效添加圓角效果實(shí)戰(zhàn)講解 汇鞭。

上面都不是重點(diǎn)旁涤,重點(diǎn)是由于系統(tǒng)會(huì)多次重復(fù)計(jì)算TableviewCell的高度

關(guān)于UITableViewCell怎樣設(shè)置高度以及對(duì)性能影響的詳細(xì)描述請(qǐng)直接戳 優(yōu)化UITableViewCell高度計(jì)算的那些事

上面那個(gè)文章一定要讀外莲,是百度大神sunnyxx 寫的(雖然現(xiàn)在已經(jīng)離開百度了)凭语。大神詳細(xì)講述了UITableViewCell計(jì)算的各種方式以及會(huì)有哪些性能問(wèn)題催什,并且寫了一個(gè)開源庫(kù)解決了問(wèn)題群发,提高了UITableView滑動(dòng)的流暢性凤覆。開源庫(kù)地址:UITableView+FDTemplateLayoutCell链瓦,作者寫那篇文章的時(shí)候那個(gè)庫(kù)的版本是1.2,現(xiàn)在已經(jīng)1.5了盯桦。

本文帶大家詳細(xì)探索那篇文章所對(duì)應(yīng)的庫(kù)(1.2版)慈俯。并實(shí)現(xiàn)一個(gè)swift版本的庫(kù)渤刃。(選擇他主要是因?yàn)楹?jiǎn)短,也嘗試去看過(guò)YY大神的庫(kù)肥卡,各種看不懂最后只好先擱置了溪掀。。)

有些人會(huì)問(wèn)步鉴,別人已經(jīng)有了為啥還要再實(shí)現(xiàn)一遍揪胃,OC和Swift不是可以橋接么直接用就行了。我想說(shuō)用swift實(shí)現(xiàn)主要是想理解作者的每行代碼的含義氛琢,作者的思想喊递,以及對(duì)應(yīng)swift中的實(shí)現(xiàn)方式。(OC和swift還是有挺多區(qū)別的)阳似,理解了庫(kù)的每行代碼骚勘,自己使用也放心,自己實(shí)現(xiàn)了一個(gè)以后有什么別的需求自己直接可以改自己這版代碼撮奏,掌握了他的思想俏讹,可以類比寫出別的比較好用的庫(kù)。(一舉多得 還不好好看源碼?)

這個(gè)庫(kù)能干啥 畜吊,就是利用緩存tableviewcell的高度提高滑動(dòng)的流暢性泽疆,我用了 sunnyxx 原有的數(shù)據(jù),添加了很多emoj表情在自己實(shí)現(xiàn)的swift版上做了測(cè)試 玲献。(不加很多emoj殉疼,簡(jiǎn)短的文字緩存和不緩存基本都沒秒60幀沒啥區(qū)別)

結(jié)果是緩存的基本在每秒50多幀 ,沒緩存的會(huì)掉到二十幾甚至十幾幀 捌年。(
YY大神的異步渲染估計(jì)可以保持在60幀 )

下面放兩副截圖:(測(cè)試必須在真機(jī)上運(yùn)行瓢娜,模擬器上沒有GPU,是CPU模擬的 都會(huì)掉幀看不到效果)

沒有緩存拖動(dòng)過(guò)程截圖
緩存后拖動(dòng)過(guò)程截圖

那個(gè)FPS是自己寫的一個(gè)view利用CADisplayLink 實(shí)現(xiàn)的礼预,感興趣的可以看源碼眠砾,模仿YY大神庫(kù)中的一個(gè)類。

界面繪制大量emoj的時(shí)候一般都會(huì)有卡頓托酸,尤其適用autolayout 每次計(jì)算荠藤,我們緩存了cell高度后可以緩解這樣的問(wèn)題。

下面來(lái)進(jìn)入正題获高,看下實(shí)現(xiàn)思路哈肖,其實(shí)如果你看了這篇文章,你已經(jīng)知道了實(shí)現(xiàn)思路優(yōu)化UITableViewCell高度計(jì)算的那些事

主要是利用Runloop在空閑狀態(tài)下后臺(tái)計(jì)算tableviewcell的高度并緩存起來(lái)念秧。然后在使用的時(shí)候就直接從緩存中去淤井,這里都放在一個(gè)數(shù)組里存在內(nèi)存。

對(duì)Runloop以及幾個(gè)mode不懂的可以看sunnyxx blog中的視頻 視頻可戳 , 文章的話可以看看 深入理解RunLoop 币狠、 【iOS程序啟動(dòng)與運(yùn)轉(zhuǎn)】- RunLoop個(gè)人小結(jié)

如果你耐著性子把上面的連接都看了游两,你再看sunnyxx blog中的解釋應(yīng)該就不會(huì)暈了。其實(shí)就是在kCFRunLoopDefaultMode模式下BeforWaitting狀態(tài)去執(zhí)行計(jì)算的漩绵。

下面來(lái)探究源碼贱案。首先在UITableView+FDTemplateLayoutCell 下載源碼,下載1.2版本止吐。 1.5版本沒有用Runloop實(shí)現(xiàn)宝踪。

然后你得到的庫(kù)就兩個(gè)文件 (在swift中就只有一個(gè)啦)。

配圖

大概看了下碍扔,.m文件只有500行瘩燥。

PS:只要跟著看,肯定可以看懂不同,后面還有很多干貨

看下作者的實(shí)現(xiàn)思路:(從上往下看)
1厉膀、 創(chuàng)建了一個(gè)_FDTemplateLayoutCellHeightCache類,就是管理Cache的一個(gè)類二拐,里面有兩個(gè)屬性四個(gè)方法服鹅。

屬性:
  • sections 這個(gè)變量就是用來(lái)存儲(chǔ)緩存的height的一個(gè)二維數(shù)組。(因?yàn)閠ableview有section和row組成所以必須二維)

  • _FDTemplateLayoutCellHeightCacheAbsentValue 這個(gè)是一個(gè)靜態(tài)常量百新,就是用來(lái)標(biāo)記沒有緩存高度row 企软。

方法:
  • buildHeightCachesAtIndexPathsIfNeeded:indexPaths
    這個(gè)方法傳入indexPaths數(shù)組來(lái)給sections中還沒有初始化的元素進(jìn)行初始化
  • hasCachedHeightAtIndexPath:indexPath 根據(jù)下標(biāo)索引判斷是否有緩存(其實(shí)就是判斷是否等于上面那個(gè)靜態(tài)常量)
  • cacheHeight:height:byIndexPath 根據(jù)indexPath給sections賦值。
  • cachedHeightAtIndexPath:indexPath 根據(jù)indexPath取值

這個(gè)類主要操作和存儲(chǔ)緩存的吟孙。看看swift中的實(shí)現(xiàn)方法聚蝶。

class ZZTemplateLayoutCellHeightCache:NSObject,NSCopying{
    
    var sections:[[CGFloat]] = []
    
    func copyWithZone(zone: NSZone) -> AnyObject {
        let theCopy = ZZTemplateLayoutCellHeightCache()
        theCopy.sections = self.sections
        return theCopy
    }
    
    // 標(biāo)記沒有被cache的值
    static let ZZTemplateLayoutCellHeightCacheAbsentValue:CGFloat = -1
    
    /**
     初始化Height緩存數(shù)組 對(duì)每個(gè)元素做標(biāo)記
     
     - parameter indexPaths: indexPaths數(shù)組
     */
    func buildHeightCachesAtIndexPathsIfNeeded(indexPaths:[NSIndexPath]){
        guard indexPaths.count > 0 else { return }
        indexPaths.forEach { (indexPath) -> () in
            //如果section 數(shù)組里面還沒有 創(chuàng)建一個(gè)對(duì)象的空數(shù)組
            if indexPath.section >= self.sections.count{
                self.sections.insert([], atIndex: indexPath.section)
            }
            
            var rows = self.sections[indexPath.section]
            //如果對(duì)應(yīng)的row不存在 創(chuàng)建一個(gè)并標(biāo)記為沒有被cache
            if indexPath.row >= rows.count {
                rows.insert(ZZTemplateLayoutCellHeightCache.ZZTemplateLayoutCellHeightCacheAbsentValue, atIndex: indexPath.row)
            }
            self.sections[indexPath.section] = rows
        }
        
    }
    
    /**
     indexPath是否已經(jīng)被緩存
     
     - parameter indexPath: indexPath
     
     - returns: Bool
     */
    func hasCachedHeightAtIndexPath(indexPath:NSIndexPath)->Bool{
        
        self.buildHeightCachesAtIndexPathsIfNeeded([indexPath])
        return self.sections[indexPath.section][indexPath.row] != ZZTemplateLayoutCellHeightCache.ZZTemplateLayoutCellHeightCacheAbsentValue
        
    }
    
    /**
     緩存高度
     
     - parameter height:    高度
     - parameter indexPath: indexPath索引
     */
    func cacheHeight(height:CGFloat,byIndexPath indexPath:NSIndexPath){
        
        self.buildHeightCachesAtIndexPathsIfNeeded([indexPath])
        self.sections[indexPath.section][indexPath.row] = height
        
    }
    
    /**
     獲取已經(jīng)緩存的height
     
     - parameter indexPath: indexPath索引
     
     - returns: height
     */
    func cachedHeightAtIndexPath(indexPath:NSIndexPath)->CGFloat{
        self.buildHeightCachesAtIndexPathsIfNeeded([indexPath])
        return self.sections[indexPath.section][indexPath.row]
    }
}

比他多寫了個(gè)copyWithZone: 由于要賦值需要實(shí)現(xiàn)NSCopying協(xié)議杰妓。

接著往下看,他寫了個(gè)UITableView的擴(kuò)展

配圖

看第一個(gè)方法


配圖

這個(gè)方法主要是通過(guò)你傳入的一個(gè)identifier(就是復(fù)用的id)獲取cell

第一句是這樣的

NSMutableDictionary *templateCellsByIdentifiers = objc_getAssociatedObject(self, _cmd);

OC中的 _cmd 代表的就是本方法碘勉,objc_getAssociatedObject 獲取一個(gè)關(guān)聯(lián)對(duì)象的屬性巷挥,其實(shí)在swift中 我們可以直接給系統(tǒng)已有的類添加屬性

比如這個(gè)方法中的cell還有個(gè)屬性 fd_isTemplateLayoutCell 我們實(shí)現(xiàn)一個(gè)swift版的

extension UITableViewCell{
    //在私有嵌套 struct 中使用 static var,這樣會(huì)生成我們所需的關(guān)聯(lián)對(duì)象鍵验靡,但不會(huì)污染整個(gè)命名空間倍宾。
    private struct AssociatedKeys{
        static var zz_isTemplateLayoutCell = "zz_isTemplateLayoutCell"
    }
    
    ///擴(kuò)展 添加屬性 是否為臨時(shí)布局的cell
    var zz_isTemplateLayoutCell:Bool? {
        set{
            objc_setAssociatedObject(self, &AssociatedKeys.zz_isTemplateLayoutCell,newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_COPY_NONATOMIC)
        }
        get{
            return objc_getAssociatedObject(self, &AssociatedKeys.zz_isTemplateLayoutCell) as? Bool
        }
    }
}

參考這篇文章 Swift & the Objective-C Runtime
更多Runtime知識(shí)擴(kuò)展閱讀:Objective-C Runtime
Objective-C Runtime 1小時(shí)入門教程

懂了這些東西,我們就可以把他所有用_cmd和OC版本蹩腳的屬性加上去胜嗓,用我們比較優(yōu)美的方式高职。

extension UITableView{
    
    private struct AssociatedKeys{
        static var zz_debugLogEnabled = "zz_debugLogEnabled"
        static var zz_chc = "zz_chc"
        static var zz_tmpCellForReuseId = "zz_tmpCellForReuseId"
        static var zz_autoCacheInvalidationEnabled = "zz_autoCacheInvalidationEnabled"
        static var zz_precacheEnabled = "zz_precacheEnabled"
    }
    
    /// 擴(kuò)展 添加屬性 是否在debug的情況下打印
    var zz_debugLogEnabled:Bool? {
        set{
            objc_setAssociatedObject(self, &AssociatedKeys.zz_debugLogEnabled,newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_COPY_NONATOMIC)
        }
        get{
            return objc_getAssociatedObject(self, &AssociatedKeys.zz_debugLogEnabled) as? Bool
        }
    }
    
    
    private var zz_chc:ZZTemplateLayoutCellHeightCache?{
        set{
            objc_setAssociatedObject(self, &AssociatedKeys.zz_chc,newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_COPY_NONATOMIC)
        }
        get{
            return objc_getAssociatedObject(self, &AssociatedKeys.zz_chc) as? ZZTemplateLayoutCellHeightCache
        }
    }
    
    private var zz_tmpCellForReuseId:[String:UITableViewCell]?{
        set{
            objc_setAssociatedObject(self, &AssociatedKeys.zz_tmpCellForReuseId,newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_COPY_NONATOMIC)
        }
        get{
            return objc_getAssociatedObject(self, &AssociatedKeys.zz_tmpCellForReuseId) as? [String:UITableViewCell]
        }
    }
    
    var zz_autoCacheInvalidationEnabled:Bool?{
        set{
            objc_setAssociatedObject(self, &AssociatedKeys.zz_autoCacheInvalidationEnabled,newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_COPY_NONATOMIC)
        }
        get{
            return objc_getAssociatedObject(self, &AssociatedKeys.zz_autoCacheInvalidationEnabled) as? Bool
        }
    }
    
    var zz_precacheEnabled:Bool?{
        set{
            objc_setAssociatedObject(self, &AssociatedKeys.zz_precacheEnabled,newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_COPY_NONATOMIC)
        }
        get{
            return objc_getAssociatedObject(self, &AssociatedKeys.zz_precacheEnabled) as? Bool
        }
    }
}

然后那個(gè)通過(guò)id獲取cell的方法在swift中的實(shí)現(xiàn)就沒那么長(zhǎng)了,很簡(jiǎn)短的辞州。

 /**
     根據(jù)identifier獲取UITableViewCell
     
     - parameter identifier: identifier
     
     - returns: UITableViewCell
     */
    func zz_templateCellForReuseIdentifier(identifier:String)->UITableViewCell{
        assert(identifier.characters.count>0, "需要一個(gè)正確的identifier - \(identifier)")
        
        if zz_tmpCellForReuseId == nil {
            zz_tmpCellForReuseId = [:]
        }
        var templateCell = zz_tmpCellForReuseId![identifier]
        if templateCell == nil{
            
            templateCell = self.dequeueReusableCellWithIdentifier(identifier)
            assert(templateCell != nil, "cell必須已經(jīng)用identifier注冊(cè)過(guò): - \(identifier)")
            templateCell!.zz_isTemplateLayoutCell = true
            zz_tmpCellForReuseId![identifier] = templateCell
            self.zz_debugLog("cell布局已經(jīng)創(chuàng)建: - \(identifier)")
        }
        
        return templateCell!
    }

我們?cè)谑褂眠@個(gè)庫(kù)的時(shí)候回傳入identifier 怔锌, 庫(kù)里面的通過(guò)這個(gè)方法來(lái)獲取cell。

然后他提供了一個(gè)方法用來(lái)獲取上面管理Cache的_FDTemplateLayoutCellHeightCache的對(duì)象 。
fd_cellHeightCache

由于我們給他也添加了屬性埃元,所以我們這個(gè)方法也異常的簡(jiǎn)單

/**
     返回ZZTemplateLayoutCellHeightCache的一個(gè)實(shí)例對(duì)象 如果已經(jīng)存在直接獲取
     
     - returns: ZZTemplateLayoutCellHeightCache對(duì)象
     */
    func zz_cellHeightCache()->ZZTemplateLayoutCellHeightCache{
        if zz_chc == nil{
            zz_chc = ZZTemplateLayoutCellHeightCache()
        }
        return self.zz_chc!
    }

接下來(lái)就是他寫的幾個(gè)屬性涝涤,我們上面用優(yōu)美的方式寫過(guò)了,可以無(wú)視

下面又是一個(gè)分類岛杀,(這個(gè)是重點(diǎn)計(jì)算高度阔拳,調(diào)用緩存管理方法的分類)

配圖

這個(gè)里面的方法在他blog中也有提到就是在NSDefaultRunLoopMode下當(dāng)狀態(tài)將要進(jìn)入休眠的時(shí)候把計(jì)算方法分解成多個(gè)RunLoop Source任務(wù)(source0)

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

這個(gè)方法將創(chuàng)建一個(gè) Source 0 任務(wù),分發(fā)到指定線程的 RunLoop 中类嗤,在給定的 Mode 下執(zhí)行糊肠,若指定的 RunLoop 處于休眠狀態(tài),則喚醒它處理事件.

下面來(lái)看看swift中的實(shí)現(xiàn)土浸;

// MARK: - cell的預(yù)緩存
extension UITableView{
    
    private func zz_precacheIfNeeded(){
        
        if let pe = self.zz_precacheEnabled where !pe {
            return
        }
        guard let delegate = self.delegate else { return }
        
        if !delegate.respondsToSelector(Selector("tableView:heightForRowAtIndexPath:")){
            return
        }
        
        let runLoop = CFRunLoopGetCurrent()
        //一個(gè)空閑的Runloop 罪针,當(dāng)頁(yè)面滾動(dòng)時(shí)會(huì)切換到"UITrackingRunLoopMode"
        let runLoopMode = kCFRunLoopDefaultMode
        
        // 收集所有被緩存過(guò)的index paths
        var mutableIndexPathsToBePrecached = self.zz_allIndexPathsToBePrecached()
        
        let observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, CFRunLoopActivity.BeforeWaiting.rawValue, true, 0) { (observer, _) -> Void in
            // 所有預(yù)緩存任務(wù)完成的時(shí)候 移除Observer
            if mutableIndexPathsToBePrecached.count == 0{
                CFRunLoopRemoveObserver(runLoop, observer, runLoopMode)
                return
            }
            
            // 取出第一個(gè)indexPath作為此Runloop迭代的任務(wù)
            let indexPath = mutableIndexPathsToBePrecached.first
            mutableIndexPathsToBePrecached.removeFirst()
//            這個(gè)方法將創(chuàng)建一個(gè) Source 0 任務(wù),分發(fā)到指定線程的 RunLoop 中黄伊,在給定的 Mode 下執(zhí)行泪酱,若指定的 RunLoop 處于休眠狀態(tài),則喚醒它處理事件
            self.performSelector("zz_precacheIndexPathIfNeeded:", onThread: NSThread.mainThread(), withObject: indexPath, waitUntilDone: false, modes: [NSDefaultRunLoopMode])
        }
        
        CFRunLoopAddObserver(runLoop, observer, runLoopMode)
        
    }
    
    
    /**
     如果沒有緩存的話 就緩存高度
     
     - parameter indexPath: indexpath
     */
    func zz_precacheIndexPathIfNeeded(indexPath:NSIndexPath){
        guard let delegate = self.delegate else { return }
        if !self.zz_cellHeightCache().hasCachedHeightAtIndexPath(indexPath){
            let height = delegate.tableView!(self, heightForRowAtIndexPath: indexPath)
            self.zz_cellHeightCache().cacheHeight(height, byIndexPath: indexPath)
            self.zz_debugLog("Precached - \(indexPath.section) , \(indexPath.row) , \(height)")
        }
    }
    
    /**
     獲取所有需要被緩存的indexPath
     
     - returns: [indexpath]
     */
    func zz_allIndexPathsToBePrecached()->[NSIndexPath]{
        
        var allIndexPaths:[NSIndexPath] = []
        for section in 0..<self.numberOfSections{
            for row in 0..<self.numberOfRowsInSection(section){
                
                print("section is :\(section) , row is :\(row) ")
                let indexPath = NSIndexPath(forRow: row, inSection: section)
                if !self.zz_cellHeightCache().hasCachedHeightAtIndexPath(indexPath){
                    allIndexPaths.append(indexPath)
                }
                
            }
        }
        return allIndexPaths
    }
    
}

注釋很清楚还最,主要邏輯就是先通過(guò)遍歷所有section和row找到還沒有緩存的row墓阀,然后加入到待緩存數(shù)組 ,創(chuàng)建一個(gè)observer去監(jiān)聽Runloop的狀態(tài) 拓轻,如果空閑了去創(chuàng)建source0任務(wù)斯撮,執(zhí)行計(jì)算方法并緩存起來(lái)。如果預(yù)緩存任務(wù)完成了就把監(jiān)聽的Observer移除了扶叉。(說(shuō)的挺清楚了勿锅,有啥問(wèn)題可以討論)

下面又是一個(gè)擴(kuò)展

配圖

這個(gè)分類看起來(lái)比較長(zhǎng),其實(shí)也是個(gè)紙老虎枣氧。(大多數(shù)都是同理的代碼)

因?yàn)槲覀儠?huì)有一些操作導(dǎo)致cell的改變溢十,所以這里作者要保證在每次cell改變的時(shí)候把sections數(shù)組改掉,然后如果新增或者修改了 需要重新計(jì)算高度达吞。用到了methodSwizzle 黑魔法张弛,索性我前面提到的blog也給出了swift中方法替換的實(shí)現(xiàn)方法。

這里作者把swizzle放在了UITableView的load類方法中酪劫,在swift中我們需要重寫initialize

所以這里的實(shí)現(xiàn)方式如下:

    /**
      相當(dāng)于 oc中的load類方法 在方法加載的第一時(shí)間執(zhí)行
     */
    public override class func initialize() {
        
        // 所有有可能要修改height緩存的方法
        let 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:")
                        ]
        
        // 對(duì)所有method轉(zhuǎn)換成我們自己寫的method
        for index in 0..<selectors.count{
            
            let originalSelector = selectors[index]
            let swizzledSelector = NSSelectorFromString("zz_\(NSStringFromSelector(originalSelector))")
            
            let originalMethod = class_getInstanceMethod(self, originalSelector)
            let swizzledMethod = class_getInstanceMethod(self, swizzledSelector)
            
            let didAddMethod = class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))
            
            if didAddMethod {
                class_replaceMethod(self, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
            } else {
                method_exchangeImplementations(originalMethod, swizzledMethod);
            }
            
        }
        
    }

把所有可能改變indexPath的方法抓出來(lái) 然后遍歷替換掉 吞鸭。 下面就寫上自己的方法

    func zz_reloadData(){
        if let zz_auto = self.zz_autoCacheInvalidationEnabled where zz_auto{
            self.zz_cellHeightCache().sections.removeAll()
        }
        self.zz_reloadData()
        self.zz_precacheIfNeeded()
    }
    
    func zz_insertSections(sections: NSIndexSet, withRowAnimation animation: UITableViewRowAnimation){
        if let zz_auto = self.zz_autoCacheInvalidationEnabled where zz_auto {
            sections.forEach({ (index) -> () in
                self.zz_cellHeightCache().sections.insert([], atIndex: index)
            })
        }
        
        self.zz_insertSections(sections, withRowAnimation: animation)
        self.zz_precacheIfNeeded()
    }
    
    func zz_deleteSections(sections: NSIndexSet, withRowAnimation animation: UITableViewRowAnimation){
    
        if let zz_auto = self.zz_autoCacheInvalidationEnabled where zz_auto {
            sections.forEach({ (index) -> () in
                self.zz_cellHeightCache().sections.removeAtIndex(index)
            })
        }
        self.zz_deleteSections(sections, withRowAnimation: animation)
    }

這里就放了三分方法例子,其他的看源碼覆糟,地址在文尾刻剥。

貌似一切都準(zhǔn)備的差不多了,下面就是留給外界使用者調(diào)用的方法了

// MARK: - [Public] 提供給外部的方法
extension UITableView{
    /**
     計(jì)算獲得height
     
     - parameter identifier:    id
     - parameter configuration: configuration
     
     - returns: height
     */
    func zz_heightForCellWithIdentifier(identifier:String,configuration:((cell:UITableViewCell)->())?)->CGFloat{
        // 通過(guò)id獲取一個(gè)cell
        let cell = self.zz_templateCellForReuseIdentifier(identifier)
        //手動(dòng)調(diào)用已確保和屏幕上顯示的保持一致
        cell.prepareForReuse()
        configuration?(cell: cell)
        
        //手動(dòng)添加一個(gè)約束 確保動(dòng)態(tài)內(nèi)容 如label
        let tempWidthConstraint = NSLayoutConstraint(item: cell.contentView,
            attribute: NSLayoutAttribute.Width,
            relatedBy: NSLayoutRelation.Equal,
            toItem: nil,
            attribute: NSLayoutAttribute.NotAnAttribute,
            multiplier: 1.0,
            constant: CGRectGetWidth(self.frame))
        
        cell.contentView.addConstraint(tempWidthConstraint)
        
        // 算出size
        let fittingSize = cell.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
        // 移除約束
        cell.contentView.removeConstraint(tempWidthConstraint)
        
        return fittingSize.height
    }
    
    /**
     供外界使用的方法 提供了高度的緩存
     
     - parameter identifier:    identifier
     - parameter indexPath:     indexPath
     - parameter configuration: 供外部使用的閉包
     - returns: Height
     */
    func zz_heightForCellWithIdentifier(identifier:String,cacheByIndexPath indexPath:NSIndexPath,configuration:((cell:UITableViewCell)->())?)->CGFloat{
    
        if self.zz_autoCacheInvalidationEnabled == nil{
            self.zz_autoCacheInvalidationEnabled = true
        }
        
        if self.zz_precacheEnabled == nil{
            self.zz_precacheEnabled = true
        }
        print("cache : \(indexPath.section) , \(indexPath.row)")
        // 如果存在緩存 從緩存中返回
        if self.zz_cellHeightCache().hasCachedHeightAtIndexPath(indexPath){
            self.zz_debugLog("cache : \(indexPath.section) , \(indexPath.row) , \(self.zz_cellHeightCache().cachedHeightAtIndexPath(indexPath))")
            return self.zz_cellHeightCache().cachedHeightAtIndexPath(indexPath)
        }
        
        //計(jì)算 
        let height = self.zz_heightForCellWithIdentifier(identifier, configuration: configuration)
        
        self.zz_debugLog("計(jì)算出的cache cache : \(indexPath.section) , \(indexPath.row) , \(height)")
        
        //緩存
        self.zz_cellHeightCache().cacheHeight(height, byIndexPath: indexPath)
        
        return height
    }
    
}

第一個(gè)方法是手動(dòng)計(jì)算然后緩存滩字,第二個(gè)方法是先從緩存中取透敌,如果沒有再調(diào)手動(dòng)計(jì)算的方法盯滚。

然后外界使用的時(shí)候只需要這樣就已經(jīng)給tableview計(jì)算height加上了緩存

 func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
        
        return tableView.zz_heightForCellWithIdentifier(String(ZZFeedCell), cacheByIndexPath: indexPath, configuration: { (cell) -> () in
            guard let cell = cell as? ZZFeedCell else { return }
            cell.entity = self.feedEntitySections[indexPath.section][indexPath.row]
        })
      
    }

總算讀完了一個(gè)庫(kù),雖然小酗电,但是是一個(gè)好的開端魄藕。

看源碼,看源碼撵术。地址:github地址

有什么問(wèn)題在底下交流背率。效果圖上面已經(jīng)放出,大家可以下載github上的源碼嫩与,在真機(jī)上運(yùn)行寝姿,如果swift項(xiàng)目中有想使用的只需要把下圖中文件copy到你的項(xiàng)目中就可以了

配圖
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市划滋,隨后出現(xiàn)的幾起案子饵筑,更是在濱河造成了極大的恐慌,老刑警劉巖处坪,帶你破解...
    沈念sama閱讀 211,376評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件根资,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡同窘,警方通過(guò)查閱死者的電腦和手機(jī)玄帕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,126評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)想邦,“玉大人裤纹,你說(shuō)我怎么就攤上這事∩ッ唬” “怎么了鹰椒?”我有些...
    開封第一講書人閱讀 156,966評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)呕童。 經(jīng)常有香客問(wèn)我漆际,道長(zhǎng),這世上最難降的妖魔是什么拉庵? 我笑而不...
    開封第一講書人閱讀 56,432評(píng)論 1 283
  • 正文 為了忘掉前任灿椅,我火速辦了婚禮套蒂,結(jié)果婚禮上钞支,老公的妹妹穿的比我還像新娘。我一直安慰自己操刀,他們只是感情好烁挟,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,519評(píng)論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著骨坑,像睡著了一般撼嗓。 火紅的嫁衣襯著肌膚如雪柬采。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,792評(píng)論 1 290
  • 那天且警,我揣著相機(jī)與錄音粉捻,去河邊找鬼。 笑死斑芜,一個(gè)胖子當(dāng)著我的面吹牛肩刃,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播杏头,決...
    沈念sama閱讀 38,933評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼盈包,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了醇王?” 一聲冷哼從身側(cè)響起呢燥,我...
    開封第一講書人閱讀 37,701評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎寓娩,沒想到半個(gè)月后叛氨,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,143評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡根暑,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,488評(píng)論 2 327
  • 正文 我和宋清朗相戀三年力试,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片排嫌。...
    茶點(diǎn)故事閱讀 38,626評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡畸裳,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出淳地,到底是詐尸還是另有隱情怖糊,我是刑警寧澤,帶...
    沈念sama閱讀 34,292評(píng)論 4 329
  • 正文 年R本政府宣布颇象,位于F島的核電站伍伤,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏遣钳。R本人自食惡果不足惜扰魂,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,896評(píng)論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蕴茴。 院中可真熱鬧劝评,春花似錦、人聲如沸倦淀。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,742評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)撞叽。三九已至姻成,卻和暖如春插龄,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背科展。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工均牢, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人才睹。 一個(gè)月前我還...
    沈念sama閱讀 46,324評(píng)論 2 360
  • 正文 我出身青樓膨处,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親砂竖。 傳聞我的和親對(duì)象是個(gè)殘疾皇子真椿,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,494評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容

  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫(kù)、插件乎澄、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,060評(píng)論 4 62
  • 北風(fēng) 文/鄒航 多少次在天空仰望突硝,仰望那天上一盞...
    鄒航閱讀 274評(píng)論 6 5
  • 前段時(shí)間聽說(shuō)一個(gè)營(yíng)業(yè)經(jīng)理解恰,在針對(duì)全分行的早會(huì)分享時(shí),觀點(diǎn)不一致居然就杠上了浙于,有點(diǎn)意外护盈,你的一個(gè)演講分享是為了激發(fā)認(rèn)...
    付奮閱讀 214評(píng)論 0 2
  • 文/陌宇軒 熱浪 無(wú)故席卷著 草間流逝的風(fēng)想留下來(lái)卻沒有太多的可能 花朵學(xué)會(huì)躬身?xiàng)?時(shí)刻想提醒烏云和雷 溫度展開...
    小哲小詩(shī)閱讀 144評(píng)論 0 0