- vừa được xem lúc

React Native ẩn bên trong: Mã JS, iOS và Android của bạn chạy cùng nhau như thế nào?

0 0 2

Người đăng: Tiến Minh

Theo Viblo Asia

image.png Bạn đã bao giờ tự hỏi React Native thực sự chạy mã của bạn như thế nào chưa?

Khi làm việc với React Native, việc hiểu cách JavaScript, iOS và Android xử lý việc thực thi và biên dịch mã là rất quan trọng.

Khác với các nền tảng native sử dụng ngôn ngữ biên dịch, JavaScript không có hệ thống xây dựng truyền thống mà trải qua một quá trình chuyển đổi khác. Bạn viết JavaScript/TypeScript, nhưng nó điều khiển các thành phần native của iOS và Android.

Đây là một quá trình đa giai đoạn bao gồm chuyển đổi mã, đóng gói, thực thi JavaScript, giao tiếp và kết xuất native.

Quy trình

  • Phát triển: Viết mã TS.
  • Xây dựng: TS -> JS (chuyển đổi) -> JS Bundle (Metro).
  • Khởi chạy: Mã shell của ứng dụng native bắt đầu, tải JS bundle vào JavaScript Engine (Hermes/JSC).
  • Thực thi: Mã JS chạy, React xác định cấu trúc UI.
  • Kết xuất: Các chỉ dẫn React được gửi qua Bridge/JSI tới Native View Managers, nơi tạo hoặc cập nhật các phần tử UI native (UIView, android.view.View).
  • Tương tác: Người dùng tương tác với các phần tử UI native -> Các sự kiện được gửi qua Bridge/JSI quay lại JS -> Các hàm xử lý sự kiện JS chạy -> Trạng thái cập nhật -> UI có thể được vẽ lại (trở lại bước 5).
  • Native APIs: JS gọi Native Modules -> Bridge/JSI chuyển tiếp lời gọi tới mã native -> Mã native thực thi API của nền tảng -> Kết quả được gửi lại qua Bridge/JSI tới JS.

Hãy cùng phân tích chi tiết quy trình thực thi và biên dịch cho từng phần.

JavaScript (Bộ não 🧠)

Mặc dù JavaScript thường là một ngôn ngữ được thông dịch, khác với các ngôn ngữ biên dịch native như Swift hay Kotlin, React Native có một dạng "biên dịch" trước khi mã của bạn thực sự chạy trên thiết bị.

Nhưng nếu ứng dụng của bạn được viết bằng TypeScript, tận dụng tính năng kiểu tĩnh, interfaces và các tính năng khác của TS, bước đầu tiên là:

📌 Biến đổi TypeScript thành JavaScript (Trong quá trình build)

  • Trước khi mã của bạn có thể chạy, nó cần phải được chuyển đổi thành JavaScript, thứ mà môi trường runtime của React Native hiểu. Trình biên dịch TS (tsc) chịu trách nhiệm cho việc này.
  • Trong quá trình build (khi bạn chạy npx react-native run-ios hoặc run-android), tsc (thường được gọi một cách ngầm định bởi Metro bundler) sẽ đọc các file .ts và .tsx của bạn. Nó thực hiện việc kiểm tra kiểu dữ liệu và sau đó loại bỏ tất cả các cú pháp đặc thù của TypeScript (như chú thích kiểu, interfaces, v.v.).

➡️ Kết quả: Kết quả của bước này là mã JavaScript chuẩn (.js), về cơ bản tương đương với mã TypeScript của bạn, chỉ khác là không có thông tin kiểu dữ liệu.

Bây giờ logic JavaScript của bạn được transpile và đóng gói bởi Metro trong quá trình build. Nếu sử dụng Hermes, mã này sẽ được biên dịch thêm thành bytecode tối ưu hóa. Gói cuối cùng này chạy trong một môi trường JavaScript chuyên dụng trên thiết bị, giao tiếp với phần native để điều khiển giao diện người dùng và chức năng của ứng dụng.

📌 JavaScript Bundling (Trong Thời Gian Build)

React Native sử dụng Metro, một trình bundler JavaScript chuyên dụng, để xử lý việc này. Metro thực hiện hai nhiệm vụ chính:

Transpilation: Do sự phát triển của các tiêu chuẩn JavaScript (ECMAScript) và việc sử dụng JSX, mã của bạn cần được chuyển đổi thành một phiên bản mà công cụ JavaScript mục tiêu (ví dụ, Hermes) có thể hiểu được. Metro sử dụng Babel dưới nền để thực hiện việc này; Babel sẽ chuyển đổi cú pháp JavaScript hiện đại và JSX thành mã tương thích, theo cấu hình preset của React Native để phù hợp với khả năng của engine đang sử dụng.

Bundling: Metro đóng gói tất cả các tệp JavaScript và phụ thuộc của bạn thành một (hoặc vài) tệp JS hiệu quả (bundle), tối ưu hóa cho việc tải khi chạy (dễ hiểu cho JS engine).

➡️ Kết quả: JS Bundle; Thông thường, đây là một tệp .js duy nhất (ví dụ, index.bundle) chứa toàn bộ logic ứng dụng đã sẵn sàng để thực thi.

📌 Chạy JavaScript (Runtime)

Khi bạn build ứng dụng React Native, một ứng dụng "shell" Native được tạo ra cho cả iOS (sử dụng Objective-C/Swift) và Android (sử dụng Java/Kotlin). Shell này chứa mã Native cần thiết để host React Native.

Execution JS Engine (JavaScriptCore vs. Hermes):
Shell Native này nhúng một engine JS (một chương trình thực thi mã JS). React Native có thể sử dụng các engine khác nhau:

  • JavaScriptCore (JSC): Trước đây là engine phổ biến, được sử dụng bởi Safari. Nó cũng có sẵn trên Android nhưng hiện nay ít được sử dụng hơn.
  • Hermes: Một engine JavaScript mã nguồn mở được Meta tối ưu hóa đặc biệt để chạy ứng dụng React Native và thường là mặc định cho các dự án React Native mới trên cả hai nền tảng.

Một lợi thế chính của Hermes là trong giai đoạn build ứng dụng, nó biên dịch trước mã JavaScript thành bytecode. Định dạng bytecode này cho phép giảm thời gian khởi động ứng dụng (Time To Interactive - TTI) và giảm mức sử dụng bộ nhớ so với việc giải thích mã JS thô hoặc sử dụng biên dịch Just-In-Time (JIT) trên thiết bị.

➡️ Vậy khi bạn khởi chạy ứng dụng: Shell Native khởi động engine JS và tải JS bundle được tạo bởi Metro. Sau đó, engine JS bắt đầu thực thi mã ứng dụng của bạn, bắt đầu từ điểm nhập (thường là index.js). React bắt đầu render các component của bạn vào bộ nhớ.

iOS (Swift/Objective-C - Giao Diện và Trải Nghiệm trên Apple 🍎)

image.png Khác với JavaScript, Swift và Objective-C là các ngôn ngữ biên dịch. Hệ thống build của iOS biên dịch mã nguồn (bao gồm cả các phần Native của React Native và bất kỳ Native Modules nào bạn thêm vào) thành mã máy trước khi thực thi, đảm bảo hiệu suất tối ưu trên các thiết bị của Apple.

React Native cho iOS dựa vào hệ thống build của Xcode, sử dụng trình biên dịch Clang (cho Objective-C) và công cụ LLVM (cho Swift) để chuyển mã nguồn dễ đọc của con người thành mã nhị phân tối ưu dành riêng cho bộ xử lý iPhone/iPad (như .app bundle).

Dưới đây là các bước chính:

  • Biên Dịch Mã Nguồn: Các file Swift và Objective-C được biên dịch thành mã máy sử dụng Clang/LLVM.
  • Linking: Mã đã biên dịch, các framework hệ thống và thư viện bên ngoài (từ CocoaPods) được liên kết lại với nhau.
  • Ký: Apple yêu cầu các ứng dụng phải được ký bằng chứng chỉ và hồ sơ provisioning hợp lệ trước khi có thể chạy trên thiết bị.
  • Đóng Gói: Mã nhị phân cuối cùng (.ipa file) được tạo ra, chứa ứng dụng và các tài nguyên của nó.

➡️ Kết Quả: Mã đã biên dịch này được đóng gói vào .app bundle và được cài đặt trên thiết bị của người dùng. Nó chạy trực tiếp trên phần cứng khi ứng dụng được khởi động.

Android (Kotlin/Java - Giao Diện và Trải Nghiệm trên Android 🤖)

image.png Tương tự như iOS (nhưng quy trình có phần phức tạp hơn), Android sử dụng hệ thống build biên dịch, dựa vào Gradle, tự động hóa quá trình xây dựng, kiểm thử và đóng gói ứng dụng Android.

React Native cho Android biên dịch mã nguồn Java/Kotlin thành định dạng Dalvik Executable (DEX), được tối ưu hóa để thực thi bởi Android Runtime (ART).

Dưới đây là các bước chính:

  • Biên Dịch Mã Nguồn: Các file Java/Kotlin được biên dịch thành các file bytecode .class sử dụng các trình biên dịch javac (cho Java) và kotlinc (cho Kotlin).
  • Chuyển Đổi DEX: Các file .class đã biên dịch (JVM Bytecode) được chuyển thành các file .dex (Dalvik Bytecode), tối ưu hóa cho Android Runtime bằng trình biên dịch DEX (d8).
  • Xử Lý Tài Nguyên: Các layout XML, hình ảnh và tài nguyên khác được đóng gói lại.
  • Linking: Các thư viện và phụ thuộc bên ngoài (quản lý bởi Gradle) được liên kết.
  • Ký: Android yêu cầu các ứng dụng phải được ký bằng một tệp keystore hợp lệ.
  • Đóng Gói: Cuối cùng, tệp .apk hoặc .aab được tạo ra để cài đặt trên thiết bị.

➡️ Kết Quả: Mã đã biên dịch/tối ưu hóa này được đóng gói vào tệp .apk hoặc .aab để phân phối.

📌 The Bridge / JSI (Runtime)

Đây là cốt lõi của cách JavaScript tương tác với nền tảng native. Có hai kiến trúc chính:

  1. The Bridge (Kiến Trúc Cũ): The Bridge hoạt động như một trung gian giữa mã JS của bạn và nền tảng native iOS/Android. Nó gửi dữ liệu tuần tự (lệnh, cập nhật giao diện người dùng) từ JS sang Native một cách bất đồng bộ. Sau đó, mã Native thực thi các lệnh này và gửi kết quả hoặc các sự kiện tương tác của người dùng trở lại JS. Tuy nhiên, tính chất bất đồng bộ của nó có thể dẫn đến độ trễ, chi phí tuần tự/hủy tuần tự ("Thuế Bridge") và có thể tạo ra các nút thắt cổ chai nếu quá nhiều tin nhắn được gửi đi.

  2. JavaScript Interface (JSI) (Kiến Trúc Mới - Fabric & TurboModules): Kiến trúc mới của React Native thay thế Bridge bất đồng bộ bằng JavaScript Interface (JSI). JSI cho phép các lệnh đồng bộ trực tiếp giữa JavaScript và Native (qua C++), loại bỏ chi phí tuần tự. Điều này cung cấp sức mạnh cho Fabric (hệ thống hiển thị giao diện người dùng nhanh hơn) và TurboModules (các mô-đun native được tải lười và nhanh hơn). Kết quả là hiệu suất, độ phản hồi được cải thiện đáng kể và hỗ trợ tính năng tốt hơn.

Để tìm hiểu thêm về các kiến trúc này, bạn có thể xem bài viết cách React Native hoạt động nội bộ.

📌 Native Modules và Native Components (Runtime)

Native Modules Native Modules tiếp xúc với các API của nền tảng native (như camera, vị trí địa lý, bộ nhớ, Bluetooth, mã native tùy chỉnh) từ mã JavaScript của bạn. Khi mã JS gọi NativeModules.MyCamera.takePicture(), JSI sẽ chuyển đổi điều này thành một lệnh gọi tới phương thức native tương ứng của Objective-C/Swift (iOS) hoặc Java/Kotlin (Android). Mã native sẽ thực thi và kết quả sẽ được trả lại cho JS.

Native Components (View Managers): Đây là các đối tác của các component React như <View>, <Text>, <Image>, v.v. Khi React quyết định hiển thị một <View>, nó không tạo ra một

HTML mà gửi các hướng dẫn (qua JSI) tới phía native.

-> Trên iOS, một "View Manager" native nhận các hướng dẫn này và tạo/cập nhật một UIView native thực tế. -> Trên Android, một "View Manager" native tạo/cập nhật một android.view.View native thực tế.

Điều này đảm bảo rằng giao diện người dùng của bạn trông và cảm nhận như native vì nó đang sử dụng các phần tử UI native của nền tảng. Logic bố cục (như Flexbox) được tính toán (thường trên một luồng nền) và sau đó được áp dụng vào các views native này trên luồng chính của UI.

Chạy ứng dụng trên thiết bị

Kiểm thử là một phần thiết yếu trong quy trình phát triển ứng dụng di động. Khác với các ứng dụng web, có thể kiểm thử trực tiếp trong trình duyệt, ứng dụng di động cần các trình giả lập (emulator trên Android) hoặc thiết bị vật lý để kiểm thử. Các trình giả lập cho phép các nhà phát triển chạy và gỡ lỗi ứng dụng trong một môi trường kiểm soát, mô phỏng hành vi thực tế của thiết bị.

■ Android
Bạn có thể tương tác với các trình giả lập Android bằng các phương pháp sau:

Android Studio AVD Manager: Công cụ tích hợp sẵn để quản lý các trình giả lập Android. Bạn có thể tạo, cấu hình và khởi chạy các thiết bị ảo tại đây.

Dòng lệnh (adb và emulator commands): emulator -list-avds: Liệt kê tất cả các thiết bị ảo có sẵn.
emulator @Pixel_6_API_34: Khởi chạy một thiết bị cụ thể.
adb devices: Liệt kê các trình giả lập đang chạy và các thiết bị vật lý đã kết nối. Các công cụ dòng lệnh như Expo CLI hoặc React Native Community CLI sử dụng các lệnh này bên dưới để cung cấp trải nghiệm phát triển tốt hơn.

image.png

■ iOS
Bạn có thể tương tác với các trình giả lập iOS qua các phương pháp sau: Xcode: Công cụ tích hợp sẵn của Apple để chạy ứng dụng iOS trên một thiết bị giả lập. Bạn có thể khởi chạy nó qua Xcode > Open Developer Tool > Simulator. Dòng lệnh (xcrun simctl commands):
xcrun simctl list: Liệt kê các trình giả lập có sẵn.
xcrun simctl boot "iPhone 15": Khởi động một trình giả lập cụ thể.
xcrun simctl shutdown all: Tắt tất cả các trình giả lập đang chạy.

Kết luận

Việc hiểu rõ quy trình chạy và biên dịch ứng dụng trong React Native là rất quan trọng đối với các nhà phát triển muốn tối ưu hóa hiệu suất và trải nghiệm người dùng. Từ việc biên dịch TypeScript thành JavaScript, đóng gói mã nguồn với Metro, đến việc sử dụng các công cụ như Hermes để cải thiện thời gian khởi động và tối ưu bộ nhớ, mỗi bước trong quy trình đều góp phần vào việc xây dựng một ứng dụng hiệu quả. Bên cạnh đó, khả năng tương tác với các thành phần gốc qua Native Modules và Native Components đảm bảo rằng ứng dụng của bạn không chỉ chạy mượt mà mà còn mang lại trải nghiệm gốc chính xác trên mỗi nền tảng.

Việc kiểm thử trên các thiết bị vật lý hoặc giả lập không chỉ giúp phát hiện lỗi mà còn đảm bảo rằng ứng dụng của bạn hoạt động ổn định trên mọi môi trường. Sự hiểu biết và kiểm soát quy trình này giúp bạn xây dựng được các ứng dụng di động mạnh mẽ, dễ bảo trì và có thể mở rộng trong tương lai.

Hãy áp dụng những kiến thức này vào dự án của bạn để tối ưu hóa quá trình phát triển và nâng cao chất lượng sản phẩm cuối cùng!

Bình luận

Bài viết tương tự

- vừa được xem lúc

Some useful javascript functions (P2)

Như Phần trước mình đã đề cập đến 4 functions mình cho là khá hữu dụng trong javascript. Trong bài này mình sẽ tiếp tục đưa ra một số functions nữa.

0 0 41

- vừa được xem lúc

Cưỡi React Native xem Appium (Phần 1 - Cùng tìm hiểu cách hoạt động của Appium)

1. Mở đầu.

0 0 44

- vừa được xem lúc

[React Native] Dựng base App React Native - Navigation Sample - Modal Navigation

Introduction. Hi All. Trước khi vào chủ đề chính thì mình xin đưa ra 2 bài toán sau:. .

0 0 48

- vừa được xem lúc

[React Native] Dựng base App React Native - Mobx - Áp dụng thực tế

Chào mọi người. Và hôm nay chúng ta sẽ đi vào bài toán thực tế mà chúng ta có thể sử dụng với Mobx.

0 0 146

- vừa được xem lúc

React Router v6

React Router v6 đang được dần hoàn thiện và sẽ trình làng trong thời gian sắp tới. 1.

0 0 38

- vừa được xem lúc

"Một chút" về kiến trúc mới của React Native

Bài viết gốc: https://vir.vn/mot-chut-ve-kien-truc-moi-cua-react-native/.

0 0 119