阅读视图
Trae + SwiftUI 1 小时实现一个单词本 Mac App
音视频基础能力之 iOS 视频篇(六):使用Metal进行视频渲染
音视频学习笔记十六——图像处理之OpenCV基础一
题记:前文介绍GPUImage滤镜链的原理,但实际上要写出效果,还需要理解其中图片处理的过程,所以本章开始会介绍一些OpenCV基础相关。图像处理需要用到很多专业的算法,本人业余学习略知皮毛,只是庶竭驽钝叙其所得,在音视频学习Demo有一些的示例。文章或代码若有错误,也希望大佬不吝赐教。
一、OpenCV简介
OpenCV(Open Source Computer Vision Library)是一个开源的计算机视觉和机器学习库,广泛应用于人脸识别与生物识别、自动驾驶、工业检测等,核心功能包括:
- 图像处理:滤波、边缘检测、几何变换、颜色空间转换、直方图均衡化等。
- 视频分析:运动检测、光流检测等。
- 特征提取与匹配:SIFT、SURF、ORB、角点检测等。
- 目标检测与识别:Haar级联分类器(人脸检测)、HOG+SVM(行人检测)、深度学习模型(YOLO、SSD)。
- 机器学习:支持向量机(SVM)、神经网络等算法。
二、基础操作
2.1. 输入/输出
// 读取图像
cv::Mat img = cv::imread("xxx/xxx.jpg", cv::IMREAD_COLOR);
// 保存图像
cv::imwrite("xxx/xxx.jpg", img);
cv::imshow(winname, img)
创建窗口显示,移动端没有实现,iOS端转换为UIImage:
- (UIImage *)matToUIImage:(const cv::Mat&)mat {
NSData *data = [NSData dataWithBytes:mat.data length:mat.elemSize() * mat.total()];
CGColorSpaceRef colorSpace = mat.channels() == 1 ? CGColorSpaceCreateDeviceGray() : CGColorSpaceCreateDeviceRGB();
CGDataProviderRef provider = CGDataProviderCreateWithCFData((__bridge CFDataRef)data);
CGImageRef imageRef = CGImageCreate(mat.cols, mat.rows, 8, 8 * mat.channels(), mat.step[0], colorSpace, kCGImageAlphaNone|kCGBitmapByteOrderDefault, provider, NULL, false, kCGRenderingIntentDefault);
UIImage *image = [UIImage imageWithCGImage:imageRef];
CGImageRelease(imageRef);
CGDataProviderRelease(provider);
CGColorSpaceRelease(colorSpace);
return image;
}
2.2. Mat对象
Mat基本是OpenCV中基本操作单元,可以从图片中读取(channels为BGR
注意与移动端常用的RGB
区别),也可以创建空矩阵。
// 空矩阵
cv::Mat emptyMat;
// 指定尺寸和类型(行,列,数据类型)
cv::Mat mat(480, 640, CV_8UC3); // 3通道 8位无符号(BGR图像)
cv::Mat floatMat(100, 100, CV_32FC1); // 单通道浮点矩阵
// 初始化值
cv::Mat redMat(100, 100, CV_8UC3, cv::Scalar(0, 0, 255)); // 全红色图像
cv::Mat ones = cv::Mat::ones(3, 3, CV_32F); // 全1矩阵
Mat的数据实际存储在u(UMatData)
中,而data的内存管理,使用引用计数,可以使用mat.u->refcount
查看引用的计数。
-
浅拷贝:默认赋值或传参,共享数据内存
cv::Mat shallow = mat;
-
深拷贝:独立内存
cv::Mat deep = mat.clone(); // 或 mat.copyTo(deep);
2.2.1. 访问和修改像素
- 单通道(灰度):
uchar pixel = mat.at<uchar>(y, x); // 读取 (y,x) 处的值(注意行列顺序!)
mat.at<uchar>(y, x) = 255; // 修改
- 多通道(如 BGR 图像):
cv::Vec3b& pixel = mat.at<cv::Vec3b>(y, x);
pixel[0] = 255; // 蓝色通道
pixel[1] = 0; // 绿色通道
pixel[2] = 0; // 红色通道
- 使用指针高效遍历:
for (int i = 0; i < mat.rows; i++) {
uchar* row = mat.ptr<uchar>(i);
for (int j = 0; j < mat.cols; j++) {
row[j] = ...; // 修改像素
}
}
2.2.2. 图像处理操作
- 调整大小:
cv::Mat resized;
cv::resize(inputMat, resized, cv::Size(newWidth, newHeight));
- 颜色空间转换:
cv::Mat gray;
cv::cvtColor(colorMat, gray, cv::COLOR_BGR2GRAY);
- 旋转:
cv::Mat rotated;
cv::rotate(inputMat, rotated, cv::ROTATE_90_CLOCKWISE);
- 裁剪 ROI(Region of Interest):
int x = (cols - 200) / 2;
int y = (rows - 200) / 2;
cv::Rect roi_rect(x, y, 200, 200);
- 矩阵运算
cv::Mat A = ... , B = ... , C;
cv::add(A, B, C); // 矩阵加法
cv::multiply(A, B, C); // 逐元素乘法
C = A * B; // 矩阵乘法(非逐元素)
cv::transpose(A, C); // 转置
- 数据类型转换
cv::Mat floatMat;
mat.convertTo(floatMat, CV_32F, 1.0/255.0); // 转为浮点并归一化
- 单通道显示
cv::split
分离通道操作,注意是按照BGR
的顺序,所以R通道为channels[2]
。
std::vector<cv::Mat> channels;
cv::split(mat, channels);
// 创建零矩阵并合并三通道
cv::Mat zeroMat = cv::Mat::zeros(mat.size(), CV_8UC1);
std::vector<cv::Mat> mergedChannels{zeroMat, zeroMat, channels[2]};
cv::Mat des;
cv::merge(mergedChannels, des);
- Blend效果
// 调整img2尺寸与输入图像匹配
cv::resize(img2, img2, mat.size(), 0, 0, cv::INTER_LINEAR);
// 使用addWeighted进行混合
cv::addWeighted(mat, 0.6, img2, 0.4, 0.0, blended);
- 二值操作
cv::Mat gray, dst;
// 转换为灰度图像
cv::cvtColor(mat, gray, cv::COLOR_BGR2GRAY);
// 应用Otsu二值化
cv::threshold(gray, dst, 0, 255, cv::THRESH_BINARY | cv::THRESH_OTSU);
return dst;
三、形态学处理
形态学是一类基于图像形状的图像处理技术,以下图为例,看一下形态学的变化。形态学是对实际上会对各个通道进行独立操作,默认是对单通道图像(如灰度图或二值图)操作
,所以一般使用二值图看效果。
3.1. 腐蚀
腐蚀操作原理是取邻域最小值,如下图,处理像素点1
时,检查周围像素点(MORPH_RECT
),取色值最小的点(右下),所以当前的点变成黑色。换个角度,黑色像素点会把周围点都变成黑色,像黑色来腐蚀了白色。
代码如下:
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3,3));
cv::morphologyEx(mat, mat, cv::MORPH_ERODE, kernel);
效果如图,整体变小了,毛刺少了很多。
3.2. 膨胀
膨胀操作原理是取邻域最大值,就是和腐蚀相反的操作。如下图,处理像素点1
时,检查周围像素点(MORPH_RECT
),取色值最大的点(右下),所以当前的点变成白色。换个角度,白色像素点会把周围点都染白,像白色像素进行了膨胀。
代码如下:
cv::Mat dilated;
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3,3));
cv::dilate(mat, dilated, kernel);
效果如图,整体变大了,毛刺变得更粗壮了。
3.3. 开运算
上述两种运算都会原来的形状(变大或缩小),而先腐蚀后膨胀就是开运算。开运算一般用于去噪,如下图,先腐蚀会让黑色区域变大,从而中间的白色噪点消失,再膨胀白色区域恢复(原来的噪点消失)。
代码如下:
cv::Mat opened;
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(5,5));
cv::morphologyEx(mat, opened, cv::MORPH_OPEN, kernel);
效果如图,可以通过改变kernel大小调整效果(5x5效果):
3.4. 闭运算
闭运算是开运算相反的操作先膨胀再腐蚀,运用孔洞填充,如下图字母T
,由于打印或拍摄问题,有些像素点缺失。先膨胀就可以把区域连通,再腐蚀恢复成原来大小。
代码如下:
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(5,5));
cv::morphologyEx(mat, mat, cv::MORPH_CLOSE, kernel);
效果如图,可以通过改变kernel大小调整效果(5x5效果),连通了毛刺中间的区域:
3.5. 礼帽
礼帽操作是用原图-开运算
,开运算作用是去毛刺,那么礼帽的作用就是获取图片中的毛刺,提取亮细节。
代码如下:
cv::Mat result;
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3,3));
cv::morphologyEx(mat, result, cv::MORPH_BLACKHAT, kernel);
效果如图,获取到毛刺:
3.6. 黑帽
黑帽操作是用闭运算-原图
,闭运算作用是连通,那么黑帽的作用就是提取暗细节。
代码如下:
cv::Mat result;
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3,3));
cv::morphologyEx(mat, result, cv::MORPH_BLACKHAT, kernel);
Socket 与 WebSocket 的区别是啥?
一、基础概念
1. Socket
-
定义:
Socket 是操作系统提供的 网络通信接口,是应用程序与网络协议(如 TCP/UDP)之间的桥梁。它通过底层协议直接与网络交互,提供 全双工通信能力。-
协议类型:
- TCP Socket:基于 TCP 协议,保证数据可靠传输(有序、无丢失)。
- UDP Socket:基于 UDP 协议,提供低延迟传输(但不保证可靠性)。
-
特点:
- 灵活性:可完全控制数据格式、连接状态和传输逻辑。
- 低层控制:适合需要精细控制网络行为的场景(如游戏、物联网设备)。
-
协议类型:
2. WebSocket
-
定义:
WebSocket 是一种 应用层协议,基于 TCP,通过 HTTP 协议升级为持久化双向连接。它通过一次握手后,建立长期稳定的通信通道。-
特点:
- 全双工通信:客户端和服务器可随时主动发送数据。
- 轻量高效:数据帧头部仅 2-10 字节,传输效率高。
- 兼容性:原生支持浏览器和移动设备,适合实时 Web 应用。
-
特点:
二、核心区别对比
1. 协议与连接方式
维度 | Socket | WebSocket |
---|---|---|
协议层级 | 传输层(TCP/UDP)或应用层(自定义) | 应用层(基于 HTTP/TCP) |
连接建立 | 通过 TCP 三次握手直接建立连接 | 通过 HTTP 协议升级(如 GET /ws ) |
连接模式 | 短连接(需手动维护长连接) | 长连接(持久化,一次建立长期有效) |
数据格式 | 纯字节流(需自行解析) | 帧格式(自动分隔和重组数据) |
2. 数据传输与性能
维度 | Socket | WebSocket |
---|---|---|
传输效率 | 低延迟(直接 TCP) | 实时性高,但略高于纯 TCP(因握手开销) |
头部开销 | 无额外 HTTP 头 | 初始握手有 HTTP 头,后续数据头小 |
可靠性 | TCP 保证可靠传输 | 基于 TCP,但需处理协议层错误 |
3. 开发复杂度与维护
维度 | Socket | WebSocket |
---|---|---|
连接管理 | 需手动处理心跳、重连、错误恢复 | 自动管理连接(内置心跳机制) |
数据处理 | 需自行解析分包、处理编码/解码 | 自动处理数据分帧(如文本/二进制) |
安全性 | 需手动实现加密(如 SSL/TLS) | 支持 WSS(WebSocket over TLS) |
防火墙穿透 | 可能被拦截(需开放特定端口) | 通过 HTTP/80 或 HTTPS/443 穿透 |
三、iOS 开发实践
1. Socket 实现
-
常用框架:
- GCDAsyncSocket(TCP):支持异步操作,适合复杂协议控制。
- SwiftSocket(TCP/UDP):轻量级,适合简单通信。
-
开发流程:
-
建立连接:通过
connect(toHost:onPort:)
建立 TCP 连接。 - 数据收发:手动发送字节流(需协议解析)。
-
错误处理:监听
didDisconnectWithError
等回调。
-
建立连接:通过
-
示例代码(TCP):
let socket = GCDAsyncSocket() do { try socket.connect(toHost: "example.com", onPort: 8080, withTimeout: 10) socket.readData(withTimeout: -1, tag: 0) } catch { print("连接失败:\(error)") }
2. WebSocket 实现
-
常用框架:
- Starscream:轻量级,支持文本/二进制消息。
- Socket.IO:兼容多种传输协议(WebSocket 优先)。
-
开发流程:
-
建立连接:通过
connect()
发送 HTTP 升级请求。 -
消息监听:通过
onText
、onData
等回调接收数据。 - 自动重连:支持配置重连策略(如指数退避)。
-
建立连接:通过
-
示例代码(Starscream):
import Starscream let socket = WebSocket(url: URL(string: "wss://example.com/ws")!) socket.onConnect = { print("连接成功") } socket.onText = { text in print("收到文本消息:\(text)") } socket.connect()
四、场景选择与性能优化
1. 适用场景
场景类型 | Socket | WebSocket |
---|---|---|
实时性要求极高 | 游戏、物联网设备控制(毫秒级响应) | 聊天、股票行情(秒级响应) |
低延迟传输 | 传感器数据流、在线游戏同步 | 实时消息推送、音视频同步 |
复杂协议控制 | 自定义二进制协议(如金融交易) | 标准化协议(如聊天协议) |
浏览器/移动端混合开发 | 需额外适配(如 WebSockets 桥接) | 原生支持,适合跨平台实时通信 |
2. 性能优化
Socket 优化建议
- 复用连接:避免频繁建立/关闭连接,复用长连接。
- 数据压缩:使用二进制协议(如 Protobuf)减少传输体积。
- 心跳机制:定期发送心跳包检测连接状态(如每 30 秒一次)。
WebSocket 优化建议
- 消息队列:批量发送消息以减少网络开销。
- 协议压缩:启用 Permessage-Deflate 压缩(需服务器支持)。
- 服务器选择:使用高性能 WebSocket 服务器(如 Nginx、Socket.IO 服务端)。
五、总结:如何选择?
-
选 Socket 的场景:
- 需要 底层协议控制(如自定义二进制协议)。
- 高实时性要求(如游戏、传感器数据)。
- 局域网或封闭环境(防火墙限制较少)。
-
选 WebSocket 的场景:
- 快速开发:标准化协议,开发效率高。
- 实时 Web 应用:聊天、在线协作、股票推送。
- 混合开发:需要与浏览器或跨平台应用通信。
六、关键术语表
术语 | 解释 |
---|---|
TCP | 可靠传输协议,保证数据顺序和完整性。 |
UDP | 无连接协议,低延迟但不保证可靠性。 |
HTTP 升级 | WebSocket 通过 HTTP 请求升级连接。 |
Permessage-Deflate | WebSocket 的数据压缩扩展协议。 |
SwiftUI 国际化
iOS开发:关于路由
在iOS开发中引入路由框架一直是一个有争议的话题。
因为即使不使用路由框架,似乎也不会有太大的影响。那么我们先来回顾一下几个典型的跳转场景:
从外部跳转到App
-
Safari浏览器网页点击事件跳转到App的页面
-
App在挂起或者杀死状态,收到推送跳转到App的页面
-
收到短信,短信里面有短链接跳转到App的页面
-
从邮件中的链接跳转到App的页面
-
从社交媒体应用(如微信、微博)中的链接跳转到App的页面
-
从App的小组件跳转到App的页面
-
Siri、ShotCut进行跳转
App内部跳转
-
单一主工程,无业务模块依赖,页面间进行跳转
-
多模块工程,多个模块之间可以随意跳转
需要注意的是,对于多模块工程,模块可能是第三方开发的,并不遵守内部开发标准,这种情况不在考虑范围内。
路由中心
- 跳转到微信小程序:其实跳转到其他App的小程序也可以认为是这种业务场景。目前已经无法从微信小程序跳转到App了。点击查看
- 跳转到其他App:这种情况跳转API相对固定,传值也有规则,无需路由框架,不在我们的讨论之中。
- 跳转到Safari浏览器:这种情况跳转API也相对固定,传值也有规则,无需路由框架,不在我们的讨论之中。
上图展示了非常复杂的跳转场景。在日常开发过程中,比如推送跳转到不同页面,实际上是通过推送信息中的字符串创建一个枚举映射判断,不同的字段跳转到不同的页面。浏览器和短信消息以及App开屏广告跳转也是使用枚举映射的逻辑。
维护一个枚举映射表可以完成这个功能,但随着业务量的增大,这种方式虽然可行,但不够友好,下面是伪代码示例:
func pushToAppPage(model: LaunchAdModel,
tabbarController: UITabBarController,
navigationController: UINavigationController) {
switch model.appPage {
case "app_home_page":
break
case "app_message_center":/// 消息中心
navigationController.pushViewController(MessageCenterController(), animated: true)
case "app_message_center_detail":/// 公告详情
let vc = AppMessageDetailController()
vc.messageId = model.appItemId
navigationController.pushViewController(vc, animated: true)
case "community_topics":// 资讯
break
case "activity":
break
/// 业务增加会case也逐渐增加,如果入参规则不同,还需要不同的构建器,初始化方法和赋值
}
}
如果使用路由框架将页面和路由表提前绑定,此时外部跳转进来,只需一行代码即可搞定。可以认为是将集中的枚举映射分散到了路由框架中。
其实我完全可以把上面的伪代码封装成一个路由中心,然后制定一系列入参传参规则来保证一致性,但是我也可以直接使用现有的框架来避免我重复造轮子,我只用了解框架的使用与传参规则就可以。
另外,良好的跳转逻辑,不仅需要移动端制定规则,还需要后端配合,完成数据下发的格式的对应。
可以想象一下再App中增加了一个路由中心,所有的跳转情况逻辑与跳转Action都由路由管理,然后再从路由中心发出去:
TheRouter
我最近研究了一下相关框架,目前觉得TheRouter的功能和业务场景符合要求,因为它同时兼容OC,所以在某些语法上看起来很怪异。
其实所有的路由这种从前端借鉴过来的舶来物,总需要这样个过程:
-
注册路由
-
保证注册之后再使用路由
-
异常路由侧进去了定义好的错误页面
比如Flutter中使用GetX的路由,我们会这样:
abstract class Routes {
Routes._();
static const coinRink = '/coinRink';
static const unknown = "/unknown";
///页面合集
static final routePage = [
GetPage(
name: coinRink,
page: () => const CoinRankPage(),
binding: CoinRankBinding(),
middlewares: [LoginMiddleware()],
),
GetPage(
name: unknown,
page: () => const UnknownPage(),
),
];
static final unknownPage = GetPage(
name: Routes.unknown,
page: () => const UnknownPage(),
);
}
TheRouter对比这种思路,手动注册之外,有一个我觉得很有特色功能就是通过runtime遍历进行路由的自动注册,减少了手动注册的不舒适度。
let beginRegisterTime = CFAbsoluteTimeGetCurrent()
var resultXLClass = [AnyClass]()
let bundles = CFBundleGetAllBundles() as? [CFBundle]
for bundle in bundles ?? [] {
let identifier = CFBundleGetIdentifier(bundle);
if let id = identifier as? String {
if excludeCocoapods {
if id.hasPrefix(kSAppleSuffix) || id.hasPrefix(kSCocoaPodsSuffix) {
continue
}
} else {
if id.hasPrefix(kSAppleSuffix) {
continue
}
}
}
guard let execURL = CFBundleCopyExecutableURL(bundle) as NSURL? else { continue }
let imageURL = execURL.fileSystemRepresentation
let classCount = UnsafeMutablePointer<UInt32>.allocate(capacity: MemoryLayout<UInt32>.stride)
guard let classNames = objc_copyClassNamesForImage(imageURL, classCount) else {
continue
}
for idx in 0..<classCount.pointee {
let currentClassName = String(cString: classNames[Int(idx)])
guard let currentClass = NSClassFromString(currentClassName) else {
continue
}
if class_getInstanceMethod(currentClass, NSSelectorFromString("methodSignatureForSelector:")) != nil,
class_getInstanceMethod(currentClass, NSSelectorFromString("doesNotRecognizeSelector:")) != nil {
if let cls = currentClass as? UIViewController.Type {
resultXLClass.append(cls)
}
}
#if DEBUG
if let clss = currentClass as? CustomRouterInfo.Type {
apiArray.append(clss.patternString)
classMapArray.append(clss.routerClass)
}
#endif
}
}
for i in 0 ..< resultXLClass.count {
let currentClass: AnyClass = resultXLClass[i]
if let cls = currentClass as? TheRouterable.Type {
let fullName: String = NSStringFromClass(currentClass.self)
if fullName.contains(kSADelegateClassSensorsSuffix) {
break
}
for s in 0 ..< cls.patternString.count {
if fullName.contains(NSKVONotifyingPrefix) {
let range = fullName.index(fullName.startIndex, offsetBy: NSKVONotifyingPrefix.count)..<fullName.endIndex
let subString = fullName[range]
registerRouterList.append([TheRouterPath: cls.patternString[s], TheRouterClassName: "\(subString)", TheRouterPriority: "\(cls.priority)"])
} else {
registerRouterList.append([TheRouterPath: cls.patternString[s], TheRouterClassName: fullName, TheRouterPriority: "\(cls.priority)"])
}
}
} else if currentClass.self.conforms(to: TheRouterableProxy.self) {
let fullName: String = NSStringFromClass(currentClass.self)
if fullName.contains(kSADelegateClassSensorsSuffix) {
break
}
for s in 0 ..< currentClass.patternString().count {
if fullName.contains(NSKVONotifyingPrefix) {
let range = fullName.index(fullName.startIndex, offsetBy: NSKVONotifyingPrefix.count)..<fullName.endIndex
let subString = fullName[range]
registerRouterList.append([TheRouterPath: currentClass.patternString()[s], TheRouterClassName: "\(subString)", TheRouterPriority: "\(String(describing: currentClass.priority()))"])
} else {
registerRouterList.append([TheRouterPath: currentClass.patternString()[s], TheRouterClassName: fullName, TheRouterPriority: "\(String(describing: currentClass.priority()))"])
}
}
}
}
let endRegisterTime = CFAbsoluteTimeGetCurrent()
另外需要注意,在最新的Xcode16下面,Debug模式下面自动注册runtime不起作用,需要修改一下工程配置:
Xcode16 下 Debug 模式
ENABLE_DEBUG_DYLIB
选项默认开启,开启之后objc_copyClassNamesForImage
主工程 image 调用失败,Debug 模式下会使用**.debug.dylib
文件,所以会有点问题。可以先将ENABLE_DEBUG_DYLIB
关闭
同时TheRouter会有一个强制校验过程,也就是必须在工程中手动维护一张路由表,来保证自动注册的路由表和手动注册的路由表一致,这种措施是为了保证在Debug环境下的一致性,当然如果工程不那么复杂,这个功能不用也罢:
/// - Parameters:
/// - excludeCocoapods: 排除一些非业务注册类,这里一般会将 "com.apple", "org.cocoapods" 进行过滤,但是如果组件化形式的,创建的BundleIdentifier也是
/// org.cocoapods,这里需要手动改下,否则组件内的类将不会被获取。
/// - urlPath: 将要打开的路由path
/// - userInfo: 路由传递的参数
/// - forceCheckEnable: 是否支持强制校验,强制校验要求Api声明与对应的类必须实现TheRouterAble协议
/// - forceCheckEnable 强制打开TheRouterApi定义的便捷类与实现TheRouterAble协议类是否相同,打开的话,debug环境会自动检测,避免线上出问题,建议打开
return TheRouterManager.addGloableRouter(true, url, userInfo, forceCheckEnable: false)
如果项目是多模块组成,传统的push与pop可能需要模块对外暴露Controller,以保证可以构建控制器与页面跳转。使用路由框架可以抹掉这些细节与传参、构造器方法,对外暴露跳转路径即可。
同时我也在思考,如果一个Flutter项目也是多模块的情况下,主工程无法知道子模块的暴露的Page,是如何维护路由表的呢?
截止我发文的时候,掘金的货拉拉又发了一篇文章,《iOS货运用户App组件路由器设计与实践》,不过目前被删除了,也不知道是个啥情况,反正就是说他们还有一套与TheRouter的不同的路由框架,嗯,好吧~
结论
-
路由框架并不是iOS开发的必备工具。如果外部跳转到App场景少,App内部跳转简单,单一工程,或者多模块但模块间跳转场景少、不复杂,可以不用。
-
路由带来方便的同时,可能会导致页面切换转场动画的固定化,因为路由的目的是打开页面,而页面相关的动画等,如果放在路由框架中,显然又不太合适,所以当存在路由框架时,在需要使用转场动画时,可能无法尽善尽美。
-
如果外部跳转和App内部跳转复杂,可以考虑使用路由框架,以减轻维护逻辑的编写。同时如果考虑双端一致性,甚至可以一次配置,双端可行。