如何构造 Swift 框架(四)

本贴最后更新于 3084 天前,其中的信息可能已经水流花落

上期提到了使用Moya作为网络基础模块,但是涉及到了一个sampleData的问题,我们也是即时的提交了一个issue来质问这样的默认Response data为什么类型竟然是Optional的。Moya的开发者举例:可以将上一次获取到的数据在需要的时候(网络请求失败)传入这里,所以进而给出建议:将var sampleData改为var cachePolicy进行缓存控制即可,缓存过期的时间由Server端使用Cache-control或Expires决定,目前有的回复是,作者觉得这个建议很棒,说不定有机会为Moya加入缓存机制。接下来继续我们的开发计划:

推送服务

应当明确的是,每家公司用的推送第三方都是不同的(大部分是阿里云、极光、个推),所以继承第三方SDK这个事情不应该出现在框架中。框架仅仅负责申请推送能力即可。测试:在测试之前,Info.plist中所需要申请权限的Key需要自己手动配置。Xcode 8 后打开推送需要在程序中打开选项:

这样,如果是单单写权限的话,直接用之前我们引入的PermissionScope就可以搞定了,Push的class可以写为open的,因为每个项目对Push的需求不同,所以在Push中我们顺便截获一下信息然后提供给用户,也很简单。所以获得推送权限的需求我们放到Permission.swift中。由于屏幕限制,所以Permission也最多允许大家同时打开3个权限。改写之前的Permission.swift

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
import Foundation
import PermissionScope
 
public enum INSPermissionType {
case notification(Set<UIUserNotificationCategory>?, String)
case locationAlways(String)
case locationWhenInUse(String)
case contact(String)
case event(String)
case microphone(String)
case camera(String)
case photos(String)
case reminders(String)
case bluetooth(String)
case motion(String)
}
 
open class Permission {
open static let `default` = Permission()
static let pscope: PermissionScope = {
let permissionScope = PermissionScope()
// Default customs
permissionScope.headerLabel.text = "嗨,你好!"
permissionScope.bodyLabel.text = "在使用我们的应用之前\n我们需要你做一些事情:"
permissionScope.closeButtonTextColor = UIColor.clear
permissionScope.permissionButtonΒorderWidth = 0.5
permissionScope.permissionButtonCornerRadius = 2
/// 如果你希望更改权限开启按钮的英文,就需要自己配置本地化文件
/// 参考这里 https://github.com/nickoneill/PermissionScope/pull/12#issuecomment-96428580
return permissionScope
}()
 
open class func requestPermission(_ permissionTypes: [INSPermissionType], _ authChange: authClosureType? = nil, cancelled: cancelClosureType? = nil) {
for item in permissionTypes {
switch item {
case .notification(let categories, let message):
pscope.addPermission(NotificationsPermission(notificationCategories: categories), message: message)
continue
case .locationAlways(let message):
pscope.addPermission(LocationWhileInUsePermission(), message: message)
continue
case .locationWhenInUse(let message):
pscope.addPermission(LocationWhileInUsePermission(), message: message)
continue
case .contact(let message):
pscope.addPermission(ContactsPermission(), message: message)
continue
case .event(let message):
pscope.addPermission(EventsPermission(), message: message)
continue
case .microphone(let message):
pscope.addPermission(MicrophonePermission(), message: message)
continue
case .camera(let message):
pscope.addPermission(CameraPermission(), message: message)
continue
case .photos(let message):
pscope.addPermission(PhotosPermission(), message: message)
continue
case .reminders(let message):
pscope.addPermission(RemindersPermission(), message: message)
continue
case .bluetooth(let message):
pscope.addPermission(BluetoothPermission(), message: message)
continue
case .motion(let message):
pscope.addPermission(MotionPermission(), message: message)
continue
default:
continue
}
}
 
pscope.show(authChange, cancelled: cancelled)
}
}

然后测试效果(别忘记在Info.plist中添加相关的请求权限的Key-Desc):

1
2
3
4
5
6
7
let permissionTypes = [
INSPermissionType.notification(nil, "打开推送服务"),
INSPermissionType.camera("打开相机服务"),
INSPermissionType.photos("希望使用照片")
]
 
Permission.requestPermission(permissionTypes)

原本计划是要写Push.swift进行截获数据的,但是总感觉这样做貌似不太合理。所以索性我们止只统计一下用户收到推送好了,在Push.swift中仅提供一个方法入口,把推送的内容传进来供我们内部处理。不要干涉AppDelegate处理推送了,而且在iOS10之后要做版本兼容,使用UNUserNotificationCenterDelegate来处理推送,而且大多数第三方SDK都会有自己的处理方式。

所以在我们的Push.swift中,我们先预留一些代码:

等会儿先看个东西:

好、继续写代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
final public class Push {
public static let `default` = Push()
 
public func DeviceToken(_ deviceToken: Data) {
 
}
 
public func ReceivedPushMessage (_ userInfo: [AnyHashable : Any]) {
 
}
 
private init() {
 
}
}

简单的预留一些方法入口即可,不急着写,接着往下写日志上报(直接改造之前的Logger类):

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
final public class INSLogger {
/// 默认为输出全部日志
public static let `default` = INSLogger()
 
/// 日志级别
public var level: LogLevel = .all
/// 是否上报崩溃
public var crashCollect: Bool = true
 
/// 日志输出
///
/// - parameter lev: 日志级别
/// - parameter content: 日志内容
public func printLog(_ lev: LogLevel, _ details: String, _ items: Any) {
guard level == .all || level == lev, ModeSwitcher.currentMode == .develope else {
return
}
 
print(lev.rawValue, details, "\n", items)
}
private var exception: NSException? = nil
public func setUncaughtException() {
NSSetUncaughtExceptionHandler {
let exception = $0
let name = exception.name
let reason = exception.reason ?? "Without system crash version."
let callStack = exception.callStackSymbols
let crashLog = "name:\(name)\nreason:\(reason)\ncallStack:\(callStack.joined(separator: "\n"))"
 
// TODO: 上报
}
}
}

获取到崩溃的信息后,我们在这里加一个TODO标签。 这里需要注意的是:框架外部如果也需要做日志捕获,那么需要先使用NSGetUncaughtExceptionHandler()获取当前的捕获器,在自己的捕获成功之后也让别人的捕获成功。啊好累啊,这还不是完整的奔溃捕获,于是我们接着写代码(写代码到时无妨,主要是这里有一坑爹的事情,需要自己去查看解决,说明:无法把方法传入这些捕获方法也附上气前一个链接中的代码):

慵懒的完善了signal后(上边提到的不能使用C方法的问题自己去解决把,这里仅仅是展示,所以不写那么详细了):

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
/// 设置异常捕获
public func setUncaughtException() {
 
NSSetUncaughtExceptionHandler {
let exception = $0
let name = exception.name
let reason = exception.reason ?? "Without system crash version."
let callStack = exception.callStackSymbols
let crashLog = "name:\(name)\nreason:\(reason)\ncallStack:\(callStack.joined(separator: "\n"))"
 
exception.raise()
// TODO: 上报
}
 
signal(SIGILL) {
let crashLog = "SignalRaisedException(\($0)): Illegal instruction (not reset when caught)"
// TODO: 上报
}
signal(SIGABRT) {
let crashLog = "SignalRaisedException(\($0)): Abort, abort()"
// TODO: 上报
}
signal(SIGFPE) {
let crashLog = "SignalRaisedException(\($0)): Floating point exception"
// TODO: 上报
}
signal(SIGBUS) {
let crashLog = "SignalRaisedException(\($0)): Bus Error"
// TODO: 上报
}
signal(SIGSEGV) {
let crashLog = "SignalRaisedException(\($0)): segmentation violation"
// TODO: 上报
}
signal(SIGSYS) {
let crashLog = "SignalRaisedException(\($0)): Bad argument to system call"
// TODO: 上报
}
signal(SIGPIPE) {
let crashLog = "SignalRaisedException(\($0)): Write on a pipe with no one to read it"
// TODO: 上报
}
}
 
public func unSetUncaughtException() {
NSSetUncaughtExceptionHandler(nil)
signal(SIGILL, SIG_DFL);
signal(SIGABRT, SIG_DFL);
signal(SIGFPE, SIG_DFL);
signal(SIGBUS, SIG_DFL);
signal(SIGSEGV, SIG_DFL);
signal(SIGSYS, SIG_DFL);
signal(SIGPIPE, SIG_DFL);
}

这里只捕获了一部分signal,点进去自己看了解下,我之前也写过一篇关于日志捕获的文章,可以去找找。继续往下写:信息收集,新建swift文件Analytics.swift,这里我只给出一部分思路(完整的Analytics又是一个独立的框架,建议参考的是开源的ZhugeIO):

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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import Foundation
import LKDBHelper
 
let AnalyticsManagerFlushedFlagKey = "AnalyticsManagerFlushedFlagKey"
 
class AnalyticsItem: NSObject {
/// 事件名称
var eventName: String
/// 事件数据
var parameters: [String: Any]?
 
func toAnalytice() ->[String: String] {
return [eventName ?? "": "\(parameters ?? ["": ""])"]
}
 
init(_ eventName: String, _ parameters: [String: Any]? = nil) {
self.eventName = eventName
self.parameters = parameters
}
 
override static func getTableName() -> String {
return "AnalyticsItem"
}
}
 
final public class Analytics {
public static let `default` = Analytics()
public typealias FlushHandler = (_ info: [String], _ analyticsData: [[String: String]])->()
 
/// 设备唯一标识,默认是UUID
public var deviceIdentifier: String
/// 设备用户标识,以设备标识为准
public var userIdentifier: String
/// 上报间隔,会调用上报的方法,外部控制网络请求
public var flushInterval: Int = 10
/// 上报的回调方法
public var flushHandler: FlushHandler?
 
/// 存储准备上报的数组
private var analyticsItems: [AnalyticsItem] = []
/// 是否已经上报,通过检查本地值来确定
private var flushed: Bool
private var timer: Timer? = nil
 
/// 追踪事件
public func track(_ eventName: String, _ parameters: [String: Any]? = nil) {
if analyticsItems.count == 0 {
startTimer()
}
analyticsItems.append(AnalyticsItem(eventName, parameters))
}
 
/// 主动上报到服务器
public func flush() {
guard analyticsItems.count > 0, let handler = flushHandler else {
return
}
handler([deviceIdentifier, userIdentifier], analyticsItems.map { return $0.toAnalytice() })
stopTimer()
flushed = true
}
 
private func startTimer() {
stopTimer()
timer = Timer.init(timeInterval: TimeInterval(flushInterval), target: self, selector: "flush", userInfo: nil, repeats: true)
RunLoop.current.add(timer!, forMode: .commonModes)
}
 
private func stopTimer() {
timer?.invalidate()
timer = nil
analyticsItems.removeAll()
}
 
private func getLocalAnalyticsItem() {
AnalyticsItem.search(withWhere: nil).forEach {
[unowned self] in
self.analyticsItems.append($0 as! AnalyticsItem)
}
let dbHelper = AnalyticsItem.getUsingLKDBHelper()!
dbHelper.dropTable(with: AnalyticsItem.self)
}
 
public func setNeedsRestoreItems() {
analyticsItems.forEach { $0.saveToDB() }
analyticsItems.removeAll()
UserDefaults.standard.set(false, forKey: AnalyticsManagerFlushedFlagKey)
UserDefaults.standard.synchronize()
}
 
public func restoreItems() {
if flushed == false {
getLocalAnalyticsItem()
}
}
 
private func UIApplicationDidEnterBackground() {
setNeedsRestoreItems()
}
 
private func UIApplicationDidBecomeActive() {
self.flushed = UserDefaults.standard.bool(forKey: AnalyticsManagerFlushedFlagKey)
restoreItems()
}
 
private func addListener() {
NotificationCenter.default.addObserver(self, selector: "UIApplicationDidEnterBackground", name: .UIApplicationDidEnterBackground, object: nil)
NotificationCenter.default.addObserver(self, selector: "UIApplicationDidBecomeActive", name: .UIApplicationDidBecomeActive, object: nil)
}
 
private func removeListener() {
stopTimer()
NotificationCenter.default.removeObserver(self)
}
 
private init() {
self.deviceIdentifier = UUID().uuidString
self.userIdentifier = "iOS Device"
self.flushed = UserDefaults.standard.bool(forKey: AnalyticsManagerFlushedFlagKey)
restoreItems()
addListener()
}
 
deinit {
removeListener()
}
}
 
public let AnalyticsManager = Analytics.default

完成之前的奔溃时日志上报

1
2
3
4
// TODO: 上报
AnalyticsManager.track("CRASH", ["info": crashLog])
// 程序奔溃需要调用标记未上传
AnalyticsManager.setNeedsRestoreItems()

写到这里,框架其实只有30%,只有结合业务才能做出与业务相匹配的框架,接下来就是Cache,我只给出代码框架:

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
//存储引擎
public enum IDPStorageType {
case disk
case sql
}
 
//缓存策略
public enum IDPCacheStoragePolicy {
case memory
case disk
case memoryAndDisk
}
 
open class INSCache {
open static let `default` = INSCache()
 
open var _nameSpace: String = "INSCache"
open var _cacheStoragePolicy: IDPCacheStoragePolicy = .memoryAndDisk
open var _memoryCapacity: Float = 0
open var _memoryTotalCost: Float = 0
open var _diskExpiredTime: Int = 0
 
open func existCacheForKey(_ key: String) ->Bool {
return false
}
 
open func clearMemory() {
 
}
 
open func existCacheForKeyInMemory(_ key: String) ->Bool {
return false
}
 
open func existCacheForKeyOnDisk(_ key: String) ->Bool {
return false
}
 
open func setObject(_ data: AnyObject, for key: String) {
 
}
 
open func getObject(for key: String) ->AnyObject? {
return nil
}
 
open func objectForKeyOnlyInMemory(_ key: String) ->AnyObject? {
return nil
}
 
open func asyncObject(forKey key: String, _ handler: (AnyObject)->()) {
 
}
 
open func removeObjcet(for key: String) {
 
}
 
open func removeObjcetForKeyOnlyInMemory(_ key: String) {
 
}
 
open func removeAll () {
 
}
 
open func removeAllInMemory() {
 
}
 
open func removeAllInDisk() {
 
}
 
open class func removeNameSpace(_ spaceName: String) {
 
}
}

包括模型的基类:

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
open class Model: NSObject {
open var _ModelIdentifier: String?
 
open var _ModelUpdatedAt: Date?
open var _ModelCreatedAt: Date?
open var _ModelExpiredAt: Date?
open var _ModelNeedsCache: Bool?
 
open var _CurrentPage: Int = 0
open var _PageSize: Int = 10
open var _TotalCount: Int = 0
open var _StartAt: Int = 0
 
open func load() { }
open func refresh() { }
open func cancel() { }
 
open func goNextPage() { }
open func goPrevPage() { }
 
open func hasPrev() ->Bool{ return false }
open func hasNext() ->Bool{ return false }
 
public override init() {
 
}
}

框架到这里就不说了,接下来有时间就会实际的在使用中一步步的优化框架,让框架适应业务。最近有点忙,开了算法课程,所以框架上边大部分东西都是懒得写,但是使用到的第三方库都建议大家去阅读源码(除ASDK以外)。希望会有所提升。代码地址。仅供作为Swift的语言熟悉,不作为框架教学。

  • Swift

    Swift 是苹果于 2014 年 WWDC(苹果开发者大会)发布的开发语言,可与 Objective-C 共同运行于 Mac OS 和 iOS 平台,用于搭建基于苹果平台的应用程序。

    36 引用 • 37 回帖 • 545 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...

推荐标签 标签

  • Android

    Android 是一种以 Linux 为基础的开放源码操作系统,主要使用于便携设备。2005 年由 Google 收购注资,并拉拢多家制造商组成开放手机联盟开发改良,逐渐扩展到到平板电脑及其他领域上。

    335 引用 • 324 回帖
  • Laravel

    Laravel 是一套简洁、优雅的 PHP Web 开发框架。它采用 MVC 设计,是一款崇尚开发效率的全栈框架。

    20 引用 • 23 回帖 • 737 关注
  • RYMCU

    RYMCU 致力于打造一个即严谨又活泼、专业又不失有趣,为数百万人服务的开源嵌入式知识学习交流平台。

    4 引用 • 6 回帖 • 53 关注
  • NGINX

    NGINX 是一个高性能的 HTTP 和反向代理服务器,也是一个 IMAP/POP3/SMTP 代理服务器。 NGINX 是由 Igor Sysoev 为俄罗斯访问量第二的 Rambler.ru 站点开发的,第一个公开版本 0.1.0 发布于 2004 年 10 月 4 日。

    315 引用 • 547 回帖
  • Anytype
    3 引用 • 31 回帖 • 12 关注
  • Windows

    Microsoft Windows 是美国微软公司研发的一套操作系统,它问世于 1985 年,起初仅仅是 Microsoft-DOS 模拟环境,后续的系统版本由于微软不断的更新升级,不但易用,也慢慢的成为家家户户人们最喜爱的操作系统。

    226 引用 • 476 回帖 • 2 关注
  • NetBeans

    NetBeans 是一个始于 1997 年的 Xelfi 计划,本身是捷克布拉格查理大学的数学及物理学院的学生计划。此计划延伸而成立了一家公司进而发展这个商用版本的 NetBeans IDE,直到 1999 年 Sun 买下此公司。Sun 于次年(2000 年)六月将 NetBeans IDE 开源,直到现在 NetBeans 的社群依然持续增长。

    78 引用 • 102 回帖 • 702 关注
  • 30Seconds

    📙 前端知识精选集,包含 HTML、CSS、JavaScript、React、Node、安全等方面,每天仅需 30 秒。

    • 精选常见面试题,帮助您准备下一次面试
    • 精选常见交互,帮助您拥有简洁酷炫的站点
    • 精选有用的 React 片段,帮助你获取最佳实践
    • 精选常见代码集,帮助您提高打码效率
    • 整理前端界的最新资讯,邀您一同探索新世界
    488 引用 • 384 回帖 • 6 关注
  • 职场

    找到自己的位置,萌新烦恼少。

    127 引用 • 1708 回帖
  • flomo

    flomo 是新一代 「卡片笔记」 ,专注在碎片化时代,促进你的记录,帮你积累更多知识资产。

    6 引用 • 140 回帖
  • 电影

    这是一个不能说的秘密。

    122 引用 • 608 回帖
  • 智能合约

    智能合约(Smart contract)是一种旨在以信息化方式传播、验证或执行合同的计算机协议。智能合约允许在没有第三方的情况下进行可信交易,这些交易可追踪且不可逆转。智能合约概念于 1994 年由 Nick Szabo 首次提出。

    1 引用 • 11 回帖 • 1 关注
  • LeetCode

    LeetCode(力扣)是一个全球极客挚爱的高质量技术成长平台,想要学习和提升专业能力从这里开始,充足技术干货等你来啃,轻松拿下 Dream Offer!

    209 引用 • 72 回帖
  • Q&A

    提问之前请先看《提问的智慧》,好的问题比好的答案更有价值。

    9409 引用 • 42877 回帖 • 110 关注
  • ZooKeeper

    ZooKeeper 是一个分布式的,开放源码的分布式应用程序协调服务,是 Google 的 Chubby 一个开源的实现,是 Hadoop 和 HBase 的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。

    59 引用 • 29 回帖 • 2 关注
  • Flutter

    Flutter 是谷歌的移动 UI 框架,可以快速在 iOS 和 Android 上构建高质量的原生用户界面。 Flutter 可以与现有的代码一起工作,它正在被越来越多的开发者和组织使用,并且 Flutter 是完全免费、开源的。

    39 引用 • 92 回帖 • 1 关注
  • B3log

    B3log 是一个开源组织,名字来源于“Bulletin Board Blog”缩写,目标是将独立博客与论坛结合,形成一种新的网络社区体验,详细请看 B3log 构思。目前 B3log 已经开源了多款产品:SymSoloVditor思源笔记

    1063 引用 • 3455 回帖 • 165 关注
  • Quicker

    Quicker 您的指尖工具箱!操作更少,收获更多!

    36 引用 • 155 回帖 • 2 关注
  • Solidity

    Solidity 是一种智能合约高级语言,运行在 [以太坊] 虚拟机(EVM)之上。它的语法接近于 JavaScript,是一种面向对象的语言。

    3 引用 • 18 回帖 • 431 关注
  • MongoDB

    MongoDB(来自于英文单词“Humongous”,中文含义为“庞大”)是一个基于分布式文件存储的数据库,由 C++ 语言编写。旨在为应用提供可扩展的高性能数据存储解决方案。MongoDB 是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。它支持的数据结构非常松散,是类似 JSON 的 BSON 格式,因此可以存储比较复杂的数据类型。

    90 引用 • 59 回帖 • 6 关注
  • Vue.js

    Vue.js(读音 /vju ː/,类似于 view)是一个构建数据驱动的 Web 界面库。Vue.js 的目标是通过尽可能简单的 API 实现响应的数据绑定和组合的视图组件。

    267 引用 • 666 回帖 • 1 关注
  • 微信

    腾讯公司 2011 年 1 月 21 日推出的一款手机通讯软件。用户可以通过摇一摇、搜索号码、扫描二维码等添加好友和关注公众平台,同时可以将自己看到的精彩内容分享到微信朋友圈。

    132 引用 • 796 回帖
  • Spark

    Spark 是 UC Berkeley AMP lab 所开源的类 Hadoop MapReduce 的通用并行框架。Spark 拥有 Hadoop MapReduce 所具有的优点;但不同于 MapReduce 的是 Job 中间输出结果可以保存在内存中,从而不再需要读写 HDFS,因此 Spark 能更好地适用于数据挖掘与机器学习等需要迭代的 MapReduce 的算法。

    74 引用 • 46 回帖 • 567 关注
  • Outlook
    1 引用 • 5 回帖
  • Excel
    31 引用 • 28 回帖
  • WordPress

    WordPress 是一个使用 PHP 语言开发的博客平台,用户可以在支持 PHP 和 MySQL 数据库的服务器上架设自己的博客。也可以把 WordPress 当作一个内容管理系统(CMS)来使用。WordPress 是一个免费的开源项目,在 GNU 通用公共许可证(GPLv2)下授权发布。

    66 引用 • 114 回帖 • 200 关注
  • SMTP

    SMTP(Simple Mail Transfer Protocol)即简单邮件传输协议,它是一组用于由源地址到目的地址传送邮件的规则,由它来控制信件的中转方式。SMTP 协议属于 TCP/IP 协议簇,它帮助每台计算机在发送或中转信件时找到下一个目的地。

    4 引用 • 18 回帖 • 637 关注