• 本文由CocoaChina--夜微眠(github)翻译

  • 作者:@Todd Kramer

  • 原文:Improving UICollectionView & UITableView Scrolling Performance With AsyncDisplayKit


目标:使用AsyncDisplayKit和Alamofire的异步下载、缓存以及图像解码 来提升UICollectionView的滚动性能。
上一篇教程 Downloading & Caching Images Asynchronously In Swift With Alamofire (使用Alamofire异步下载以及缓存图片)中,我描述了如何使用Alamofire和AlamofireImage 库异步下载和缓存图片,进而显示在UICollectionView中。通过使用这些库可以轻松实现滚动流畅的滚动视图和集合视图,但是如果你的UI很复杂,图片很多,有可能不能达到60fps

此次教程中我们使用Facebook AsyncDisplayKit库重建Glacier Scenics工程,AsyncDisplayKit有很多我们提升滚动流畅所需要的工具以及图片异步下载功能(如果你对缓存不感兴趣)。如果需要实现缓存的话,Alamofire和AlamofireImage还是可以派上用场。

AsyncDisplayKit 概览

AsyncDisplayKit用相关node类,替换了UIView和它的子类,而且是线程安全的。它可以异步解码图片,调整图片大小以及对图片和文本进行渲染。在大部分项目中,主要的目标就是实现图片异步解码。UIImage显示之前必须要先解码完成,而且解码还是同步的。尤其是在UICollectionView/UITableView 中使用 prototype cell显示大图,UIImage的同步解码在滚动的时候会有明显的卡顿。

另外一个很吸引人的点是AsyncDisplayKit可以把view层次结构转成layer。因为复杂的view层次结构开销很大,如果不需要view特有的功能(例如点击事件),就可以使用AsyncDisplayKit 的layer backing特性从而获得一些额外的提升。

AsyncDisplayKit还有很多其他的特性,最后要提到就是基于node把UICollectionView 和 UITableView 替换为 ASCollectionView 和 ASTableView 的特性。替换的类可以使用UIkit中大量的数据源和 delegate方法,这样便于你很快适应从UIKit部分到基于node架构的变化。

尽管AsyncDisplayKit基于node的架构,但每个node都有相应UIView 属性。这样你可以添加不需要与node类有交互的子视图。

设置

下图就是我们完成的工程

工程依赖
使用CocoaPods获取AsyncDisplayKit依赖,下面是Podfile

1
2
3
4
5
6
platform :ios, '8.0'  
use_frameworks!
target 'GlacierScenics' do  
  pod 'AsyncDisplayKit'
end

数据

图片的名称和URL从property list(plist)文件获取,分别是两个带有"name" and "imageURL"的数组。

Storyboard

项目中的Storyboard很简单,是因为AsyncDisplayKit不支持Storyboard,所以相关约束都用代码实现。我们只需要一个navigation controller 和root view controller(等下会被设成PhotosViewController)

ASCollectionView默认图片下载

第一步从plist里读取数据。我们先定义一个简单的struct GlacierScenic 存照片信息

1
2
3
4
struct GlacierScenic {  
    let name: String
    let photoURLString: String
}

这就可以了。下一步我们创建一个数据管理器从plist读取和存储照片信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class PhotosDataManager {
    static let sharedManager = PhotosDataManager()
    private var photos = [GlacierScenic]()
     
    func allPhotos() -> [GlacierScenic] {
        if !photos.isEmpty { return photos }
        guard let data = NSArray(contentsOfFile: dataPath()) as? [NSDictionary] else return photos }
        for photoInfo in data {
            let name = photoInfo["name"] as! String
            let urlString = photoInfo["imageURL"] as! String
            let glacierScenic = GlacierScenic(name: name, photoURLString: urlString)
            photos.append(glacierScenic)
        }
        return photos
    }
     
    func dataPath() -> String {
        return NSBundle.mainBundle().pathForResource("GlacierScenics", ofType: "plist")!
    }
     
}

接下来看下view controller代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import UIKit  
import AsyncDisplayKit
class PhotosViewController: UIViewController {
    var collectionView: ASCollectionView!
    var photosDataSource = PhotosDataSource()
     
    //MARK: - View Controller Lifecycle
     
    override func viewDidLoad() {
        super.viewDidLoad()
         
        configureCollectionView()
    }
     
    override func prefersStatusBarHidden() -> Bool {
        return true
    }
     
    override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
         
        coordinator.animateAlongsideTransition({ (context) -> Void in
            self.collectionView.frame.size = self.view.frame.size
            self.collectionView.reloadData()
            }, completion: nil)
    }
     
    //MARK: - Collection View
     
    func configureCollectionView() {
        let layout = UICollectionViewFlowLayout()
        layout.minimumInteritemSpacing = 1
        layout.minimumLineSpacing = 1
        var frame = view.frame
        if let navigationBar = navigationController?.navigationBar {
            frame.size.height -= navigationBar.frame.height
        }
        collectionView = ASCollectionView(frame: frame, collectionViewLayout: layout)
        collectionView.backgroundColor = UIColor.blackColor()
        collectionView.asyncDataSource = photosDataSource
        view.addSubview(collectionView)
        collectionView.reloadData()
    }
     
}

我们这所需要做的就是配置collection view  以及处理不同大小size classes之间的转场变化。

这里有一些注意事项:

  • 第一,ASCollectionView使用asyncDataSource和asyncDelegate。这个很重要,因为ASCollectionView也有标准的data Source 和 delegate。所以获取数据源和委托的时候不要混淆。

  • 第二,ASCollectionView构造器需要UICollectionViewLayout参数,但是不是所有的布局配置能生效。一个很重要的例子就是cell大小,这个需要用另外的方法处理。

  • 最后,collectionView的布局属性有个方法invalidateLayout不起作用(问题),所以我们不使用viewWillTransitionToSize方法。

现在我们需要实现上边设置的data source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import UIKit  
import AsyncDisplayKit
class PhotosDataSource: NSObject, ASCollectionDataSource {
    func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
        return 1
    }
    func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return PhotosDataManager.sharedManager.allPhotos().count
    }
    func collectionView(collectionView: ASCollectionView, nodeForItemAtIndexPath indexPath: NSIndexPath) -> ASCellNode {
        let glacierScenic = glacierScenicAtIndex(indexPath)
        return PhotoCollectionViewCellNode(glacierScenic: glacierScenic)
    }
    func glacierScenicAtIndex(indexPath: NSIndexPath) -> GlacierScenic {
        let photos = PhotosDataManager.sharedManager.allPhotos()
        return photos[indexPath.row]
    }
}

这段代码很简单。我们用一个section显示图片。接着我们返回一个新的collection view cell node (AsyncDisplayKit的 ASCellNode 子类)。

要注意的是AsyncDisplayKit 用nodeForItem 方法替换了cellForItem方法,也就需要在collection view上注册reuse identifiers。

最后就是PhotoCollectionViewCellNode。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import UIKit  
import AsyncDisplayKit
class PhotoCollectionViewCellNode: ASCellNode {
    var loadingIndicator = UIActivityIndicatorView(activityIndicatorStyle: .WhiteLarge)
    var imageNode = ASNetworkImageNode()
    var blurView = UIVisualEffectView(effect: UIBlurEffect(style: .Light))
    var captionContainerNode = ASDisplayNode()
    var captionLabelNode = AttributedTextNode()
    let glacierScenic: GlacierScenic
    var nodeSize: CGSize {
        let spacing: CGFloat = 1
        let screenWidth = UIScreen.mainScreen().bounds.width
        let itemWidth = floor((screenWidth / 2) - (spacing / 2))
        let itemHeight = floor((screenWidth / 3) - (spacing / 2))
        return CGSize(width: itemWidth, height: itemHeight)
    }
    init(glacierScenic: GlacierScenic) {
        self.glacierScenic = glacierScenic
        super.init()
        configure()
    }
    func configure() {
        backgroundColor = UIColor.blackColor()
        configureLoadingIndicator()
        configureImageNode()
        configureCaptionNodes()
    }
    func configureLoadingIndicator() {
        loadingIndicator.center = loadingIndicatorCenter()
        view.addSubview(loadingIndicator)
        loadingIndicator.startAnimating()
        view.addSubview(loadingIndicator)
    }
    func loadingIndicatorCenter() -> CGPoint {
        let centerX = nodeSize.width / 2
        let centerY = nodeSize.height / 2 - captionContainerFrame().height / 2
        return CGPoint(x: centerX, y: centerY)
    }
    func configureImageNode() {
        imageNode.frame = viewFrame()
        imageNode.delegate = self
        imageNode.URL = NSURL(string: glacierScenic.photoURLString)
        addSubnode(imageNode)
    }
    func configureCaptionNodes() {
        configureCaptionBlurView()
        configureCaptionContainerNode()
        configureCaptionLabelNode()
    }
    func configureCaptionBlurView() {
        blurView.frame = captionContainerFrame()
        view.addSubview(blurView)
    }
    func configureCaptionContainerNode() {
        captionContainerNode.frame = captionContainerFrame()
        captionContainerNode.backgroundColor = UIColor.blackColor().colorWithAlphaComponent(0.5)
        addSubnode(captionContainerNode)
    }
    func configureCaptionLabelNode() {
        captionLabelNode.configure(glacierScenic.name, size: 16, textAlignment: .Center)
        let constrainedSize = CGSize(width: nodeSize.width, height: CGFloat.max)
        let labelNodeHeight: CGFloat = captionLabelNode.attributedString!.boundingRectWithSize(constrainedSize, options: .UsesFontLeading, context: nil).height
        let labelNodeYValue = captionContainerFrame().height / 2 - labelNodeHeight / 2
        captionLabelNode.frame = CGRect(x: 0, y: labelNodeYValue, width: nodeSize.width, height: labelNodeHeight)
        captionContainerNode.addSubnode(captionLabelNode)
    }
    func captionContainerFrame() -> CGRect {
        let containerHeight: CGFloat = 35
        return CGRect(x: 0, y: nodeSize.height - containerHeight, width: nodeSize.width, height: containerHeight)
    }
    func viewFrame() -> CGRect {
        return CGRect(x: 0, y: 0, width: nodeSize.width, height: nodeSize.height)
    }
    override func calculateLayoutThatFits(constrainedSize: ASSizeRange) -> ASLayout {
        return ASLayout(layoutableObject: self, size: nodeSize)
    }
}
extension PhotoCollectionViewCellNode: ASNetworkImageNodeDelegate {
    func imageNode(imageNode: ASNetworkImageNode, didLoadImage image: UIImage) {
        loadingIndicator.stopAnimating()
    }
}

你可能已经注意到很多代码都是layout代码。是因为AsyncDisplayKit使用动态布局机制。复杂的布局已经超出了这篇教程的范围,但如果你只需要一个固定的cell大小,那重写calculateLayoutThatFits方法就可以了。注意,计算型属性“nodeSize”代码在类的顶部。

AsyncDisplayKit使得异步下载图片变得非常简单,此外ASImageNode可以作为UIImageView的一部分,AsyncDisplayKit还有ASNetworkImageNode子类 ,你只需要把图片设置URL属性就可以了。

在这个例子中,我们还需要一个在图片下载完成时终止加载动画的加载指示器。因为ASNetworkImageNode有delegate属性,等下我们可以使用扩展来实现delegate和处理加载指示。delegate还提供了何时图片解码完成以及图片下载失败的方法。

下一步使用“AttributedTextNode”作为标题,与UILabel不同,ASTextNode没有默认字体的“text”属性,它使用attributed string。AttributedTextNode子类提供了一个实用的函数来处理node的attributed string

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import UIKit  
import AsyncDisplayKit
class AttributedTextNode: ASTextNode {
    func configure(text: String, size: CGFloat, color: UIColor = UIColor.whiteColor(), textAlignment: NSTextAlignment = .Left) {
        let mutableString = NSMutableAttributedString(string: text)
        let range = NSMakeRange(0, text.characters.count)
        mutableString.addAttribute(NSFontAttributeName, value: UIFont.systemFontOfSize(size), range: range)
        mutableString.addAttribute(NSForegroundColorAttributeName, value: color, range: range)
        let paragraphStyle = NSMutableParagraphStyle()
        paragraphStyle.alignment = textAlignment
        mutableString.addAttribute(NSParagraphStyleAttributeName, value: paragraphStyle, range: range)
        attributedString = mutableString
    }
}

最后,如上所述,我们能获取cell node的view属性来添加没有相应node classes的subview 。本文例子中就有UIActivityIndicatorView 和 UIVisualEffectView

图像缓存

AsyncDisplayKit让异步图片下载变得非常简单,但是没有默认缓存支持。那么为了实现缓存,我们需要替代AsyncDisplayKit默认的下载器,所以我们用Alamofire和AlamofireImage实现下载和缓存。首先我们先更新Podfile

1
2
3
4
5
6
7
platform :ios, '8.0'  
use_frameworks!
target 'GlacierScenics' do  
  pod 'AsyncDisplayKit'
  pod 'AlamofireImage''~> 2.0'
end

警告:运行前,先执行pod install

之前,我们用无参数初始化network image node。AsyncDisplayKit还有另外一个以缓存和下载器为参数的构造器。

缓存和下载器需要遵照ASImageCacheProtocol 和 ASImageDownloaderProtocol 协议。我们工程中缓存和下载及都实现在PhotosDataManager 中,所以我们需要更新PhotosDataManager以实现这些协议并提供缓存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import UIKit  
import Alamofire  
import AlamofireImage  
import AsyncDisplayKit
class PhotosDataManager: NSObject {
    static let sharedManager = PhotosDataManager()
    private var photos = [GlacierScenic]()
    let photoCache = AutoPurgingImageCache(
        memoryCapacity: 100 * 1024 * 1024,
        preferredMemoryUsageAfterPurge: 60 * 1024 * 1024
    )
    func allPhotos() -> [GlacierScenic] {
        if !photos.isEmpty { return photos }
        guard let data = NSArray(contentsOfFile: dataPath()) as? [NSDictionary] else return photos }
        for photoInfo in data {
            let name = photoInfo["name"] as! String
            let urlString = photoInfo["imageURL"] as! String
            let glacierScenic = GlacierScenic(name: name, photoURLString: urlString)
            photos.append(glacierScenic)
        }
        return photos
    }
    func cacheImage(url: String, image: Image) {
        photoCache.addImage(image, withIdentifier: url)
    }
    func cachedImage(url: String) -> Image? {
        return photoCache.imageWithIdentifier(url)
    }
    func dataPath() -> String {
        return NSBundle.mainBundle().pathForResource("GlacierScenics", ofType: "plist")!
    }
}
extension PhotosDataManager: ASImageDownloaderProtocol {  
    func downloadImageWithURL(URL: NSURL, callbackQueue: dispatch_queue_t?, downloadProgressBlock: ((CGFloat) -> Void)?, completion: ((CGImage?, NSError?) -> Void)?) -> AnyObject? {
        let request = Alamofire.request(.GET, URL.absoluteString).responseImage { (response) -> Void in
            guard let image = response.result.value else {
                completion?(nil, nil)
                return
            }
            self.cacheImage(URL.absoluteString, image: image)
            completion?(image.CGImage, nil)
        }
        return request
    }
    func cancelImageDownloadForIdentifier(downloadIdentifier: AnyObject?) {
        if let request = downloadIdentifier where request is Request {
            (request as! Request).cancel()
        }
    }
}
extension PhotosDataManager: ASImageCacheProtocol {  
    func fetchCachedImageWithURL(URL: NSURL?, callbackQueue: dispatch_queue_t?, completion: (CGImage?) -> Void) {
        if let url = URL, cachedImage = cachedImage(url.absoluteString) {
            completion(cachedImage.CGImage)
            return
        }
        completion(nil)
    }
}

现在我们加了photoCache 以及两个函数,一个用于缓存图片,另外一个用于获取缓存图片。缓存最大为100MB,最优为60MB。缓存标识使用图片的URL,AsyncDisplayKit协议将会在设置network image node的URL属性后进行传递。

接着我们实现协议。第一个协议ASImageCacheProtocol就包含一个方法fetchCachedImageWithURL,用于获取缓存图片,如果对于的URL的图片存在,就返回。否则nil传给completion block,这样就会触发下载图片。

第二个协议ASImageDownloaderProtocol 包含两个方法,一个下载另一个取消下载。下载方法里我们用Alamofire Request下载图片,如果下载成功则进行缓存,然后调用 completion block。要注意的是,我们也要返回请求对象。如果取消下载,则"cancelImageDownloadForIdentifier"方法会用到它。

在取消方法里,先检查下载标识是否存在,request 是不是Request对象,然后在request上调用cancel()方法。

最后,我们替换掉PhotoCollectionViewCellNode 里ASNetworkImageNode 构造器

1
2
3
4
5
6
7
8
func configureImageNode() {  
    let manager = PhotosDataManager.sharedManager
    imageNode = ASNetworkImageNode(cache: manager, downloader: manager)
    imageNode.frame = viewFrame()
    imageNode.delegate = self
    imageNode.URL = NSURL(string: glacierScenic.photoURLString)
    addSubnode(imageNode)
}

Layer Backing

在介绍之前,我们再加一个优化。就是AsyncDisplayKit概览中提到layer backing。它能够帮助我们通过将视图层次结构转成layer层来提升滚动性能。我们的案例中,

view/node的层次结构不太复杂,但是有两处可以添加Layer Backing。第一处就是image node,实现起来就一行代码,将layerBacked 属性设置为true。

1
2
3
4
5
6
7
8
9
func configureImageNode() {  
    let manager = PhotosDataManager.sharedManager
    imageNode = ASNetworkImageNode(cache: manager, downloader: manager)
    imageNode.frame = viewFrame()
    imageNode.delegate = self
    imageNode.URL = NSURL(string: glacierScenic.photoURLString)
    imageNode.layerBacked = true
    addSubnode(imageNode)
}

第二处就是 container node 以及caption label subnode。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func configureCaptionContainerNode() {  
    captionContainerNode.frame = captionContainerFrame()
    captionContainerNode.backgroundColor = UIColor.blackColor().colorWithAlphaComponent(0.5)
    captionContainerNode.layerBacked = true
    addSubnode(captionContainerNode)
}
func configureCaptionLabelNode() {  
    captionLabelNode.configure(glacierScenic.name, size: 16, textAlignment: .Center)
    let constrainedSize = CGSize(width: nodeSize.width, height: CGFloat.max)
    let labelNodeHeight: CGFloat = captionLabelNode.attributedString!.boundingRectWithSize(constrainedSize, options: .UsesFontLeading, context: nil).height
    let labelNodeYValue = captionContainerFrame().height / 2 - labelNodeHeight / 2
    captionLabelNode.frame = CGRect(x: 0, y: labelNodeYValue, width: nodeSize.width, height: labelNodeHeight)
    captionContainerNode.layer.addSublayer(captionLabelNode.layer)
}

AsyncDisplayKit通过把图片解码、大小调整以及图像文本的渲染放在子线程,从而提升collectionview 和 tableview的滚动性能。也正如刚才看到的,AsyncDisplayKit默认下载不支持缓存,所以使用前需要考虑到AsyncDisplayKit一些不足的地方。

第一,AsyncDisplayKit不支持Storyboard、Xib以及Autolayout,不过并不意味着你不能在项目中使用这些工具,事实上我们依然在这个项目中使用了storyboard。如果你需要用Interface Builder和Autolayout实现collection view,那就需要另外的方法来提高流畅度。当然,如果不使用Autolayout就用程序写frame这样可以减少约束相关的消耗。总的来说,如果项目中一定要用到Autolayout,可能就要自己实现异步图片解码了。

第二,UITableView 和 UICollectionView一些重要的方法没有被AsyncDisplayKit替换或继承。在写这篇文章前,他们还处于开发阶段,有肯能会有所变化。

总的来说,无论用不用AsyncDisplayKit或者其他第三方库,这个取决于cell和collection view  UI相关细节。虽然有时候你决定自己实现它的一些功能,但是该库提供

一个很好的处理UITableView 和 UICollectionView性能问题的途径。

文章中使用的项目源码存放在"GlacierScenicsAsyncDisplayKit" 文件夹下。

使用AsyncDisplayKit提升UICollectionView和UITableView的滚动性能相关推荐

  1. UICollectionView和UITableView的区别

    原文:https://blog.csdn.net/vbirdbest/article/details/50720915 1. UICollectionView 和 UITableView 的UI区别 ...

  2. 官方文档链接(Core Graphics、View Controller、UICollectionView、UITableView、第三方库)

    Core Graphics Quartz 2D Programming Guide Core Graphics (Framework) Drawing(UIKit).Images and PDF(UI ...

  3. iOS - UITableView reloadData滚动到顶部无效问题解决

    iOS - UITableView reloadData滚动到顶部无效问题解决 参考文章: (1)iOS - UITableView reloadData滚动到顶部无效问题解决 (2)https:// ...

  4. 用FlatBuffers提升Android平台上Facebook的性能

    在Facebook上,人们可以通过阅读状态更新和查看照片同他们的家人和朋友来往.在我们的后端,我们保存了组成这些连接的社交图谱的所有数据.在我们的移动客户端,我们不能下载完整的图谱,而是以一个本地的树 ...

  5. Atitit.提升软件Web应用程序 app性能的方法原理 h5 js java c# php python android .net

    Atitit.提升软件Web应用程序 app性能的方法原理 h5 js java c# php python android .net 1. 提升单例有能力的1 2. 减少工作数量2 2.1. 减少距 ...

  6. scroll滚动性能优化

    在绑定 scroll .resize 这类事件时,当它发生时,它被触发的频次非常高,间隔很近.如果事件中涉及到大量的位置计算.DOM 操作.元素重绘等工作且这些工作无法在下一个 scroll 事件触发 ...

  7. 系列解读SMC-R:透明无感提升云上 TCP 应用网络性能(一)| 龙蜥技术

    文/龙蜥社区高性能网络SIG 引言 Shared Memory Communication over RDMA (SMC-R) 是一种基于 RDMA 技术.兼容 socket 接口的内核网络协议,由 ...

  8. UITableView上下滚动卡顿(获取网络数据,下载图片之后)

    今天遇到一个问题,从网络上,下载图片之后tableview上下滚动会很卡  通过上网查资料,找到解决办法  因为下载图片的时候是在主线程进行的  通过开辟一个子线程去下载图片 tableview上下滚 ...

  9. 记录--两行CSS让页面提升了近7倍渲染性能!

    这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 前言 对于前端人员来讲,最令人头疼的应该就是页面性能了,当用户在访问一个页面时,总是希望它能够快速呈现在眼前并且是可交互状态.如果页面加载 ...

  10. 两行CSS让页面提升了近7倍渲染性能

    前言 对于前端人员来讲,最令人头疼的应该就是页面性能了,当用户在访问一个页面时,总是希望它能够快速呈现在眼前并且是可交互状态.如果页面加载过慢,你的用户很可能会因此离你而去.所以页面性能对于前端开发者 ...

最新文章

  1. DOS批处理高级教程精选(六)
  2. 如何解构单体前端应用——前端应用的微服务式拆分
  3. 引领深度学习潮流,刷屏计算机视觉顶会,揭秘商汤研究梦之队
  4. Handler消息处理机制
  5. Ubuntu 14.04 改变文件或者文件夹的拥有者
  6. SpringBoot取出信息
  7. JDATA绝对语义识别挑战大赛-季军方案
  8. Spring Boot的快速入门
  9. IntelliJ IDEA 默认快捷键大全
  10. 地图统计_博客 城市访问量统计并且通过Echarts+百度地图展示
  11. c++ python opencv_从C++到Python的OpenCV垫
  12. oracle中多个数据库连接池,数据库连接池为什么要建立多个连接
  13. 散射回波仿真Matlab,基于matlab的体目标回波模拟方法与流程
  14. 【RecSys】推荐系统和计算广告经典算法论文及实现总结
  15. Java写的答题助手项目分析与总结(三)
  16. 树莓派 kali系统默认密码
  17. CASAIM自动化精密尺寸测量设备全尺寸检测铸件自动化检测铸件
  18. 计算机辅助外语教学,【外语教学论文】计算机辅助外语教学浅述(共3543字)
  19. 在Excel工作簿中显示网络图片
  20. iOS 中生成随机数的4种方法(rand、random、arc4random、arc4random_uniform)

热门文章

  1. Tomcat Linux下自启动
  2. 用svn进行多人合作开发
  3. 数据库设计经验谈[2]
  4. IntelliJ IDEA使用技巧(七)——常用快捷键Mac篇
  5. Ubuntu 11.10 下安装 JDK_6_27
  6. C#效率优化(2)-- 方法内联
  7. UEditor使用说明
  8. BS CS 优缺点比较 及 适应场合 (部分转载+个人见解)
  9. Python 持久存储
  10. 全国省市县无刷新级联菜单