返回顶部
首页 > 资讯 > 后端开发 > 其他教程 >EvenLoop模型在iOS的RunLoop应用示例
  • 378
分享到

EvenLoop模型在iOS的RunLoop应用示例

2024-04-02 19:04:59 378人浏览 薄情痞子
摘要

目录引言线程保活停止Runloop设置超时时间直接停止延迟加载图片卡顿监测CADisplayLink(FPS)子线程Ping实时监控Crash防护Matrix框架总结引言 Runlo

引言

Runloop在iOS中是一个很重要的组成部分,对于任何单线程的UI模型都必须使用EvenLoop才可以连续处理不同的事件,而RunLoop就是EvenLoop模型在ioS中的实现。在前面的几篇文章中,我已经介绍了Runloop的底层原理等,这篇文章主要是从实际开发的角度,探讨一下实际上在哪些场景下,我们可以去使用RunLoop。

线程保活

在实际开发中,我们通常会遇到常驻线程的创建,比如说发送心跳包,这就可以在一个常驻线程来发送心跳包,而不干扰主线程的行为,再比如音频处理,这也可以在一个常驻线程中来处理。以前在Objective-C中使用的AFNetworking 1.0就使用了RunLoop来进行线程的保活。

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中至少需要一个port/timer/observer,否则RunLoop只会执行一次就退出了。

停止Runloop

离开RunLoop一共有两种方法:其一是给RunLoop配置一个超时的时间,其二是主动通知RunLoop离开。Apple在文档中是推荐第一种方式的,如果能直接定量的管理,这种方式当然是最好的。

设置超时时间

然而实际中我们无法准确的去设置超时的时刻,比如在线程保活的例子中,我们需要保证线程的RunLoop一直保持运行中,所以结束的时间是一个变量,而不是常量,要达到这个目标我们可以结合一下RunLoop提供的api,在开始的时候,设置RunLoop超时时间为无限,但是在结束时,设置RunLoop超时时间为当前,这样变相通过控制timeout的时间停止了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() 但是这个方法只会停止当前这次循环的RunLoop,并不会完全停止RunLoop。那么有没有其它的策略呢?我们知道RunLoop的Mode中必须要至少有一个port/timer/observer才会工作,否则就会退出,而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!)**

所以很自然的联想到如果移除source/timer/observer, 那么这个方案可不可以停止RunLoop呢?

答案是否定的,这一点在Apple的官方文档中有比较详细的描述:

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.

简而言之,就是你无法保证你移除的就是全部的source/timer/observer,因为系统可能会添加一些必要的source来处理事件,而这些source你是无法确保移除的。

延迟加载图片

这是一个很常见的使用方式,因为我们在滑动scrollView/tableView/collectionView的过程,总会给cell设置图片,但是直接给cell的imageView设置图片的过程中,会涉及到图片的解码操作,这个就会占用CPU的计算资源,可能导致主线程发生卡顿,所以这里可以将这个操作,不放在trackingMode,而是放在defaultMode中,通过一种取巧的方式来解决可能的性能问题。

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

卡顿监测

目前来说,一共有三种卡顿监测的方案,然而基本上每一种卡顿监测的方案都和RunLoop是有关联的。

CADisplayLink(FPS)

YYFPSLabel 采用的就是这个方案,FPS(Frames Per Second)代表每秒渲染的帧数,一般来说,如果App的FPS保持50~60之间,用户的体验就是比较流畅的,但是Apple自从iPhone支持120HZ的高刷之后,它发明了一种ProMotion的动态屏幕刷新率的技术,这种方式基本就不能使用了,但是这里依旧提供已作参考。

这里值得注意的技术细节是使用了NSObject来做方法的转发,在OC中可以使用NSProxy来做消息的转发,效率更高。

// 抽象的超类,用来充当其它对象的一个替身
// Timer/CADisplayLink可以使用NSProxy做消息转发,可以避免循环引用
// swift中我们是没发使用NSInvocation的,所以我们直接使用NSobject来做消息转发
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、设置刷新的时间: 这里是设置为1秒(即每秒刷新)
        guard timeDuration >= 1.0 else { return }
        // 2、计算当前的FPS
        let fps = Double(count)/timeDuration
        count = 0
        lastTime = link.timestamp
        // 3、开始设置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

这种方法是创建了一个子线程,通过GCD给主线程添加异步任务:修改是否超时的参数,然后让子线程休眠一段时间,如果休眠的时间结束之后,超时参数未修改,那说明给主线程的任务并没有执行,那么这就说明主线程的上一个任务还没有做完,那就说明卡顿了,这种方式其实和RunLoop没有太多的关联,它不依赖RunLoop的状态。在ANREye中是采用子线程Ping的方式来监测卡顿的。

同时为了让这些操作是同步的,这里使用了信号量。

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之后,主线程依然没有执行派发的任务,这里就认为它是处于卡顿的
                if timeout == true {
                    //TODO: 这里需要取出崩溃方法栈中的符号来判断为什么出现了卡顿
                    // 可以使用微软的框架:PLCrashReporter
                }
                self.semphore.wait()
            }
        }
    }
}

这个方法在正常情况下会每隔一段时间让主线程执行GCD派发的任务,会造成部分资源的浪费,而且它是一种主动的去Ping主线程,并不能很及时的发现卡顿问题,所以这种方法会有一些缺点。

实时监控

而我们知道,主线程中任务都是通过RunLoop来管理执行的,所以我们可以通过监听RunLoop的状态来知道是否会出现卡顿的情况,一般来说,我们会监测两种状态:第一种是kCFRunLoopAfterWaiting 的状态,第二种是kCFRunLoopBeforeSource的状态。为什么是两种状态呢?

首先看第一种状态kCFRunLoopAfterWaiting ,它会在RunLoop被唤醒之后回调这种状态,然后根据被唤醒的端口来处理不同的任务,如果处理任务的过程中耗时过长,那么下一次检查的时候,它依然是这个状态,这个时候就可以说明它卡在了这个状态了,然后可以通过一些策略来提取出方法栈,来判断卡顿的代码。同理,第二种状态也是一样的,说明一直处于kCFRunLoopBeforeSource 状态,而没有进入下一状态(即休眠),也发生了卡顿。

class RunLoopMonitor {
    private init() {}
    static let shared: RunLoopMonitor = RunLoopMonitor.init()
    var timeoutCount = 0
    var runloopObserver: CFRunLoopObserver?
    var runLoopActivity: CFRunLoopActivity?
    var dispatchSemaphore: DispatchSemaphore?
    // 原理:进入睡眠前方法的执行时间过长导致无法进入睡眠,或者线程唤醒之后,一直没进入下一步
    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)
        // 初始化的信号量为0
        dispatchSemaphore = DispatchSemaphore.init(value: 0)
        DispatchQueue.global().async {
            while true {
                // 方案一:可以通过设置单次超时时间来判断 比如250毫秒
								// 方案二:可以通过设置连续多次超时就是卡顿 戴铭在GCDFetchFeed中认为连续三次超时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("------------卡顿时方法栈:\n \(report)\n")
                            } catch _ {
                                NSLog("解析crash data错误")
                            }
                        }
                    }
                }
            }
        }
    }
    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防护

Crash防护是一个很有意思的点,处于应用层的APP,在执行了某些不被操作系统允许的操作之后会触发操作系统抛出异常信号,但是因为没有处理这些异常从而被系操作系统杀掉的线程,比如常见的闪退。这里不对Crash做详细的描述,我会在下一个模块来描述iOS中的异常。要明确的是,有些场景下,是希望可以捕获到系统抛出的异常,然后将App从错误中恢复,重新启动,而不是被杀死。而对应在代码中,我们需要去手动的重启主线程,已达到继续运行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) 因为无法确定RunLoop到底是怎样启动的,所以采用了这种方式来启动RunLoop的每一个Mode,也算是一种替代方案了。因为CFRunLoopRunInMode 在运行的时候本身就是一个循环并不会退出,所以while循环不会一直执行,只是在mode退出之后,while循环遍历需要执行的mode,直到继续在一个mode中常驻。

这里只是重启RunLoop,其实在Crash防护里最重要的还是要监测到何时发送崩溃,捕获系统的exception信息,以及singal信息等等,捕获到之后再对当前线程的方法栈进行分析,定位为crash的成因。

Matrix框架

接下来我们具体看一下RunLoop在Matrix框架中的运用。Matrix是腾讯开源的一款用于性能监测的框架,在这个框架中有一款插件**WCFPSMonitorPlugin:**这是一款FPS监控工具,当用户滑动界面时,记录主线程的调用栈。它的源码中和我们上述提到的通过CADisplayLink来来监测卡顿的方案的原理是一样的:

- (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 {
    // 当前时间: 单位为秒
    double nowTime = CFAbsoluteTimeGetCurrent();
    // 将单位转化为毫秒
    double diff = (nowTime - m_lastTime) * 1000;
		// 1、如果时间间隔超过最大的帧间隔:那么此次屏幕刷新方法超时
    if (diff > self.pluginConfig.maxFrameInterval) {
        m_currRecorder.dumpTimeTotal += diff;
        m_dropTime += self.pluginConfig.maxFrameInterval * pow(diff / self.pluginConfig.maxFrameInterval, self.pluginConfig.powFactor);
        // 总超时时间超过阈值:展示超时信息
        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、如果时间间隔没有最大的帧间隔:那么此次屏幕刷新方法不超时
    } else {
        // 总超时时间超过阈值:展示超时信息
        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);
						....
				// 总超时时间不超过阈值:将时间归0 重新计数
        } else {
            m_currRecorder.dumpTimeTotal = 0;
            m_currRecorder.dumpTimeBegin = nowTime + 0.0001;
        }
    }
    m_lastTime = nowTime;
}

它通过次数以及两次之间允许的时间间隔作为阈值,超过阈值就记录,没超过阈值就归0重新计数。当然这个框架也不仅仅是作为一个简单的卡顿监测来使用的,还有很多性能监测的功能以供平时开发的时候来使用:包括对崩溃时方法栈的分析等等。

总结

本篇文章我从线程保活开始介绍了RunLoop在实际开发中的使用,然后主要是介绍了卡顿监测和Crash防护中的高阶使用,当然,RunLoop的运用远不止这些,如果有更多更好的使用,希望大家可以留言交流。

以上就是EvenLoop模型在iOS的RunLoop应用示例的详细内容,更多关于ios EvenLoop模型RunLoop的资料请关注编程网其它相关文章!

--结束END--

本文标题: EvenLoop模型在iOS的RunLoop应用示例

本文链接: https://lsjlt.com/news/164744.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • EvenLoop模型在iOS的RunLoop应用示例
    目录引言线程保活停止Runloop设置超时时间直接停止延迟加载图片卡顿监测CADisplayLink(FPS)子线程Ping实时监控Crash防护Matrix框架总结引言 Runlo...
    99+
    2024-04-02
  • Flex应用程序模型的示例分析
    这篇文章主要介绍Flex应用程序模型的示例分析,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!创建一个Flex应用程序Flex应用程序模型Flex创建一个应用程序时,你使用组件(容器/containers和控件/con...
    99+
    2023-06-17
  • 设计模式中的原型模式在Python程序中的应用示例
    原型模式: 原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。 原型模式本质就是克隆对象,所以在对象初始化操作比较复杂的情况下,很实用,能大大降低耗时,提高性能,因为“不用重新初始化对象,而是动...
    99+
    2022-06-04
    模式 原型 示例
  • IOS在SwiftUI中显示模态视图的实例代码
    简介 这里教大家如何弹出一个简单的模态视图。分别有两个页面,ContentView和GCPresentedView,以下对应简称为A和B。我们要做的是在A视图中点击按钮跳转到B视图,...
    99+
    2022-05-30
    ios swiftUI 模态视图
  • iOS 11开发中iOS11应用视图的示例分析
    这篇文章给大家分享的是有关iOS 11开发中iOS11应用视图的示例分析的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。在iPhone或者iPad中,用户看到的和摸到的都是视图。视图是用户界面的重要组成元素。本节将...
    99+
    2023-06-04
  • css盒模型的示例分析
    小编给大家分享一下css盒模型的示例分析,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧! 1.各种盒模型 inline-blo...
    99+
    2024-04-02
  • CSS中盒模型用法的示例分析
    CSS中盒模型用法的示例分析,相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。视觉类型的媒体根据CSS的视觉格式化模型(Visual format...
    99+
    2024-04-02
  • Flutter 容器盒子模型的使用示例
    目录样例代码在讲 Flutter 的盒子模型前,先看看HTML 中的盒子模型。如下图所示,一个页面元素包括了与父级容器的外边距(margin),自身内容与边框的内边距(padding...
    99+
    2024-04-02
  • iOS实现模拟定位功能的示例代码
    前言 App中越来越多的功能依赖用户实际的位置,例如基于用户位置提供推荐数据、基于定位判断某些功能是否可用,但是在开发调试中XCode却没有提供自定义的模拟定位的功能,所以本文主要...
    99+
    2022-05-16
    iOS 模拟定位
  • iOS 11开发中iOS11模拟器的示例分析
    这篇文章将为大家详细讲解有关iOS 11开发中iOS11模拟器的示例分析,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。iOS11模拟器介绍在图1.6或者1.7中所看到的类似于手机的模型就是iOS模拟器。i...
    99+
    2023-06-04
  • Python Logging 模块在大型应用程序中的应用
    Logging 模块概览 Python Logging 模块是一个功能强大的库,用于处理应用程序中的日志消息。它提供了创建日志记录器、配置日志级别和过滤器的能力。通过使用 Logging 模块,开发人员可以轻松地捕获、组织和分析应用程序...
    99+
    2024-02-20
    Python Logging 大型应用程序 日志记录 错误处理 调试
  • 空气污染扩散模型-高斯烟羽烟团模型原理及GIS应用示例
    1 在线模拟示例 在线示例https://simu.cool/vue/ 2 高斯模型 2.1 高斯烟团模型    突发性泄漏事故中,经常发生污染源在短时间内突然释放大量的有害气体,此时对地面污染浓度的求解适合采用高斯烟团模型。烟团模型假定污...
    99+
    2023-09-23
    java
  • iOS开发中UITabBarController的使用示例
    首先我们看一下它的view层级图: - (BOOL)application:(UIApplication *)application didFinishLaunchingWith...
    99+
    2022-05-22
    iOS
  • RBAC权限模型的示例分析
    小编给大家分享一下RBAC权限模型的示例分析,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!权限系统与RBAC模型概述RBAC(R...
    99+
    2024-04-02
  • db2进程模型的示例分析
    这期内容当中小编将会给大家带来有关db2进程模型的示例分析,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。DB2 进程技术模型 所有 DB2 数据库服务器使用的进程技术...
    99+
    2024-04-02
  • HTML中盒模型的示例分析
    小编给大家分享一下HTML中盒模型的示例分析,希望大家阅读完这篇文章之后都有所收获,下面让我们一起去探讨吧!盒模型规定了元素框处理元素内容width与height值、内边距padding、边框border 和 外边距margin 的数值大小...
    99+
    2023-06-08
  • Java内存模型的示例分析
    这篇文章主要为大家展示了“Java内存模型的示例分析”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下“Java内存模型的示例分析”这篇文章吧。1. 为什么要有内存模型?要想回答这个问题,我们需要先弄...
    99+
    2023-06-29
  • Java原型模式的示例分析
    这篇文章主要介绍了Java原型模式的示例分析,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。1.思考问题现在有一只羊 tom,姓名为: tom,年龄为:1,颜色为:白色,请编写...
    99+
    2023-06-29
  • Tomcat中NIO模型的示例分析
    这篇文章给大家分享的是有关Tomcat中NIO模型的示例分析的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。一、I/O复用模型解读Tomcat的NIO是基于I/O复用来实现的。对这点一定要清楚,不然我们的讨论就不在...
    99+
    2023-06-02
  • MVVM模型在Vue中如何应用
    本篇内容主要讲解“MVVM模型在Vue中如何应用”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“MVVM模型在Vue中如何应用”吧!理解MVVM模型我们知道每一个 Vue 应用都是从创建一个新的实...
    99+
    2023-07-04
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作