最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

EvenLoop模型在iOS的RunLoop應(yīng)用示例

 更新時(shí)間:2022年07月20日 11:26:51   作者:向輝_  
這篇文章主要為大家介紹了EvenLoop模型在iOS的RunLoop應(yīng)用示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

引言

Runloop在iOS中是一個(gè)很重要的組成部分,對(duì)于任何單線程的UI模型都必須使用EvenLoop才可以連續(xù)處理不同的事件,而RunLoop就是EvenLoop模型在iOS中的實(shí)現(xiàn)。在前面的幾篇文章中,我已經(jīng)介紹了Runloop的底層原理等,這篇文章主要是從實(shí)際開發(fā)的角度,探討一下實(shí)際上在哪些場(chǎng)景下,我們可以去使用RunLoop。

線程?;?/h2>

在實(shí)際開發(fā)中,我們通常會(huì)遇到常駐線程的創(chuàng)建,比如說發(fā)送心跳包,這就可以在一個(gè)常駐線程來發(fā)送心跳包,而不干擾主線程的行為,再比如音頻處理,這也可以在一個(gè)常駐線程中來處理。以前在Objective-C中使用的AFNetworking 1.0就使用了RunLoop來進(jìn)行線程的?;?。

var thread: Thread!
func createLiveThread() {
		thread = Thread.init(block: {
				let port = NSMachPort.init()
        RunLoop.current.add(port, forMode: .default)
        RunLoop.current.run()
		})
		thread.start()
}

值得注意的是RunLoop的mode中至少需要一個(gè)port/timer/observer,否則RunLoop只會(huì)執(zhí)行一次就退出了。

停止Runloop

離開RunLoop一共有兩種方法:其一是給RunLoop配置一個(gè)超時(shí)的時(shí)間,其二是主動(dòng)通知RunLoop離開。Apple在文檔中是推薦第一種方式的,如果能直接定量的管理,這種方式當(dāng)然是最好的。

設(shè)置超時(shí)時(shí)間

然而實(shí)際中我們無法準(zhǔn)確的去設(shè)置超時(shí)的時(shí)刻,比如在線程?;畹睦又校覀冃枰WC線程的RunLoop一直保持運(yùn)行中,所以結(jié)束的時(shí)間是一個(gè)變量,而不是常量,要達(dá)到這個(gè)目標(biāo)我們可以結(jié)合一下RunLoop提供的API,在開始的時(shí)候,設(shè)置RunLoop超時(shí)時(shí)間為無限,但是在結(jié)束時(shí),設(shè)置RunLoop超時(shí)時(shí)間為當(dāng)前,這樣變相通過控制timeout的時(shí)間停止了RunLoop,具體代碼如下:

var thread: Thread?
var isStopped: Bool = false
func createLiveThread() {
		thread = Thread.init(block: { [weak self] in
				guard let self = self else { return }
				let port = NSMachPort.init()
        RunLoop.current.add(port, forMode: .default)
				while !self.isStopped {
		        RunLoop.current.run(mode: .default, before: Date.distantFuture)
        }
		})
		thread?.start()
}
func stop() {
		self.perform(#selector(self.stopThread), on: thread!, with: nil, waitUntilDone: false)
}
@objc func stopThread() {
		self.isStopped = true
		RunLoop.current.run(mode: .default, before: Date.init())
    self.thread = nil
}

直接停止

CoreFoundation提供了API:CFRunLoopStop() 但是這個(gè)方法只會(huì)停止當(dāng)前這次循環(huán)的RunLoop,并不會(huì)完全停止RunLoop。那么有沒有其它的策略呢?我們知道RunLoop的Mode中必須要至少有一個(gè)port/timer/observer才會(huì)工作,否則就會(huì)退出,而CF提供的API中正好有:

**public func CFRunLoopRemoveSource(_ rl: CFRunLoop!, _ source: CFRunLoopSource!, _ mode: CFRunLoopMode!)
public func CFRunLoopRemoveObserver(_ rl: CFRunLoop!, _ observer: CFRunLoopObserver!, _ mode: CFRunLoopMode!)
public func CFRunLoopRemoveTimer(_ rl: CFRunLoop!, _ timer: CFRunLoopTimer!, _ mode: CFRunLoopMode!)**

所以很自然的聯(lián)想到如果移除source/timer/observer, 那么這個(gè)方案可不可以停止RunLoop呢?

答案是否定的,這一點(diǎn)在Apple的官方文檔中有比較詳細(xì)的描述:

Although removing a run loop’s input sources and timers may also cause the run loop to exit, this is not a reliable way to stop a run loop. Some system routines add input sources to a run loop to handle needed events. Because your code might not be aware of these input sources, it would be unable to remove them, which would prevent the run loop from exiting.

簡(jiǎn)而言之,就是你無法保證你移除的就是全部的source/timer/observer,因?yàn)橄到y(tǒng)可能會(huì)添加一些必要的source來處理事件,而這些source你是無法確保移除的。

延遲加載圖片

這是一個(gè)很常見的使用方式,因?yàn)槲覀冊(cè)诨瑒?dòng)scrollView/tableView/collectionView的過程,總會(huì)給cell設(shè)置圖片,但是直接給cell的imageView設(shè)置圖片的過程中,會(huì)涉及到圖片的解碼操作,這個(gè)就會(huì)占用CPU的計(jì)算資源,可能導(dǎo)致主線程發(fā)生卡頓,所以這里可以將這個(gè)操作,不放在trackingMode,而是放在defaultMode中,通過一種取巧的方式來解決可能的性能問題。

func setupImageView() {
		self.performSelector(onMainThread: #selector(self.setupImage), 
												 with: nil, 
												 waitUntilDone: false,
												 modes: [RunLoop.Mode.default.rawValue])
}
@objc func setupImage() {
		imageView.setImage()
}

卡頓監(jiān)測(cè)

目前來說,一共有三種卡頓監(jiān)測(cè)的方案,然而基本上每一種卡頓監(jiān)測(cè)的方案都和RunLoop是有關(guān)聯(lián)的。

CADisplayLink(FPS)

YYFPSLabel 采用的就是這個(gè)方案,F(xiàn)PS(Frames Per Second)代表每秒渲染的幀數(shù),一般來說,如果App的FPS保持50~60之間,用戶的體驗(yàn)就是比較流暢的,但是Apple自從iPhone支持120HZ的高刷之后,它發(fā)明了一種ProMotion的動(dòng)態(tài)屏幕刷新率的技術(shù),這種方式基本就不能使用了,但是這里依舊提供已作參考。

這里值得注意的技術(shù)細(xì)節(jié)是使用了NSObject來做方法的轉(zhuǎn)發(fā),在OC中可以使用NSProxy來做消息的轉(zhuǎn)發(fā),效率更高。

// 抽象的超類,用來充當(dāng)其它對(duì)象的一個(gè)替身
// Timer/CADisplayLink可以使用NSProxy做消息轉(zhuǎn)發(fā),可以避免循環(huán)引用
// swift中我們是沒發(fā)使用NSInvocation的,所以我們直接使用NSobject來做消息轉(zhuǎn)發(fā)
class WeakProxy: NSObject {
    private weak var target: NSObjectProtocol?
    init(target: NSObjectProtocol) {
        self.target = target
        super.init()
    }
    override func responds(to aSelector: Selector!) -> Bool {
        return (target?.responds(to: aSelector) ?? false) || super.responds(to: aSelector)
    }
    override func forwardingTarget(for aSelector: Selector!) -> Any? {
        return target
    }
}
class FPSLabel: UILabel {
    var link: CADisplayLink!
    var count: Int = 0
    var lastTime: TimeInterval = 0.0
    fileprivate let defaultSize = CGSize.init(width: 80, height: 20)
    override init(frame: CGRect) {
        super.init(frame: frame)
        if frame.size.width == 0 || frame.size.height == 0 {
            self.frame.size = defaultSize
        }
        layer.cornerRadius = 5.0
        clipsToBounds = true
        textAlignment = .center
        isUserInteractionEnabled = false
        backgroundColor = UIColor.white.withAlphaComponent(0.7)
        link = CADisplayLink.init(target: WeakProxy.init(target: self), selector: #selector(FPSLabel.tick(link:)))
        link.add(to: RunLoop.main, forMode: .common)
    }
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    deinit {
        link.invalidate()
    }
    @objc func tick(link: CADisplayLink) {
        guard lastTime != 0 else {
            lastTime = link.timestamp
            return
        }
        count += 1
        let timeDuration = link.timestamp - lastTime
        // 1、設(shè)置刷新的時(shí)間: 這里是設(shè)置為1秒(即每秒刷新)
        guard timeDuration >= 1.0 else { return }
        // 2、計(jì)算當(dāng)前的FPS
        let fps = Double(count)/timeDuration
        count = 0
        lastTime = link.timestamp
        // 3、開始設(shè)置FPS了
        let progress = fps/60.0
        let color = UIColor(hue: CGFloat(0.27 * (progress - 0.2)), saturation: 1, brightness: 0.9, alpha: 1)
        self.text = "\(Int(round(fps))) FPS"
        self.textColor = color
    }
}

子線程Ping

這種方法是創(chuàng)建了一個(gè)子線程,通過GCD給主線程添加異步任務(wù):修改是否超時(shí)的參數(shù),然后讓子線程休眠一段時(shí)間,如果休眠的時(shí)間結(jié)束之后,超時(shí)參數(shù)未修改,那說明給主線程的任務(wù)并沒有執(zhí)行,那么這就說明主線程的上一個(gè)任務(wù)還沒有做完,那就說明卡頓了,這種方式其實(shí)和RunLoop沒有太多的關(guān)聯(lián),它不依賴RunLoop的狀態(tài)。在ANREye中是采用子線程Ping的方式來監(jiān)測(cè)卡頓的。

同時(shí)為了讓這些操作是同步的,這里使用了信號(hào)量。

class PingMonitor {
    static let timeoutInterval: TimeInterval = 0.2
    static let queueIdentifier: String = "com.queue.PingMonitor"
    private var queue: DispatchQueue = DispatchQueue.init(label: queueIdentifier)
    private var isMonitor: Bool = false
    private var semphore: DispatchSemaphore = DispatchSemaphore.init(value: 0)
    func startMonitor() {
        guard isMonitor == false else { return }
        isMonitor = true
        queue.async {
            while self.isMonitor {
                var timeout = true
                DispatchQueue.main.async {
                    timeout = false
                    self.semphore.signal()
                }
                Thread.sleep(forTimeInterval:PingMonitor.timeoutInterval)
                // 說明等了timeoutInterval之后,主線程依然沒有執(zhí)行派發(fā)的任務(wù),這里就認(rèn)為它是處于卡頓的
                if timeout == true {
                    //TODO: 這里需要取出崩潰方法棧中的符號(hào)來判斷為什么出現(xiàn)了卡頓
                    // 可以使用微軟的框架:PLCrashReporter
                }
                self.semphore.wait()
            }
        }
    }
}

這個(gè)方法在正常情況下會(huì)每隔一段時(shí)間讓主線程執(zhí)行GCD派發(fā)的任務(wù),會(huì)造成部分資源的浪費(fèi),而且它是一種主動(dòng)的去Ping主線程,并不能很及時(shí)的發(fā)現(xiàn)卡頓問題,所以這種方法會(huì)有一些缺點(diǎn)。

實(shí)時(shí)監(jiān)控

而我們知道,主線程中任務(wù)都是通過RunLoop來管理執(zhí)行的,所以我們可以通過監(jiān)聽RunLoop的狀態(tài)來知道是否會(huì)出現(xiàn)卡頓的情況,一般來說,我們會(huì)監(jiān)測(cè)兩種狀態(tài):第一種是kCFRunLoopAfterWaiting 的狀態(tài),第二種是kCFRunLoopBeforeSource的狀態(tài)。為什么是兩種狀態(tài)呢?

首先看第一種狀態(tài)kCFRunLoopAfterWaiting ,它會(huì)在RunLoop被喚醒之后回調(diào)這種狀態(tài),然后根據(jù)被喚醒的端口來處理不同的任務(wù),如果處理任務(wù)的過程中耗時(shí)過長(zhǎng),那么下一次檢查的時(shí)候,它依然是這個(gè)狀態(tài),這個(gè)時(shí)候就可以說明它卡在了這個(gè)狀態(tài)了,然后可以通過一些策略來提取出方法棧,來判斷卡頓的代碼。同理,第二種狀態(tài)也是一樣的,說明一直處于kCFRunLoopBeforeSource 狀態(tài),而沒有進(jìn)入下一狀態(tài)(即休眠),也發(fā)生了卡頓。

class RunLoopMonitor {
    private init() {}
    static let shared: RunLoopMonitor = RunLoopMonitor.init()
    var timeoutCount = 0
    var runloopObserver: CFRunLoopObserver?
    var runLoopActivity: CFRunLoopActivity?
    var dispatchSemaphore: DispatchSemaphore?
    // 原理:進(jìn)入睡眠前方法的執(zhí)行時(shí)間過長(zhǎng)導(dǎo)致無法進(jìn)入睡眠,或者線程喚醒之后,一直沒進(jìn)入下一步
    func beginMonitor() {
        let uptr = Unmanaged.passRetained(self).toOpaque()
        let vptr = UnsafeMutableRawPointer(uptr)
        var context = CFRunLoopObserverContext.init(version: 0, info: vptr, retain: nil, release: nil, copyDescription: nil)
        runloopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                                  CFRunLoopActivity.allActivities.rawValue,
                                                  true,
                                                  0,
                                                  observerCallBack(),
                                                  &context)
        CFRunLoopAddObserver(CFRunLoopGetMain(), runloopObserver, .commonModes)
        // 初始化的信號(hào)量為0
        dispatchSemaphore = DispatchSemaphore.init(value: 0)
        DispatchQueue.global().async {
            while true {
                // 方案一:可以通過設(shè)置單次超時(shí)時(shí)間來判斷 比如250毫秒
								// 方案二:可以通過設(shè)置連續(xù)多次超時(shí)就是卡頓 戴銘在GCDFetchFeed中認(rèn)為連續(xù)三次超時(shí)80秒就是卡頓
                let st = self.dispatchSemaphore?.wait(timeout: DispatchTime.now() + .milliseconds(80))
                if st == .timedOut {
                    guard self.runloopObserver != nil else {
                        self.dispatchSemaphore = nil
                        self.runLoopActivity = nil
												self.timeoutCount = 0
                        return
                    }
                    if self.runLoopActivity == .afterWaiting || self.runLoopActivity == .beforeSources {
												self.timeoutCount += 1
                        if self.timeoutCount < 3 { continue }
                        DispatchQueue.global().async {
                            let config = PLCrashReporterConfig.init(signalHandlerType: .BSD, symbolicationStrategy: .all)
                            guard let crashReporter = PLCrashReporter.init(configuration: config) else { return }
                            let data = crashReporter.generateLiveReport()
                            do {
                                let reporter = try PLCrashReport.init(data: data)
                                let report = PLCrashReportTextFormatter.stringValue(for: reporter, with: PLCrashReportTextFormatiOS) ?? ""
                                NSLog("------------卡頓時(shí)方法棧:\n \(report)\n")
                            } catch _ {
                                NSLog("解析crash data錯(cuò)誤")
                            }
                        }
                    }
                }
            }
        }
    }
    func end() {
        guard let _ = runloopObserver else { return }
        CFRunLoopRemoveObserver(CFRunLoopGetMain(), runloopObserver, .commonModes)
        runloopObserver = nil
    }
    private func observerCallBack() -> CFRunLoopObserverCallBack {
        return { (observer, activity, context) in
            let weakself = Unmanaged<RunLoopMonitor>.fromOpaque(context!).takeUnretainedValue()
            weakself.runLoopActivity = activity
            weakself.dispatchSemaphore?.signal()
        }
    }
}

Crash防護(hù)

Crash防護(hù)是一個(gè)很有意思的點(diǎn),處于應(yīng)用層的APP,在執(zhí)行了某些不被操作系統(tǒng)允許的操作之后會(huì)觸發(fā)操作系統(tǒng)拋出異常信號(hào),但是因?yàn)闆]有處理這些異常從而被系操作系統(tǒng)殺掉的線程,比如常見的閃退。這里不對(duì)Crash做詳細(xì)的描述,我會(huì)在下一個(gè)模塊來描述iOS中的異常。要明確的是,有些場(chǎng)景下,是希望可以捕獲到系統(tǒng)拋出的異常,然后將App從錯(cuò)誤中恢復(fù),重新啟動(dòng),而不是被殺死。而對(duì)應(yīng)在代碼中,我們需要去手動(dòng)的重啟主線程,已達(dá)到繼續(xù)運(yùn)行App的目的。

let runloop = CFRunLoopGetCurrent()
guard let allModes = CFRunLoopCopyAllModes(runloop) as? [CFRunLoopMode] else {
    return
}
 while true {
	  for mode in allModes {
        CFRunLoopRunInMode(mode, 0.001, false)
    }
 }

CFRunLoopRunInMode(mode, 0.001, false) 因?yàn)闊o法確定RunLoop到底是怎樣啟動(dòng)的,所以采用了這種方式來啟動(dòng)RunLoop的每一個(gè)Mode,也算是一種替代方案了。因?yàn)?code>CFRunLoopRunInMode 在運(yùn)行的時(shí)候本身就是一個(gè)循環(huán)并不會(huì)退出,所以while循環(huán)不會(huì)一直執(zhí)行,只是在mode退出之后,while循環(huán)遍歷需要執(zhí)行的mode,直到繼續(xù)在一個(gè)mode中常駐。

這里只是重啟RunLoop,其實(shí)在Crash防護(hù)里最重要的還是要監(jiān)測(cè)到何時(shí)發(fā)送崩潰,捕獲系統(tǒng)的exception信息,以及singal信息等等,捕獲到之后再對(duì)當(dāng)前線程的方法棧進(jìn)行分析,定位為crash的成因。

Matrix框架

接下來我們具體看一下RunLoop在Matrix框架中的運(yùn)用。Matrix是騰訊開源的一款用于性能監(jiān)測(cè)的框架,在這個(gè)框架中有一款插件**WCFPSMonitorPlugin:**這是一款FPS監(jiān)控工具,當(dāng)用戶滑動(dòng)界面時(shí),記錄主線程的調(diào)用棧。它的源碼中和我們上述提到的通過CADisplayLink來來監(jiān)測(cè)卡頓的方案的原理是一樣的:

- (void)startDisplayLink:(NSString *)scene {
    FPSInfo(@"startDisplayLink");
    m_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(onFrameCallback:)];
    [m_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
		...
}
- (void)onFrameCallback:(id)sender {
    // 當(dāng)前時(shí)間: 單位為秒
    double nowTime = CFAbsoluteTimeGetCurrent();
    // 將單位轉(zhuǎn)化為毫秒
    double diff = (nowTime - m_lastTime) * 1000;
		// 1、如果時(shí)間間隔超過最大的幀間隔:那么此次屏幕刷新方法超時(shí)
    if (diff > self.pluginConfig.maxFrameInterval) {
        m_currRecorder.dumpTimeTotal += diff;
        m_dropTime += self.pluginConfig.maxFrameInterval * pow(diff / self.pluginConfig.maxFrameInterval, self.pluginConfig.powFactor);
        // 總超時(shí)時(shí)間超過閾值:展示超時(shí)信息
        if (m_currRecorder.dumpTimeTotal > self.pluginConfig.dumpInterval * self.pluginConfig.dumpMaxCount) {
            FPSInfo(@"diff %lf exceed, begin: %lf, end: %lf, scene: %@, you can see more detail in record id: %d",
                    m_currRecorder.dumpTimeTotal,
                    m_currRecorder.dumpTimeBegin,
                    m_currRecorder.dumpTimeBegin + m_currRecorder.dumpTimeTotal / 1000.0,
                    m_scene,
                    m_currRecorder.recordID);
						...... 
        }
		// 2、如果時(shí)間間隔沒有最大的幀間隔:那么此次屏幕刷新方法不超時(shí)
    } else {
        // 總超時(shí)時(shí)間超過閾值:展示超時(shí)信息
        if (m_currRecorder.dumpTimeTotal > self.pluginConfig.maxDumpTimestamp) {
            FPSInfo(@"diff %lf exceed, begin: %lf, end: %lf, scene: %@, you can see more detail in record id: %d",
                    m_currRecorder.dumpTimeTotal,
                    m_currRecorder.dumpTimeBegin,
                    m_currRecorder.dumpTimeBegin + m_currRecorder.dumpTimeTotal / 1000.0,
                    m_scene,
                    m_currRecorder.recordID);
						....
				// 總超時(shí)時(shí)間不超過閾值:將時(shí)間歸0 重新計(jì)數(shù)
        } else {
            m_currRecorder.dumpTimeTotal = 0;
            m_currRecorder.dumpTimeBegin = nowTime + 0.0001;
        }
    }
    m_lastTime = nowTime;
}

它通過次數(shù)以及兩次之間允許的時(shí)間間隔作為閾值,超過閾值就記錄,沒超過閾值就歸0重新計(jì)數(shù)。當(dāng)然這個(gè)框架也不僅僅是作為一個(gè)簡(jiǎn)單的卡頓監(jiān)測(cè)來使用的,還有很多性能監(jiān)測(cè)的功能以供平時(shí)開發(fā)的時(shí)候來使用:包括對(duì)崩潰時(shí)方法棧的分析等等。

總結(jié)

本篇文章我從線程?;铋_始介紹了RunLoop在實(shí)際開發(fā)中的使用,然后主要是介紹了卡頓監(jiān)測(cè)和Crash防護(hù)中的高階使用,當(dāng)然,RunLoop的運(yùn)用遠(yuǎn)不止這些,如果有更多更好的使用,希望大家可以留言交流。

以上就是EvenLoop模型在iOS的RunLoop應(yīng)用示例的詳細(xì)內(nèi)容,更多關(guān)于ios EvenLoop模型RunLoop的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Swift hello world!Swift快速入門教程

    Swift hello world!Swift快速入門教程

    這篇文章主要介紹了Swift hello world!Swift快速入門教程,本文在快速了解Swift編程語言,需要的朋友可以參考下
    2014-07-07
  • swift使用SDPhotoBriwser瀏覽圖片教程

    swift使用SDPhotoBriwser瀏覽圖片教程

    這篇文章主要為大家介紹了swift如何使用SDPhotoBriwser瀏覽圖片的教程示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步
    2021-10-10
  • swift4.0實(shí)現(xiàn)視頻播放、屏幕旋轉(zhuǎn)、倍速播放、手勢(shì)調(diào)節(jié)及鎖屏面板等功能實(shí)例

    swift4.0實(shí)現(xiàn)視頻播放、屏幕旋轉(zhuǎn)、倍速播放、手勢(shì)調(diào)節(jié)及鎖屏面板等功能實(shí)例

    這篇文章主要給大家介紹了關(guān)于swift4.0實(shí)現(xiàn)視頻播放、屏幕旋轉(zhuǎn)、倍速播放、手勢(shì)調(diào)節(jié)及鎖屏面板等功能的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。
    2018-01-01
  • Swift初始化方法的使用介紹

    Swift初始化方法的使用介紹

    Swift有著超級(jí)嚴(yán)格的初始化方法,不僅強(qiáng)化了designated初始化方法的地位,所有不加修飾的init方法都需要在方法中確保非Optional的實(shí)例變量被賦值初始化,下面這篇文章主要給大家介紹了關(guān)于Swift中初始化init的相關(guān)資料,需要的朋友可以參考下。
    2022-08-08
  • Ubuntu 16.04上安裝 Swift 3.0及問題解答

    Ubuntu 16.04上安裝 Swift 3.0及問題解答

    本文給大家分享的是在Ubuntu系統(tǒng)中安裝 Swift 3.0的方法和步驟,以及安裝過程中有可能遇到的問題的解答,這里推薦給小伙伴們,希望大家能夠喜歡
    2016-07-07
  • Swift類和對(duì)象的底層探索分析

    Swift類和對(duì)象的底層探索分析

    這篇文章主要為大家介紹了Swift類和對(duì)象的底層探索分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-09-09
  • Swift開發(fā)應(yīng)用中如何更方便地使用顏色詳解

    Swift開發(fā)應(yīng)用中如何更方便地使用顏色詳解

    這篇文章主要給大家介紹了關(guān)于Swift開發(fā)應(yīng)用中如何更方便地使用顏色的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。
    2018-03-03
  • Swift繪制漸變色的方法

    Swift繪制漸變色的方法

    這篇文章主要為大家詳細(xì)介紹了Swift繪制漸變色的方法,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2021-08-08
  • Swift4使用GCD實(shí)現(xiàn)計(jì)時(shí)器

    Swift4使用GCD實(shí)現(xiàn)計(jì)時(shí)器

    這篇文章主要為大家詳細(xì)介紹了Swift4使用GCD實(shí)現(xiàn)計(jì)時(shí)器,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2020-03-03
  • Swift 4.2使用self做為變量名淺析

    Swift 4.2使用self做為變量名淺析

    這篇文章主要給大家介紹了關(guān)于Swift 4.2使用self做為變量名的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2018-09-09

最新評(píng)論

远安县| 济南市| 博兴县| 同江市| 新疆| 略阳县| 柳州市| 平凉市| 灌云县| 当雄县| 房产| 诏安县| 河池市| 石泉县| 都匀市| 油尖旺区| 六安市| 大悟县| 江西省| 福泉市| 漳平市| 芜湖县| 闻喜县| 怀来县| 册亨县| 香港 | 镇平县| 当雄县| 龙江县| 广宗县| 安仁县| 延吉市| 沁水县| 北碚区| 明溪县| 延津县| 财经| 新津县| 镇赉县| 龙岩市| 进贤县|