乾貨 | 攜程機票 App Kotlin Multiplatform 初探

2021-02-07 安卓綠色聯盟

作者簡介:

陳琦,攜程機票研發部無線研發總監,負責攜程App機票業務的技術研發和管理工作。

從2017年9月到2019年5月,經過一年半的努力,攜程機票App團隊完成90%從 Native 到 Ctrip React Native (CRN) 的技術棧轉型。

2019年初,我們開始思考下一步規劃。

除了繼續做深 React Native 技術,更快更穩定的迭代交付機票業務需求,優化用戶體驗,我們需要從具體業務邏輯實現層次抽離出來,從一個完整的應用程式架構設計和實現的角度,尋找跨平臺技術的未來方向。

React Native 和 Flutter 這類大前端技術方案已經可以很好的支撐用戶界面和組件,業務邏輯需求功能的實現,但是單線程動態腳本語言在以下領域仍顯不足。

我們希望能夠找到一種可靠的跨平臺,原生,或能夠與原生 API 進行靈活自由雙向互操作的技術方案。經過一段時間的針對 Kotlin 及相關開源社區的調研,觀察,實踐,Kotlin Multiplatform 技術在這方面展現出了良好的發展潛力。

傳統主流的跨平臺原生方案是 C/C++,目前依然是最被廣泛使用的。 React Native 和 Flutter 的底層實現也是如此。

Kotlin Multiplatform 的跨平臺遷移如下圖。

了解 Kotlin Multiplatform 需要先從 Kotlin Native 入手。相比 Kotlin/JVM,Kotlin Native 使用 Kotlin 語言編譯器,配合 LLVM backend,將 Kotlin 代碼編譯為平臺原生二進位文件,不依賴虛擬機或運行時環境。當前 LLVM 版本 6.0.1 。官方正在將編譯方案從 LLVM 的backend 轉移到 frontend (clang) 。

目前已支持的平臺:

iOS 9.0+ (arm32, arm64, x86_64 模擬器)

MAC OS (x86_64)

Android (arm32, arm64) ,編譯生成 Linux SO 文件

Windows (mingw x86_64, x86)

Linux (x86_64, arm32, MIPS, MIPS little endian, Raspberry Pi)

WebAssembly (wasm32)

Kotlin Native 官方附帶工具,用於快速生成Kotlin與平臺C庫互相調用操作所需的內容。

首先創建一個.def文件,描述需要包含在語言綁定的內容。

然後使用 cinterop 分析C頭文件,映射生成 Kotlin 語言的類型,函數和常量,完成Kotlin綁定。

最後通過LLVM編譯器連結生成最終的可執行文件 *.kexe 或庫文件 *.klib。

kexe 是平臺相關的可執行程序文件格式。

klib 是平臺相關的庫文件格式,類似 JAR 的ZIP格式,細節詳見官網文檔:

https://kotlinlang.org/docs/reference/native/libraries.html#the-library-format

解壓後的文件夾結構如下:

- foo/- targets/    - $platform/    - kotlin/        - Kotlin compiled to LLVM bitcode.    - native/        - Bitcode files of additional native objects.    - $another_platform/    - There can be several platform specific kotlin and native pairs.- linkdata/    - A set of ProtoBuf files with serialized linkage metadata.- resources/    - General resources such as images. (Not used yet).- manifest - A file in *java property* format describing the library.

左右滑動可查看全部


3.2 平臺庫

大多數情況下,我們並不需要使用 cinterop 手動生成所有所需的C庫綁定。

Kotlin Native SDK已經提供了大部分平臺的原生庫綁定。例如:

Kotlin Native 在本機開發時默認下載到 ~/.konan/ 文件夾,例如 ~/.konan/kotlin-native-macos-1.2.1/, 平臺庫文件位於~/.konan/kotlin-native-macos-1.2.1/klib/platform/,已包含以下內容,可見大部分平臺SDK都已預處理完成。

Android Native Arm32

├── android├── android_arm32.tree.txt├── builtin├── egl├── gles├── gles2├── gles3├── glesCommon├── linux├── media├── omxal├── posix├── sles└── zlib

13 directories, 1 file

iOS Arm64

├── ARKit├── AVFoundation├── AVKit├── Accelerate├── Accounts├── AdSupport├── AddressBook├── AddressBookUI├── AssetsLibrary├── AudioToolbox├── AuthenticationServices├── BusinessChat├── CFNetwork├── CallKit├── CarPlay├── ClassKit├── CloudKit├── CommonCrypto├── Contacts├── ContactsUI├── CoreAudio├── CoreAudioKit├── CoreBluetooth├── CoreData├── CoreFoundation├── CoreGraphics├── CoreImage├── CoreLocation├── CoreMIDI├── CoreML├── CoreMedia├── CoreMotion├── CoreNFC├── CoreServices├── CoreSpotlight├── CoreTelephony├── CoreText├── CoreVideo├── DeviceCheck├── EAGL├── EventKit├── EventKitUI├── ExternalAccessory├── FileProvider├── FileProviderUI├── Foundation├── GLKit├── GSS├── GameController├── GameKit├── GameplayKit├── HealthKit├── HealthKitUI├── HomeKit├── IOSurface├── IdentityLookup├── IdentityLookupUI├── ImageIO├── Intents├── IntentsUI├── LocalAuthentication├── MapKit├── MediaAccessibility├── MediaPlayer├── MediaToolbox├── MessageUI├── Messages├── Metal├── MetalKit├── MetalPerformanceShaders├── MobileCoreServices├── ModelIO├── MultipeerConnectivity├── NaturalLanguage├── Network├── NetworkExtension├── NewsstandKit├── NotificationCenter├── OpenAL├── OpenGLES├── OpenGLES2├── OpenGLES3├── OpenGLESCommon├── PDFKit├── PassKit├── Photos├── PhotosUI├── PushKit├── QuartzCore├── QuickLook├── ReplayKit├── SafariServices├── SceneKit├── Security├── Social├── Speech├── SpriteKit├── StoreKit├── SystemConfiguration├── Twitter├── UIKit├── UserNotifications├── UserNotificationsUI├── VideoSubscriberAccount├── VideoToolbox├── Vision├── WatchConnectivity├── WatchKit├── WebKit├── builtin├── darwin├── iAd├── iconv├── ios_arm64.tree.txt├── objc├── posix└── zlib

116 directories, 1 file

Kotlin Native 與 Swift/Objective-C 雙向互操作

基於cinteroop,增加了面向對象的映射。細節詳見官網文檔:

https://kotlinlang.org/docs/reference/native/objc_interop.html#mappings

Kotlin 1.3 重新設計了多平臺工程項目架構,以提高工程結構的靈活性和擴展性,更容易共享復用Kotlin代碼。

Kotlin Native 變成 Kotlin Multiplatfrom 的目標平臺之一, 相關庫和插件轉為內部實現。

例如對於應用程式開發人員使用的Gradle插件,從org.jetbrains.kotlin.konan 變更為

 org.jetbrains.kotlin.multiplatform。

konan 變成 multiplatform 內部引用的依賴庫,創建Kotlin Gradle工程時後臺自動下載並保持在本機 ~/.konan/ 文件夾。

為了減少開發人員的誤解,對外提供的SDK版本號都統一跟隨Kotlin語言版本號。

例如,Kotlin語言最新版本是1.3.31,各平臺庫SDK版本號也一樣,而 kotlin native macos 1.2.1 僅用於Kotlin內部開發人員的版本Tag,對使用者透明,我們無需關心。

因此網上搜索得到的大部分基於konan的文章教程和GitHub源碼均已過時,需留意gradle配置中是否基於multiplatform plugin。包括官網部分文檔。

IntelliJ IDEA 提供了 Kotlin Mulitplatform的工程模版。實際上IDEA + Android SDK 可以替代Android Studio 99%的開發工作。針對Native平臺有4種模版,大同小異,區別僅是Gradle Modue結構略有不同。

1)Kotlin/Native 模版是針對單一平臺的最小化工程模版。

2)Kotlin (Mobile Android/iOS) 模版沿用Android工程的默認結構,將Android主工程,Kotlin Common代碼集放在root/app/src,根目錄額外增加了一個iOS主工程文件夾。

3)Kotlin (Multiplatform Library) 和 Kotlin (Mobile Shared Library)  非常相似,可以簡單的認為後者是前者的子集。

前者包含Kotlin/JS和3個Native (macOS,Windows,Linux) 平臺。

後者僅包含Android,iOS 2個平臺。

其中僅有一處細微差異。前者jvmMain模塊依賴stdlib-jdk8,後者jvmMain模塊依賴stdlib。即前者JVM運行環境是Java服務端,後者JVM運行環境是Android設備。

4)Kotlin(Mobile Shared Library) 是最簡結構,文件夾如下。

├── build.gradle├── gradle│   └── wrapper│       └── gradle-wrapper.properties├── gradle.properties├── settings.gradle└── src    ├── commonMain    │   ├── kotlin    │   │   └── sample    │   │       └── Sample.kt    │   └── resources    ├── commonTest    │   ├── kotlin    │   │   └── sample    │   │       └── SampleTests.kt    │   └── resources    ├── iosMain    │   ├── kotlin    │   │   └── sample    │   │       └── SampleIos.kt    │   └── resources    ├── iosTest    │   ├── kotlin    │   │   └── sample    │   │       └── SampleTestsNative.kt    │   └── resources    ├── jvmMain    │   ├── kotlin    │   │   └── sample    │   │       └── SampleJvm.kt    │   └── resources    └── jvmTest        ├── kotlin        │   └── sample        │       └── SampleTestsJVM.kt        └── resources

27 directories, 10 files

接下來我們需要首先解決一些平臺相關的上手問題,結合一些簡單且實際存在的小場景,探索Kotlin Multiplatform的實踐現狀。

列印輸出日誌是任何平臺和技術棧的開發人員每天面對的簡單且必需的功能。我們首先看看各平臺的基礎方案。大致都是定義好日誌級別,提供出全局單例或靜態方法API。

6.1 Java Logger


Since Java 1.4

import java.util.logging.Logger

private val logger = Logger.getLogger("Module")

fun javaLog() { logger.fine("Fine Msg") logger.config("Config Msg") logger.info("Info Msg") logger.warning("Warning Msg") logger.severe("Error Msg")}

Since API Level 1

import android.util.Log

private val tag = "Module"

fun androidLog() { Log.v(tag, "Verbose Msg") Log.d(tag, "Debug Msg") Log.i(tag, "Info Msg") Log.w(tag, "Warning Msg") Log.e(tag, "Error Msg")}

實際在Android日常開發中,我個人更傾向於使用Java Logger,相比Android.util.Log,Java Logger可以通過註冊 ConsoleHandler, FileHandler, SockeHandler, StreamHandler,配合 SimpleFormatter, XMLFormatter,將日誌轉存到手機本地文件或後端伺服器,供後續分析,以及每一行日誌代碼可以少寫一個Tag參數。

忽略 C print 和 Objective-C NSLog,僅看 iOS 10提供的 unified logging system。

void iOSLog(){    os_log(OS_LOG_DEFAULT, "Msg");    os_log_fault(OS_LOG_DEFAULT, "Fault Msg");    os_log_error(OS_LOG_DEFAULT, "Error Msg");    os_log_info(OS_LOG_DEFAULT, "Info Msg");    os_log_debug(OS_LOG_DEFAULT, "Debug Msg");    os_log_with_type(OS_LOG_DEFAULT, OS_LOG_TYPE_DEFAULT, "Msg with type");}

左右滑動可查看全部

6.4 Kotlin Log - Common Expect

參考 android.util.Log,複製一份 Kotlin Common 模塊的聲明。

跨平臺公共模塊的實現,除了使用常規的 Interface/Implementation 方案,Kotlin 提供了 expect/actual 聲明語法。

這裡使用 Expect 關鍵字聲明一個單例Log對象的預期。其類成員方法等同於Java的純虛方法。

/** * Keep consistent with android.utl.log constant level. * */enum class Level(val value: Int) {    VERBOSE(2),    DEBUG(3),    INFO(4),    WARN(5),    ERROR(6),    ASSERT(7)}

expect object Log { fun isLoggable(tag: String, level: Level): Boolean fun v(tag: String, msg: String) fun d(tag: String, msg: String) fun i(tag: String, msg: String) fun w(tag: String, msg: String) fun e(tag: String, msg: String) fun wtf(tag: String, msg: String)}

6.5 Kotlin Log - Android Actual

Android平臺的實現直接綁定android.util.Log。

// Kotlin Android Implementationactual object Log {    actual fun isLoggable(tag: String, level: Level): Boolean = android.util.Log.isLoggable(tag, level.value)    actual fun v(tag: String, msg: String) { android.util.Log.v(tag, msg) }    actual fun d(tag: String, msg: String) { android.util.Log.d(tag, msg) }    actual fun i(tag: String, msg: String) { android.util.Log.i(tag, msg) }    actual fun w(tag: String, msg: String) { android.util.Log.w(tag, msg) }    actual fun e(tag: String, msg: String) { android.util.Log.e(tag, msg) }    actual fun wtf(tag: String, msg: String) { android.util.Log.wtf(tag, msg) }}

左右滑動可查看全部

6.6 Kotlin Log - iOS Actual

由於cinterop工具僅處理C庫的實例和函數的綁定,不能實現 macro宏定義的綁定。而os_log()提供的常用API實際是 macro宏定義,所以我們需要找到其內部實際調用的函數 _os_log_internal()。這對新手可能是一個小坑,在未詳細了解平臺API的情況下,在Kotlin平臺庫中費時費力查找綁定方法無果。

// <os/log.h> declaration#define os_log(log, format, ...) \        os_log_with_type(log, OS_LOG_TYPE_DEFAULT, format, 

#define os_log_with_type(log, type, format, ...) __extension__({ \ _Pragma("clang diagnostic push") \ _Pragma("clang diagnostic error \"-Wformat\"") \ _Static_assert(__builtin_constant_p(format), "format argument must be a string constant"); \ _os_log_internal(&__dso_handle, log, type, format, _Pragma("clang diagnostic pop") \})

左右滑動可查看全部

_os_log_internal() 的第一個參數 __dso_handle 定義如下。這裡有趣了,我們需要調用它的內存地址指針,kotlinx.cinterop.ptr就是為此而生。

extern struct mach_header __dso_handle;

左右滑動可查看全部

下面這段實現是純Kotlin語言代碼,是上文中 Kotlin Common 的Log單例對象 expect 聲明對應的 actual實現。調用 Kotlin Native iOS平臺庫已提供好的 os_log API綁定。

// Kotlin iOS Implementationimport kotlinx.cinterop.ptrimport platform.darwin.*

actual object Log { actual fun isLoggable(tag: String, level: Level): Boolean = os_log_type_enabled(OS_LOG_DEFAULT, level.toPlatform()) actual fun v(tag: String, msg: String) = _os_log_internal(__dso_handle.ptr, OS_LOG_DEFAULT, OS_LOG_TYPE_DEFAULT, "$tag | $msg") actual fun d(tag: String, msg: String) = _os_log_internal(__dso_handle.ptr, OS_LOG_DEFAULT, OS_LOG_TYPE_DEBUG, "$tag | $msg") actual fun i(tag: String, msg: String) = _os_log_internal(__dso_handle.ptr, OS_LOG_DEFAULT, OS_LOG_TYPE_INFO, "$tag | $msg") actual fun w(tag: String, msg: String) = _os_log_internal(__dso_handle.ptr, OS_LOG_DEFAULT, OS_LOG_TYPE_INFO, "$tag | $msg") actual fun e(tag: String, msg: String) = _os_log_internal(__dso_handle.ptr, OS_LOG_DEFAULT, OS_LOG_TYPE_ERROR, "$tag | $msg") actual fun wtf(tag: String, msg: String) = _os_log_internal(__dso_handle.ptr, OS_LOG_DEFAULT, OS_LOG_TYPE_FAULT, "$tag | $msg")}

左右滑動可查看全部

6.7 Kotlin Log - AndroidNativeArm Actual

大部分業務場景中,我們使用Kotlin/JVM實現Android平臺的功能,

Kotlin Native for AndroidNativeArm32/64 可用但仍不好用。我們簡單看看其實現。

與iOS Native類似,只需在build.gradle文件中添加相應Target。這裡使用了Gradle Kotlin DSL新版本。使用Kotlin編寫Gradle配置文件的體驗比Groovy更佳,IDE支持語法高亮,自動補全,代碼跳轉,編譯提示等便捷功能。

androidNativeArm32() {        binaries {            sharedLib()// or staticLib() or executable()        }    }

Android NDK Log API

/** * Writes the constant string `text` to the log, with priority `prio` and tag * `tag`. */int __android_log_write(int prio, const char* tag, const char* text);

/** * Writes a formatted string to the log, with priority `prio` and tag `tag`. * The details of formatting are the same as for * [printf(3)](http://man7.org/linux/man-pages/man3/printf.3.html). */int __android_log_print(int prio, const char* tag, const char* fmt, ...)#if defined(__GNUC__) __attribute__((__format__(printf, 3, 4)))#endif ;

左右滑動可查看全部

繼續純Kotlin語言實現。

// Kotlin AndroidNativeArm Implementationimport platform.android.*

@kotlin.ExperimentalUnsignedTypesactual object Log { actual fun isLoggable(tag: String, level: Level): Boolean = level >= Level.INFO

actual fun v(tag: String, msg: String) { __android_log_write(ANDROID_LOG_VERBOSE.toInt(), tag, msg) } actual fun d(tag: String, msg: String) { __android_log_write(ANDROID_LOG_DEBUG.toInt(), tag, msg) } actual fun i(tag: String, msg: String) { __android_log_write(ANDROID_LOG_INFO.toInt(), tag, msg) } actual fun w(tag: String, msg: String) { __android_log_write(ANDROID_LOG_WARN.toInt(), tag, msg) } actual fun e(tag: String, msg: String) { __android_log_write(ANDROID_LOG_ERROR.toInt(), tag, msg) } actual fun wtf(tag: String, msg: String) { __android_log_write(ANDROID_LOG_FATAL.toInt(), tag, msg) }}

左右滑動可查看全部

以上Demo源碼工程,詳見:

https://github.com/9468305/log-kotlin

實際生產環境使用,推薦 Jake Wharton's Timber:

https://github.com/JakeWharton/timber

本地文件讀寫相比列印輸出日誌略複雜但也很常用。Android/JVM 和 Java 服務端的 IO File 技術方案一致但場景選型不同。

Java IO (Blocking IO)

Default IO Streaming.

Java NIO (Non-Blocking IO)

Since 1.4.

Java NIO2 (Asynchronous I/O, AIO)

Since 7, Enhancements in 8.

移動端常用 Blocking IO,一方面是因為該方案適合嵌入式平臺,另一方面是因為Android系統版本對JDK高版本的支持更新進展緩慢。長期以來我們需要兼容JDK 6,最低系統版本升級至Android 4.4(API Level 19)以上才能夠兼容JDK 7,Android 8.0(API Level 26)才升級至JDK 8,並且僅支持部分功能API。

Android NIO實際廣泛應用於網絡組件的實現,例如Google Guava,Square OKHttp。

iOS File 就是C Posix 使用方式,這裡不再贅述。

下面這段代碼使用純Kotlin語言調用iOS平臺POSIX File API。memScoped{}表示該作用域內的申請的內存空間,當離開作用域後,會被自動釋放。這是Kotlin Native不依賴JVM GC的內存管理方式,即ARC自動引用計數。

fun sample() {    val file = fopen(__filename = "filename", __mode = "r")    if (file != null) {        try {            memScoped {                 val bufferLength = 1024                val buffer = allocArray<ByteVar>(bufferLength)                while (true) {                    val line = fgets(buffer, bufferLength, file)?.toKString()                    if (line == null || line.isEmpty())                        break                    println(line)                }            }        } finally {            fclose(file)        }    }}

左右滑動可查看全部

那麼如何Kotlin Multiplatform實現 java.io.file 與 C POSIX File API的統一?我們看看官方現狀。

Package kotlin.io for native 僅有3個方法。

// Prints the given message to the standard output stream.fun print()// Prints the given message and the line separator to the standard output stream.fun println()// Reads a line of input from the standard input stream.fun readLine(): String?

左右滑動可查看全部

Package kotlinx.io 基於NIO方案實現,目前仍處於Experimental階段,官方建議配合 kotlinx.coroutines, kotlinx.atomicfu 一起使用,尚未支持Native平臺。

所以目前我們只能自己實現雙平臺的統一封裝。這部分實現並不難,可參考 OpenJDK 和 AOSP源碼。Java File底層實現原理也是通過 JNI 調用 C POSIX。Android 源碼部分改寫了OpenJDK的實現。具體細節詳見Android SDK FileInputStream/FileOutputSteam源碼。

另外 Okio 2 正在進行遷移至Kotlin和支持多平臺,square團隊的最終目標是將Retrofit和OkHttp運行在多平臺。詳見:

https://github.com/square/okio/issues/370

嵌入式平臺主流關係型數據存儲方案。

1、Android SQLiteOpenHelper

2、Android Jetpack Room

Jetpack 新組件。

SQLite 之上的 ORM 抽象層。

3、iOS SQLite library

SQLDelight

https://github.com/square/sqldelight

目前最成熟穩定的 Kotlin 多平臺 SQLite解決方案。作者 Alec Strong, Jake Wharton(又見大神)。不論是Android Java 開發,還是Kotlin多平臺開發,我都建議大家了解一下它。

它的思路非常有趣,與Room為代表的各種ORM方案截然相反。它是從SQL查詢語句生成代碼,而不是從代碼生成SQL查詢。這裡不展開介紹,直接放上Jake Wharton關於SQLDelight vs Room的評論原文。

In my opinion, Room exists at the wrong level of abstraction.

The reason Retrofit and Gson/Moshi/etc. are successful is because there's nothing from which to generate the interfaces and model objects so you write both by hand duplicating an implicit contract. If you switch to protocol buffers, you stop writing models by hand–the tool can generate those. If you switch to gRPC or Swagger, you stop writing Retrofit interfaces by hand–a tool can generate those. When you have an explicit source of schema you no longer need to duplicate that schema by hand in code.

SQL table definitions and queries are a schema. They can define the types and names of both the model objects and the interface through which you interact with queries. Thus, it doesn't really make sense to force the user to duplicate that schema by writing the model objects and interface by hand. You wouldn't do it with protobuf and gRPC. Why are you doing it with your database?

Room is an okay choice. It's far better than all the ORMs people have been using for years. Alec and I have given talks where the conclusion was that we don't care which you choose, just don't choose an ORM (https://youtu.be/4eUuD7LsqMs).

That being said, it's hard not to see SQLDelight as a step up from Room. It validates more. It has better tooling. It generates Kotlin. And it's multiplatform. Yes, I'm biased, but Alec can tell you how long we spent evaluating what the right level of abstraction is and what the right developer UX is. It's a pleasant experience writing only SQL and having SQLDelight generate the interfaces and model objects that you'd otherwise be forced to write by hand with Room. And when you do that, you also stop (ab)using star selects in your queries and selecting only the minimal amount of columns necessary rather than selecting entire tables so you can reuse entities.

You write the SQL query and you write the method signature. Generating the method signature is a pure function from the SQL query. You have to keep both in sync manually instead of having the method generated automatically based on the SQL bind args and selected columns. SQLDelight generates the interface methods that Room makes you write given the same SQL command.

Similarly, when you write a query that returns results, Room forces you to write a model object which conforms to the selected data. SQLDelight generates the model objects that Room makes you write given the same SQL query.

In summary, you can take all the SQL you're already using with Room, delete all of the interfaces and model objects you had to write manually, and SQLDelight will generate them for you (along with some extra validation that they're correct).

Kotlin解決方案組件的穩定性和進展,詳見:

https://kotlinlang.org/docs/reference/evolution/components-stability.html

Kotlin Native 處於 Additions in Incremental Releases (AIR) 階段。

Multiplatform Projects 處於 Moving fast (MF) 階段。

前不久的Google IO 2019大會上,Kotlin語言在Android平臺的地位進一步上升。Android Jetpack 系列組件優先支持Kotlin。Square的Okio 2,OkHttp 4.0 正在遷移Kotlin並支持多平臺。

所以我相信 Kotlin Multiplatform 的未來充滿想像力。

https://gradle.org/kotlin/

Gradle 5.0 已發布 Kotlin DSL v1.0 穩定版,建議儘早遷移 Gradle工程至 KTS 版本。



*本文由「攜程技術中心」整理編撰,感謝授權轉載

本文觀點不代表我方觀點

推薦閱讀

相關焦點

  • 攜程機票網app下載
    攜程機票網app為用戶提供非常豐富的旅遊景點資訊攻略,還可以為用戶提供優質的機票、火車票、酒店等等配套的優質服務,無論你是國內遊,還是出國遊,都是你出行旅遊必備的軟體。
  • 乾貨 | 攜程機票前臺埋點二三事
    攜程機票埋點隨著業務複雜度的增加而在做加法,先後上的埋點包括ctm、action、trace、pv、服務端埋點等五個大類,每個埋點均符合其時代屬性,但現在規整起來其相互間存在一定的交叉,即使冗餘但有些埋點一部分還存在價值,轉移起來造成的數據問題誰都不想背鍋,所以埋點一直在做加法。直至在app減size的大趨勢下,才順便把無效的埋點做部分清理。
  • 攜程機票網官網下載
    攜程機票網官網下載簡介:攜程機票網為用戶提供非常豐富的旅遊景點資訊攻略,還可以為用戶提供優質的機票、火車票、酒店等等配套的優質服務攜程機票網官網下載功能:- 酒店:國內93000餘家酒店和客棧,海外419000餘家酒店,暢享團購酒店、惠選酒店、今夜特價酒店、手機專享價酒店等多種預訂方式;- 機票:首單立減10元,提供國內所有航線與國際主流航線單程機票、往返機票;在線值機、選座;- 火車票:高鐵動車、普通列車等各種車次火車票,支持國際火車票通票預訂
  • 哪個app訂機票最便宜
    很多人說坐飛機很貴,買不到便宜的機票,今天我們就來說說哪個app訂機票最便宜。    網上訂機票的app有:民航在線,攜程,去哪,飛豬,美團,航空公司官網等等很多的,價格其實都差不多,不存在哪個網訂機票特別便宜,關鍵是安全可靠。旅客可關注公眾號:民航微出行 ,國內國際機票實時在線查詢預訂,人工客服24小時在線服務。
  • 機票app哪個好用?8款特價機票app下載推薦
    當然,很少買機票的朋友就在問小編:機票app哪個好用?哪款機票app是走特價路線呢?針對這個問題,小編今天就給大家帶整理幾款不錯的機票app,感興趣的就來看看吧。  一、攜程旅行攜程旅行  攜程旅行網手機版來自全球知名的在線旅行票務服務平臺官方推出手機客戶端,攜程旅行網手機版國內最好的在線出行平臺,攜程旅行網手機版無論是訂酒店還是全國各地的景點門票以及出行車票和飛機票神馬的,攜程旅行網手機版幫助你搞定一站式出行服務!
  • 攜程app引領機票預訂進入讀秒時代
    當時,國內在線支付手段與物流配送網絡均不發達,許多用戶都是通過呼叫中心預訂機票,然後等待攜程派人上門收取票款,給予票證。為此,攜程專門建立了一支自有的機票配送團隊,為用戶出行帶來了極大便利。  然而時過境遷,經過十幾年的發展變革,民航業早已進入了電子機票時代,加上在線支付全面普及,消費者預訂機票已沒有現金上門支付和乘機前配送等需求。
  • 攜程五一機票漲價 國際航線機票搜索量更是上漲達10倍
    攜程五一機票漲價 國際航線機票搜索量更是上漲達10倍時間:2019-03-23 14:27   來源:勁彪新聞客戶端   責任編輯:凌君 川北在線核心提示:原標題:攜程五一機票漲價 國際航線機票搜索量更是上漲達10倍 勁彪新聞客戶端 所有人都被五一放4天刷屏了!
  • 乾貨!最全超實用旅行APP匯總
    ,主要提供機票航班的比價服務。你可以選定一個地點和大概的時間段,就可以查到這段時間內最低的機票了。攜程、飛豬攜程和飛豬現在提供的服務不僅僅僅限於購買機票酒店之類的了。還包括租車、旅遊攻略、定製化旅遊、委託辦理籤證保險之類的。基本上只要你能想到的旅遊產品,攜程飛豬都有在做。
  • 明星韓雪炮轟後再測攜程機票預定:APP網頁端改版,搭售陷阱沒了
    直到網友大批卸載攜程,部分公司公開要求員工拒使用攜程訂票,以及10月9日晚間韓雪事件爆發,10月10日AI財經社發現攜程APP與網頁端都已改版,「默認搭售」已取消,並在頁面做了顯著的提示。韓雪炮轟攜程;再登app發現頁面已改版10月9日晚上九點四十,演員韓雪在微博上發博控訴攜程,「奉勸大家:攜程在手,看清楚再走。」
  • 機票比官網貴2100元, 攜程致歉稱系統出了BUG
    今天因疑似再次大數據殺熟攜程上熱搜了…網友稱機票價格比航空公司官網還要貴¥2100元!昨天上午10:47分自己正使用攜程app準備購買機票選擇完畢後總價格為¥17548元發現沒有選報銷憑證於是退回去修正當再次進入支付界面時攜程app顯示,票已售罄
  • 攜程崩了你慌了,這9個出行必備APP你都裝了嗎?
    昨晚8:00多,攜程伺服器史上第3次大面積崩潰,網頁app小程序齊刷刷宕機直接登上了熱搜。去哪兒、同程、藝龍、智行火車票這些攜程系的平臺,也集體「崩潰」,這不就應了那句…一家人嘛就要崩得整整齊齊~~其實不怕攜程崩潰,就怕你訂酒店買機票只會上攜程。Appstore那麼大,小編帶你們去看看~這9個出行必備APP,你們可收好了!
  • 攜程發布無線APP提供國際航路機票查詢預訂
    【天極網家電頻道】通過手機就能夠便利的進行全世界機票查詢與預訂,隨時隨地想怎麼飛、就怎麼飛,這是現今旅客在移動網際網路環境的關鍵需求。近日,攜程旅行網發布了iPhone、Android版攜程無線APP為3.3版本,新加入了查詢、預訂國際機票的實用功能,為用戶提供了更豐富的選擇,繼續了國內旅遊業在移動網際網路領域的跨躍。
  • 攜程APP
    2010年,攜程旅行網戰略投資臺灣易遊網和香港永安旅遊,完成了兩岸三地的布局。2014年,投資途風旅行網,將觸角延伸及北美洲。作為中國領先的綜合性旅行服務公司,攜程成功整合了高科技產業與傳統旅行業,向超過2.5億會員提供集無線應用、酒店預訂、機票預訂、旅遊度假、商旅管理及旅遊資訊在內的全方位旅行服務,被譽為網際網路和傳統旅遊無縫結合的典範。
  • 乾貨|手把手教你刷廉價機票
    我通常的做法是,先看看目前有哪些地方在做特價活動,世界辣麼大,絕大多數地方都沒有去過,對哪兒都是有興趣的呀,況且攜程、去哪兒都在首頁單獨列出了「特價機票」的欄目,哪兒便宜就去哪兒唄。另外,天巡app還會根據你的歷史搜索記錄,來匹配現有的高性價比機票,來猜你可能會喜歡的選擇。點進去之後會有該目的地未來半年或某個特定月份的最低價可供選擇,簡直不能更棒呀!像這樣👇:
  • 朋友圈刷屏五一放假4天消息和攜程五一機票漲價
    昨天五一放假四天的消息一發布,健叔就是迫不及待的打開了攜程看看那裡有特價機票,計劃五一出去玩玩,放鬆放鬆,突然發現前些天有留意的特價機票部分漲價近兩千元,很多做微商的寶媽們也在朋友圈抱怨這難得想和老公出去玩耍,健叔的朋友圈幾乎被這兩條消息刷屏為此,健叔特地上網查了下。
  • 特價機票APP神器推薦!
    活動也比較多,很多時候有大額機票紅包,還有1元秒殺機票的活動!訂票的時候也沒有捆綁營銷,乾淨利落!,對比各平臺的價格(同城,途牛,攜程,飛豬)自動對比價格,而且還定時有活動,獎勵紅包,買機票可以使用活動的紅包
  • 乾貨 | 叮~快來接住這份旅遊必備APP名單
    說到旅行APP的話第一時間肯定會想到住宿和攻略相關~  同類型app有很多,今天就著重介紹經過大浪淘沙留下的一些實用和常用的旅行app。 首先出行第一步,確定目的地訂機票,參考👉攻略 | 怎麼樣買到最優惠的機票 (之前專門寫的一篇關於訂機票的文章,訂機票app和tips~)機票買好了第二就是住宿。
  • 攜程「殺熟」,怎樣訂機票酒店更合適?
    近日,攜程「殺熟」又成為熱門話題。因網友@黑白影 發布的一條微博,眾多網友對攜程口誅筆伐。隨後,攜程回應稱,「酒店同方不同價並非殺熟,用戶所看到的差異,有的是因為用戶領用或購買優惠券造成,有的本身就不是同一種產品。」並稱這件事給了他們警示,下一步攜程將會優化產品設計和頁面展示,給用戶儘量好的體驗。
  • 攜程機票又現「坑」,網上預訂機票一定要注意這些!
    近日,上海市第一中級人民法院依法作出終審判決,認定攜程公司行為構成欺詐,判決其依法賠償韓先生機票退款三倍計1110元。這件事的經過是醬紫的:2015年8月15日,韓先生在攜程公司經營的「攜程旅行網」下單購買機票。
  • 攜程「假票門」事件引發網購機票信任危機 哪裡買靠譜?
    四川在線消息 本周,中國最大的在線旅遊網站攜程銷售無效機票事件火遍網絡,成為為旅遊業界最受關注的話題目。隨著不斷有網友吐槽,業內人士加入分析,事件熱度一再攀升,牽連出了網上訂購機票的一系列貓膩。網友曝訂票貓膩攜程稱問題率萬分之二數日前,微博用戶TTDZ在網上曬出在攜程訂票遇麻煩的經歷:其在攜程買了國際機票後,在國外登機時卻被認定無效,並被警察要求配合調查,原因是該機票涉嫌轉賣積分而獲得,盜客人積分換票再把這樣的票按正常票價轉賣給客人,並導致乘客耽擱境外的行為引起了網友們的共鳴,隨後許多網友分享了自己在攜程購票中遇到的類似經歷:網上付完款出了票號後卻發現票號是假的