去哪儿网

去哪儿网

1.项目

  • 降低三方 App 接入成本怎么做的?

  • 加载速度优化怎么做的?主要是从业务逻辑纬度处理的?

  • 异常监控怎么做的?

  • 动态库版式缩放怎么实现的?

2.线上闪退捕获怎么做的?有没有捕获不到的异常,watchdog 能捕获吗?APM 性能监控

3.RN 是怎么实现的?跨端还了解哪些?Flutter是怎么实现的,为什么说 Flutter 的性能比 RN 好?

4.H5 和原生通信有哪些方法?

5.怎么提升 webView 的加载速度,使其接近原生?大的图片、JS 等端上下载后,怎么交给H5使用

6.小程序是怎么实现的?

7.前端 JS 的异步是怎么实现的?

8.项目亮点

参考答案

(GPT)

1.项目

  • 降低三方 App 接入成本怎么做的?

  • 加载速度优化怎么做的?主要是从业务逻辑纬度处理的?

  • 异常监控怎么做的?

  • 动态库版式缩放怎么实现的?

2.线上闪退捕获怎么做的?有没有捕获不到的异常,watchdog能捕获吗?APM 性能监控

移动端监控体系之技术原理剖析

iOS Crash 捕获及堆栈符号化思路剖析

知乎-iOS云音乐APM性能监控实践

3.RN 是怎么实现的?跨端还了解哪些?Flutter 是怎么实现的,为什么说 Flutter 的性能比 RN 好?

React Native 通过 JavaScript 和原生代码之间的桥接,实现了跨平台的移动应用开发。其核心是通过 Bridge 在 JavaScript 和原生线程之间进行通信,使用 React 构建用户界面,并通过 Yoga 引擎处理布局。这样,开发者可以使用熟悉的 JavaScript 和 React 技术栈来构建高性能的移动应用,同时享受原生性能和体验。

Flutter 通过使用 Dart 语言、Skia 图形引擎和自定义的渲染引擎,实现了跨平台的高性能应用开发。其核心是完全自行绘制界面,不依赖于平台的原生控件,提供了一种一致的开发体验和用户体验。Flutter 的 Widget 系统、渲染树、状态管理和平台通道等机制,使其能够高效地构建复杂的跨平台应用。

Flutter 和 React Native(RN)是两种流行的跨平台移动应用开发框架,各有其优点和缺点。性能是评估这两种框架的一个重要方面。以下是从多个角度对 Flutter 和 React Native 性能的比较:

1. 启动时间

  • Flutter: Flutter 应用的启动时间通常较快,因为 Flutter 使用了 AOT(Ahead-of-Time)编译,将 Dart 代码编译成了原生 ARM 代码。在启动时,无需进行额外的解释或编译步骤。
  • React Native: React Native 应用的启动时间可能稍慢一些,因为它依赖于 JavaScriptCore(iOS)或 Hermes 引擎(Android)来解释和执行 JavaScript 代码。尽管 Hermes 引擎在某些情况下可以减少启动时间,但它仍然需要一些初始化步骤。

2. UI 渲染性能

  • Flutter: Flutter 使用 Skia 图形引擎直接绘制到屏幕上,这使得 Flutter 可以提供非常流畅的 UI 渲染性能。Flutter 的渲染是逐帧进行的,通常可以达到 60fps 或 120fps。
  • React Native: React Native 依赖于原生平台的控件和组件进行渲染,通过 Bridge 进行 JavaScript 和原生代码的通信。这种桥接机制可能会导致一定的性能瓶颈,特别是在处理复杂动画和大量 UI 更新时。

3. 动画性能

  • Flutter: Flutter 的动画性能通常非常出色,因为动画是在 Flutter 的渲染引擎中直接处理的,没有额外的通信开销。Flutter 提供了丰富的动画库和工具,使开发者可以轻松创建复杂和流畅的动画。
  • React Native: React Native 的动画性能在某些情况下可能不如 Flutter,因为动画需要通过 Bridge 进行通信。虽然 React Native 提供了 Animated API 和 Reanimated 库来优化动画性能,但在处理非常复杂的动画时,可能仍然会遇到性能问题。

4. 内存使用

  • Flutter: Flutter 的内存使用通常较为高效,但由于它将整个 UI 树都保持在内存中,内存占用可能会随着应用复杂度的增加而增加。
  • React Native: React Native 的内存使用也比较高效,不过由于 JavaScript 运行时和 Bridge 的存在,可能会引入额外的内存开销。

5. 开发体验

  • Flutter: Flutter 提供了热重载(Hot Reload)功能,使开发者可以快速看到代码修改的效果,极大地提高了开发效率。Flutter 的热重载通常速度非常快,体验流畅。
  • React Native: React Native 也提供了热重载和快速刷新(Fast Refresh)功能,不过在某些情况下,热重载的速度可能不如 Flutter 快。

6. 跨平台一致性

  • Flutter: 由于 Flutter 使用了自定义的绘图引擎,它在不同平台上的外观和行为非常一致,开发者可以更加精确地控制 UI 的细节。
  • React Native: React Native 使用的是平台原生控件,虽然这意味着它在不同平台上能提供更原生的体验,但也可能导致不同平台之间的外观和行为不一致。

总结

  • Flutter 在 UI 渲染性能、动画性能和跨平台一致性方面通常优于 React Native。其高效的渲染引擎和 AOT 编译使得 Flutter 应用的启动时间和运行性能都非常出色。
  • React Native 在使用原生控件和访问原生平台功能方面有优势,可以提供更原生的用户体验。不过,由于 JavaScript 和原生代码之间的桥接机制,可能会在某些场景下遇到性能瓶颈。

最终选择哪个框架,取决于具体项目的需求和开发团队的技术栈。如果需要非常高性能、流畅的动画和一致的跨平台体验,Flutter 可能是更好的选择。如果需要更原生的外观和感觉,或者开发团队已经熟悉 JavaScript 和 React 生态系统,React Native 可能更适合。

4.H5 和原生通信有哪些方法?

1. 通过 URL Schemes

URL Schemes 是一种通过特定 URL 格式触发原生代码的方法。

2. 通过 WKScriptMessageHandler

WKScriptMessageHandler 是 WKWebView 提供的用于接收来自 JavaScript 消息的处理器。

H5 代码

1
window.webkit.messageHandlers.myHandler.postMessage({key: 'value'});

iOS 代码

WKWebView 的配置中添加一个脚本消息处理器:

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
import WebKit

class ViewController: UIViewController, WKScriptMessageHandler {
var webView: WKWebView!

override func viewDidLoad() {
super.viewDidLoad()

let config = WKWebViewConfiguration()
config.userContentController.add(self, name: "myHandler")

webView = WKWebView(frame: self.view.frame, configuration: config)
self.view.addSubview(webView)

if let url = URL(string: "https://yourwebsite.com") {
webView.load(URLRequest(url: url))
}
}

func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if message.name == "myHandler" {
if let body = message.body as? [String: Any] {
print("Message from H5: \(body)")
}
}
}
}

3. 通过 evaluateJavaScript

iOS 可以通过 evaluateJavaScript 方法执行 H5 页面上的 JavaScript 代码。

iOS 代码

1
2
3
4
5
6
7
webView.evaluateJavaScript("javascriptFunctionName('argument')") { (result, error) in
if let error = error {
print("JavaScript evaluation error: \(error)")
} else {
print("JavaScript evaluation result: \(String(describing: result))")
}
}

4. 通过 WebView 的 loadHTMLStringload 方法

你可以通过 loadHTMLStringload 方法将 HTML 内容直接加载到 WebView 中,并在 HTML 中嵌入 JavaScript 代码与原生代码进行通信。

5.怎么提升 webView 的加载速度,使其接近原生?大的图片、js等端上下载后,怎么交给H5使用

1. 预加载(Preloading)

  • 提前加载 WebView:在用户访问页面之前,提前初始化和加载 WebView。这样用户点击时,内容已经部分或全部加载完毕。
  • 使用缓存:利用缓存机制来存储常用资源,减少重复加载。

2. 优化资源

  • 减少请求数量:合并 CSS、JavaScript 文件,减少 HTTP 请求次数。
  • 压缩资源:使用 gzip 或 brotli 压缩 HTML、CSS 和 JavaScript 文件。
  • 图片优化:压缩图片,使用适当的图片格式(如 WebP),并考虑使用延迟加载(lazy loading)。

3. 使用 WKWebView

  • 选择 WKWebView:相比 UIWebView,WKWebView 提供了更好的性能和更低的内存占用。确保你的应用使用的是 WKWebView。

4. 加载策略

  • 懒加载:对于不在视口内的内容,使用懒加载技术,只在需要时才加载这些资源。
  • 异步加载:将 JavaScript 脚本放在页面底部或者使用 deferasync 属性异步加载脚本,避免阻塞页面渲染。

5. 优化代码

  • 减少 DOM 操作:过多或复杂的 DOM 操作会降低渲染速度,优化前端代码,减少不必要的 DOM 操作。
  • 避免重排(Reflow)和重绘(Repaint):尽量减少和优化需要重排和重绘的操作。

6. 使用本地资源

  • 本地化资源:将常用的 CSS 和 JavaScript 文件内嵌到应用中,避免从远程服务器加载。
  • Service Worker:虽然 iOS 对 Service Worker 的支持不如 Android,但可以利用它在支持的场景下缓存资源,提高加载速度。

7. 网络优化

  • CDN:使用内容分发网络(CDN)来加速资源加载。
  • HTTP/2:使用 HTTP/2 协议来降低延迟和提升加载速度。

8. 确保设备性能

  • 内存优化:确保 WebView 不会消耗过多内存,避免在低内存设备上崩溃或变慢。
  • 性能监控:定期监控应用的性能,并根据实际数据进行优化。

9. 使用 SPA (Single Page Application)

  • 单页应用:如果你的应用逻辑允许,考虑将 Web 应用改为单页应用(SPA),减少页面跳转时间,并通过前端路由管理页面状态。

10. 其他优化

  • Lazy Load:对图片和其他资源使用懒加载技术。
  • 减少动画和特效:过多的动画和特效会增加 CPU 和 GPU 的负担,尽量减少或优化这些效果。

通过综合以上方法,逐步优化 WebView 的加载速度,可以显著提升用户体验,使其接近原生应用的性能。

1. 预加载并注入资源

在 WKWebView 初始化时,可以预先加载本地下载的资源,并通过 JavaScript 注入到网页中。

示例代码:

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
import WebKit

class ViewController: UIViewController, WKNavigationDelegate {
var webView: WKWebView!

override func viewDidLoad() {
super.viewDidLoad()

let webConfiguration = WKWebViewConfiguration()
webView = WKWebView(frame: .zero, configuration: webConfiguration)
webView.navigationDelegate = self
view.addSubview(webView)

// 加载本地 HTML 文件
if let localHTMLUrl = Bundle.main.url(forResource: "index", withExtension: "html") {
webView.loadFileURL(localHTMLUrl, allowingReadAccessTo: localHTMLUrl.deletingLastPathComponent())
}

// 注入本地 JavaScript 文件
if let localJSUrl = Bundle.main.url(forResource: "script", withExtension: "js"),
let localJSContent = try? String(contentsOf: localJSUrl) {
let userScript = WKUserScript(source: localJSContent, injectionTime: .atDocumentStart, forMainFrameOnly: true)
webView.configuration.userContentController.addUserScript(userScript)
}
}
}

2. 使用自定义 URL Scheme

通过自定义 URL Scheme,将本地资源提供给 WebView。

示例代码:

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 WebKit

class ViewController: UIViewController, WKNavigationDelegate {
var webView: WKWebView!

override func viewDidLoad() {
super.viewDidLoad()

let webConfiguration = WKWebViewConfiguration()
webView = WKWebView(frame: .zero, configuration: webConfiguration)
webView.navigationDelegate = self
webView.uiDelegate = self
view.addSubview(webView)

if let localHTMLUrl = Bundle.main.url(forResource: "index", withExtension: "html") {
webView.loadFileURL(localHTMLUrl, allowingReadAccessTo: localHTMLUrl.deletingLastPathComponent())
}

let schemeHandler = LocalSchemeHandler()
webConfiguration.setURLSchemeHandler(schemeHandler, forURLScheme: "myapp")

if let url = URL(string: "myapp://localResource/script.js") {
webView.load(URLRequest(url: url))
}
}
}

class LocalSchemeHandler: NSObject, WKURLSchemeHandler {
func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
if let url = urlSchemeTask.request.url, url.scheme == "myapp" {
if url.path == "/localResource/script.js",
let filePath = Bundle.main.path(forResource: "script", ofType: "js") {
let data = try? Data(contentsOf: URL(fileURLWithPath: filePath))
let response = URLResponse(url: url, mimeType: "application/javascript", expectedContentLength: data?.count ?? 0, textEncodingName: nil)
urlSchemeTask.didReceive(response)
if let data = data {
urlSchemeTask.didReceive(data)
}
urlSchemeTask.didFinish()
}
}
}

func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
// Handle task stopping if needed
}
}

3. 使用 Base64 编码

将本地资源转换成 Base64 编码,然后通过 JavaScript 将其注入到网页中。

示例代码:

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
import WebKit

class ViewController: UIViewController {
var webView: WKWebView!

override func viewDidLoad() {
super.viewDidLoad()

webView = WKWebView(frame: self.view.frame)
self.view.addSubview(webView)

if let localHTMLUrl = Bundle.main.url(forResource: "index", withExtension: "html") {
webView.loadFileURL(localHTMLUrl, allowingReadAccessTo: localHTMLUrl.deletingLastPathComponent())
}

if let imagePath = Bundle.main.path(forResource: "image", ofType: "png"),
let imageData = try? Data(contentsOf: URL(fileURLWithPath: imagePath)) {
let base64String = imageData.base64EncodedString()

let jsCode = """
var img = document.createElement('img');
img.src = 'data:image/png;base64,\(base64String)';
document.body.appendChild(img);
"""
webView.evaluateJavaScript(jsCode, completionHandler: nil)
}
}
}

4. 使用本地服务器

在应用内启动一个本地服务器(如 GCDWebServer),然后通过 HTTP 请求获取本地资源。

示例代码:

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
import WebKit
import GCDWebServer

class ViewController: UIViewController {
var webView: WKWebView!
var webServer: GCDWebServer!

override func viewDidLoad() {
super.viewDidLoad()

webServer = GCDWebServer()

// 添加处理器来提供本地资源
webServer.addGETHandler(forBasePath: "/", directoryPath: Bundle.main.resourcePath!, indexFilename: nil, cacheAge: 3600, allowRangeRequests: true)

// 启动本地服务器
webServer.start(withPort: 8080, bonjourName: "GCD Web Server")

webView = WKWebView(frame: self.view.frame)
self.view.addSubview(webView)

if let url = URL(string: "http://localhost:8080/index.html") {
webView.load(URLRequest(url: url))
}
}
}

通过上述方法,可以将本地下载的资源交给 H5 使用,以提升 WebView 的加载速度和用户体验。选择具体方案时,可以根据实际需求、项目复杂度和维护成本进行权衡。

6.小程序是怎么实现的?

7.前端 JS 的异步是怎么实现的?

JavaScript 的异步是通过事件循环(Event Loop)、回调函数、任务队列(Task Queue)和微任务队列(Microtask Queue)等机制共同实现的。下面我们详细讨论这些概念及其工作原理。

事件循环(Event Loop)

JavaScript 运行时环境(如浏览器或 Node.js)包含一个事件循环,它是处理异步操作的核心。事件循环负责监控调用栈和任务队列,并根据情况将任务队列中的任务推入调用栈执行。

调用栈(Call Stack)

调用栈用于追踪正在执行的函数,JavaScript 是单线程的,这意味着它一次只能执行一个任务。调用栈记录了函数的调用顺序,并在当前函数执行完毕后,依次返回上一级调用。

Web API

浏览器提供了一些异步 API,如 setTimeoutsetIntervalXMLHttpRequestfetch 等。当调用这些 API 时,任务会被移交给浏览器处理,而不是立即在调用栈中执行。

任务队列(Task Queue)

任务队列存储了已完成的异步操作对应的回调函数,这些回调函数等待事件循环将它们推入调用栈执行。任务队列包括宏任务队列(Macro Task Queue)和微任务队列(Microtask Queue)。

宏任务(Macro Task)

宏任务包括 setTimeoutsetInterval、I/O 操作等。这些任务会被添加到宏任务队列中,等待事件循环处理。

微任务(Microtask)

微任务包括 Promise.then 回调、MutationObserver 等。微任务会被添加到微任务队列中。微任务队列的优先级高于宏任务队列,这意味着在每次宏任务执行完毕后,事件循环会首先清空微任务队列。

工作流程

  1. 主线程执行同步代码:所有同步代码会被压入调用栈中并执行。
  2. 遇到异步操作:当遇到异步操作(如 setTimeoutfetch 等),相应的回调函数会被注册并移交给浏览器的 Web API 处理。
  3. Web API 完成任务:一旦异步操作完成,回调函数会被放入相应的任务队列中(宏任务或微任务)。
  4. 事件循环检查调用栈:事件循环不断检查调用栈是否为空。
  5. 处理微任务:如果调用栈为空,事件循环会优先处理微任务队列中的任务。
  6. 处理宏任务:如果微任务队列为空,事件循环会处理宏任务队列中的任务。

示例代码

1
2
3
4
5
6
7
8
9
10
11
console.log('Start');

setTimeout(() => {
console.log('Timeout callback');
}, 0);

Promise.resolve().then(() => {
console.log('Promise callback');
});

console.log('End');

执行顺序解析

  1. console.log('Start') 输出 “Start”。
  2. setTimeout 注册一个回调函数,并将其移交给浏览器处理(宏任务)。
  3. Promise.resolve().then 注册一个回调函数,并将其放入微任务队列。
  4. console.log('End') 输出 “End”。
  5. 主线程执行完同步代码后,调用栈为空,事件循环首先处理微任务队列。
  6. 微任务队列中的 Promise 回调函数被执行,输出 “Promise callback”。
  7. 微任务队列清空后,事件循环处理宏任务队列中的 setTimeout 回调,输出 “Timeout callback”。

综上所述,JavaScript 的异步机制通过事件循环、调用栈、任务队列和 Web API 的协同工作,实现了异步操作,使得代码可以在等待长时间操作(如网络请求)时,不阻塞主线程的执行。

8.项目亮点

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×