iOS NSCache内存优化

来源:互联网 发布:hadoop ssh端口配置 编辑:程序博客网 时间:2024/06/06 02:21

       做Java语言开发的都知道LruCache类, iOS与其对应的就是NSCache, 它俩功能类似但实现原理不同。 在Xcode API Reference里有NSCache类的详细介绍, 简要介绍一下:

1、NSCache是苹果提供的缓存类,主流的三方图片框架都用它,例如OC版的SDWebImage和Swift版的Kingfisher。

2、NSCache在系统内存低时,会自动释放一些对象(有点像Android的弱引用);但实际编码过程中,推荐在低内存的警告函数里主动释放NSCache对象。

3、NSCache自动释放内存的算法是不确定的, 有时是按照LRU释放,有时随机释放;算是个坑,要重点关注!!!

4、NSCache的API是线程安全的, 不用担心多线程并发问题;

5、NSCache是Key-Value数据结构,其中key是强引用,不实现NSCoping协议。


NSCache类重要属性:

1、countLimit: 就是缓存对象的总数,默认值是0,即不限制;

2、totalCostLimit: 缓存对象的总大小,超出上限时会先释放足够的内存空间出来再执行插入操作;

3、delegate协议属性:缓存的value对象可以实现该NSDiscardableContent
协议,当NSCache要释放对象时会执行回调函数,在回调函数里释放value对象申请的其它资源。


    场景: Android ListView的item有图片,如果不做处理,滑动列表时内存会一直增长(大多数app都用了图片库,但忽视了其内存回收的原理。简要说就是注册ListView的recycleListener或者重载控件的onDetach函数,控件释放图片的引用)。

    那么iOS会不会出现类似的问题呢?  写个tableview,10000条记录,每条记录都是1个图片和1行文字, 快速滑动列表并观察内存变化, 发现内存并没增长!!!  


 这里要注意获取UIImage对象时的区别:

1、使用named方式iOS会将图片缓存到内存, 以文件名为关键字在内存缓存中查,查不到时才从资源里读文件;优点是频繁访问时速度快,缺点是吃内存;

2、使用contensOfFile方式就是每次都从资源里读文件,不做内存缓存; 

        //let image = UIImage(named: "image1")  //iOS会将图片缓存到内存,下次先通过名字从内存查
        let path = Bundle.main.path(forResource: "image1", ofType: "png" ) 
        let image = UIImage(contentsOfFile: path!)   //不缓存,每次都读文件和解码


   因为是要验证是否内存增长,所以在demo中使用contentsOfFile方式。


      demo中扩展了UIImage类并实现NSDiscardableContent协议, 查询滑动列表时的日志发现。 NSCache在释放value对象时,会执行value的discardContentIfPossible, 从名字就可以看出该回调函数的作用是释放value对象申请的资源。

cache release: <_TtCC12NSCacheTable14ViewController8ImageExt: 0x7c8391f0>, {560, 398}discardContentIfPossible: 1cache release: <_TtCC12NSCacheTable14ViewController8ImageExt: 0x7c8de1c0>, {560, 398}discardContentIfPossible: 1cache release: <_TtCC12NSCacheTable14ViewController8ImageExt: 0x7c734ba0>, {560, 398}discardContentIfPossible: 1


     UITableView/UICollectionView内存优化的套路就包括将图片保存到NSCache中, 每次先从NSCache中取,如果查不到再执行上面的UIImage构造方法。

测试代码:

class ViewController: UIViewController, UITableViewDataSource, NSCacheDelegate {    var tablewView: UITableView?    let CELL_IDENTIFIER = "cell_image"        var cache: NSCache<NSString, NSObject>?        //有子对象需要释放资源时可以实现NSDiscardableContent接口    class ImageExt: UIImage, NSDiscardableContent {        var count = 1        var isReleased = false                 //主动调用,做逻辑判断。 NSCache不会调用        public func beginContentAccess() -> Bool {            print("beginContentAccess: \(count)")            count += 1            return true        }                 //主动调用,做逻辑判断。 NSCache不会调用        public func endContentAccess() {            print("endContentAccess: \(count)")            count -= 1        }                //NSCache类会回调执行该方法        public func discardContentIfPossible() {            print("discardContentIfPossible: \(count)")            if count == 0 && !isReleased {                //.....执行释放流程                isReleased = true            } else {                isReleased = false            }        }                //主动调用,做逻辑判断。 NSCache不会调用        public func isContentDiscarded() -> Bool {            print("isContentDiscarded: \(count)")            return isReleased        }    }        override func viewDidLoad() {        super.viewDidLoad()        // Do any additional setup after loading the view, typically from a nib.                tablewView = UITableView(frame: self.view.frame)        tablewView?.backgroundColor = UIColor.white        tablewView?.dataSource = self        tablewView?.register(ImageTableViewCell.self, forCellReuseIdentifier: CELL_IDENTIFIER)        tablewView?.estimatedRowHeight = 150        tablewView?.rowHeight = UITableViewAutomaticDimension        tablewView?.separatorColor = UIColor.gray        tablewView?.separatorStyle = .singleLine        tablewView?.tableFooterView = UIView()   //解决空白横线问题                self.view.addSubview(tablewView!)                cache = NSCache<NSString, NSObject>()        cache?.countLimit = 10   //最多10个        cache?.totalCostLimit = 50 * 1024 * 1024   //最多缓存50M字节        cache?.delegate = self    }    override func didReceiveMemoryWarning() {        super.didReceiveMemoryWarning()        // Dispose of any resources that can be recreated.                cache?.removeAllObjects()    //释放内存    }    public func cache(_ cache: NSCache<AnyObject, AnyObject>, willEvictObject obj: Any) {        print("cache release: \(obj)")    }    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {        return 100000    }            func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {        var cell = tableView.dequeueReusableCell(withIdentifier: CELL_IDENTIFIER, for: indexPath)                                        as? ImageTableViewCell                if (cell == nil) {            cell = ImageTableViewCell(style: .default, reuseIdentifier: CELL_IDENTIFIER)        }                        //说明:iOS跟Android的listview机制不同,iOS可以自动释放无效item的内存!!!!!!        //let image = UIImage(named: "image1")  //iOS会将图片缓存到内存,下次先通过名字从内存查        let path = Bundle.main.path(forResource: "image1", ofType: "png" )        let image = UIImage(contentsOfFile: path!)   //不缓存,每次都读文件和解码                let imageExt = ImageExt(contentsOfFile: path!)        cache?.setObject(imageExt!, forKey: "\(indexPath.row)" as NSString, cost: 1 * 1024 * 1024)   //模拟1M字节        //cache?.setObject(image!, forKey: "\(indexPath.row)" as NSString, cost: 1 * 1024 * 1024)   //模拟5M字节        //cell?.icon?.image = cache?.object(forKey: "\(indexPath.row)" as NSString) as! UIImage        cell?.icon?.image = image        return cell!    }        func numberOfSections(in tableView: UITableView) -> Int {        return 1    }}


class ImageTableViewCell: UITableViewCell {        var icon: UIImageView?    var name: UILabel?        override init(style: UITableViewCellStyle, reuseIdentifier: String?) {        super.init(style: style, reuseIdentifier: reuseIdentifier)                initViews()    }        required init?(coder aDecoder: NSCoder) {        fatalError("init(coder:) has not been implemented")    }        /**     * 初始化控件     * 注意: snapkit的offset函数在top/left使用正数、right/bottom使用负数!     */    func initViews() {        icon = UIImageView()        icon?.frame = CGRect(x: 0, y: 0, width: 50, height: 50)        //let image = UIImage(named: "image_1") //缓存起来,下次读取先从内存查                //let path = Bundle.main.path(forResource: "image1", ofType: "png")        //let image = UIImage(contentsOfFile: path!)   //不缓存,每次都重新加载        //icon?.image = image        self.contentView.addSubview(icon!)        icon?.snp.makeConstraints{ make in            make.width.height.equalTo(50)            make.top.equalTo(self.contentView).offset(10)            make.left.equalTo(self.contentView.snp.left).offset(5)            make.centerY.equalTo(self.contentView)            make.bottom.equalTo(self.contentView).offset(-10)   //这是是负数-10        }                name = UILabel()        name?.font = UIFont.systemFont(ofSize: 16)        name?.textColor = .black        name?.text = "测试死啦地方;少量的看风景;的说法快速的"        name?.adjustsFontSizeToFitWidth = true        self.contentView.addSubview(name!)        name?.snp.makeConstraints{ make in            make.centerY.equalTo(self.contentView)            make.left.equalTo(icon!.snp.right).offset(10)            make.right.equalTo(self.contentView).offset(-10)   //负数!        }            }    override func awakeFromNib() {        super.awakeFromNib()        // Initialization code                    }    override func setSelected(_ selected: Bool, animated: Bool) {        super.setSelected(selected, animated: animated)        // Configure the view for the selected state    }}




原创粉丝点击