Kotlin Compose Multiplatform 跨平台开发实践之加入 iOS 支持

前言

几个月前 Compose Multiplatform 的 iOS 支持就宣布进入了 Alpha 阶段,这意味着它已经具备了一定的可用性。

在它发布 Alpha 的时候,我就第一时间尝鲜,但是只是浅尝辄止,没有做过多的探索,最近恰好有点时间,于是我又重新开始学习 Compose Multiplatform ,并且尝试移植我已有的项目使其支持 iOS,并且将移植过程整理记录了下来,即为本文。

这次移植我选择的依旧是这个使用 Compose 写的计算器项目 calculator-Compose-MultiPlatform 。本来这次我想着移植一个涉及技术稍微多一点的项目的比如这个 githubAppByCompose,但是我仔细研究了一下,毕竟现在 Compose Multiplatform 还处于实验阶段,好多对应的功能和库都还没有,所以只能选择移植前者。

对于这个计算器项目,最开始只是一个使用 Compose 实现的纯 Android 项目,后来移植到了支持 Android 和 桌面 端,所以其实现在再给它添加上 iOS 支持,也算是补齐了最后一个平台了,哈哈。

在开始阅读本文之前,我会假设你已经了解并且知道 Compsoe 的基本使用方法。

为了更好的理解本文,可能需要首先阅读这两篇前置文章:

  1. 【译】快速开始 Compose 跨平台项目
  2. Kotlin & Compose Multiplatform 跨平台(Android端、桌面端)开发实践之使用 SQLDelight 将数据储存至数据库

前言的最后看一下运行效果:

Android 端:

2.png

ios 端:

4.png

桌面端:

3.png

开始移植

准备工作

首当其冲,我们需要为 iOS 的支持更改编译配置文件和添加对应的平台特定代码。

在我的这个项目中,我通过以下几个步骤为其添加了对 iOS 的支持:

更改共享代码模块名称

把公用代码模块由 common 改为 shared ,其实这里不用改也行,只是模板配置文件中写的 iOS 使用的公用代码路径是 shared ,但是直接改模块名比改配置文件简单多了,所以我们直接把模块名改了就好了。

改完之后切记要检查一下其他模块引用的名字是否改了,以及注意检查一下包名是否正确。

添加 native.cocoapods 插件

shared 模块的 build.gradle.kts 文件的 plugins 增加 native.cocoapods 插件:

plugins {kotlin("native.cocoapods")// ……
}
添加 cocoapods 配置

shared 模块的 build.gradle.kts 文件的 kotlin 下增加 cocoapods 相应的配置:

kotlin {// ……iosX64()iosArm64()iosSimulatorArm64()cocoapods {version = "1.0.0"summary = "Some description for the Shared Module"homepage = "Link to the Shared Module homepage"ios.deploymentTarget = "14.1"podfile = project.file("../iosApp/Podfile")framework {baseName = "shared"isStatic = true}extraSpecAttributes["resources"] = "['src/commonMain/resources/**', 'src/iosMain/resources/**']"}// ……
}
配置 iOS 源集

shared 模块的 build.gradle.kts 文件的 kotlin 中的 sourceSets 下增加 iOS 的源集配置:

kotlin {// ……sourceSets {// ……val iosX64Main by gettingval iosArm64Main by gettingval iosSimulatorArm64Main by gettingval iosMain by creating {dependsOn(commonMain)iosX64Main.dependsOn(this)iosArm64Main.dependsOn(this)iosSimulatorArm64Main.dependsOn(this)}}
}
添加其他插件

在项目根目录下的 settings.gradle.kts 文件的 pluginManagement 中的 plugins 增加插件配置:

pluginManagement {//……plugins {kotlin("jvm").version(extra["kotlin.version"] as String)// ……}
}
添加 iOS 项目文件

直接把官方模板中的 iosAPP 模块整个目录复制到项目根目录来。

需要注意的是,其实这个 iosAPP 目录并不是一个 idea 模块,而是一个 Xcode 项目。但是目前暂时不需要知道这是什么,只需要把相应的文件整个复制到自己项目中就行了。

然后把官方模版中的 sahred -> iosMain 文件夹整个复制到 我们项目的 sahred 模块根目录中。

适配代码

在这一节中,主要需要适配的有两种类型的代码:

一是之前就已经在项目中声明了的 expect 函数,需要为 iOS 也加上对应的 actual 函数。

二是需要将原本使用到的 jvm 相关或者说所有使用 java 实现的库和相关代码都需要重新编写或适配。

因为不同于 Android 和 桌面端,kotlin 最终会被编译成 jvm 代码,在 iOS 端,kotlin 会编译成 native 代码,所以所有使用 java 写的代码将无法再使用。

这也就是我前言中说的为啥不选择移植更复杂的项目的原因,就是因为我在其中引用了大量的使用 java 编写的第三方库,而这些第三方库又暂时没有使用纯 kotlin 实现的可用替代品。

下面,我们就开始适配代码。

更改入口

为了保证三端界面一致,我们将原本的UI界面再额外的抽出一个统一的入口函数 APP(),将其放到 shared 模块的 common 包下:

@Composable
fun APP(standardChannelTop: Channel<StandardAction>? = null,programmerChannelTop: Channel<ProgrammerAction>? = null,
) {val homeChannel = remember { Channel<HomeAction>() }val homeFlow = remember(homeChannel) { homeChannel.consumeAsFlow() }val homeState = homePresenter(homeFlow)val standardChannel = standardChannelTop ?: remember { Channel() }val standardFlow = remember(standardChannel) { standardChannel.consumeAsFlow() }val standardState = standardPresenter(standardFlow)val programmerChannel = programmerChannelTop ?: remember { Channel() }val programmerFlow = remember(programmerChannel) { programmerChannel.consumeAsFlow() }val programmerState = programmerPresenter(programmerFlow)CalculatorComposeTheme {val backgroundColor = MaterialTheme.colors.backgroundSurface(modifier = Modifier.fillMaxSize(),color = backgroundColor) {HomeScreen(homeChannel,homeState,standardChannel,standardState,programmerChannel,programmerState)}}
}

并且,因为不同平台需要差异化实现部分功能,以及目前我还没找到一个好使的支持跨平台的依赖注入库,所以我索性将所有 控制(channel) 和 状态(state) 都提升到了最顶层,作为参数传递给下面的 Compose 函数。

然后,更改三端各自的入口函数:

Android (android 模块下的 MainActivity.kt 文件)
class MainActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContent {APP()}}
}
desktop (dektop 模块下的 Main.kt 文件)
fun main() = application {val state = if (Config.boardType.value == KeyboardTypeStandard) {rememberWindowState(size = defaultWindowSize, position = defaultWindowPosition)} else {rememberWindowState(size = landWindowSize, position = defaultWindowPosition)}val standardChannel = remember { Channel<StandardAction>() }val programmerChannel = remember { Channel<ProgrammerAction>() }Window(onCloseRequest = ::exitApplication,state = state,title = Text.AppName,icon = painterResource("icon.png"),alwaysOnTop = Config.isFloat.value,onKeyEvent = {if (isKeyTyped(it)) {val btnIndex = asciiCode2BtnIndex(it.utf16CodePoint)if (btnIndex != -1) {if (Config.boardType.value == KeyboardTypeStandard) {standardChannel.trySend(StandardAction.ClickBtn(btnIndex))}else {programmerChannel.trySend(ProgrammerAction.ClickBtn(btnIndex))}}}true}) {APP()}
}
iOS ( shared模块 下的 main.ios.kt 文件)
fun MainViewController() = ComposeUIViewController {APP()
}

注意,不同于其他平台,iOS 的入口函数在 shared模块 中。

当然,你要是想直接改 iosAPP 目录中的代码,那也不是不行,只是对于我们安卓开发来说,还是直接改 shared 更方便点。

实现 iOS 的 平台代码

之前我们的项目中有几个地方的实现依赖于平台,所以写了一些 expect 函数,现在我们需要给 iOS 实现对应的 actual 函数。

首先在 shared 模块的 iosMain 包中创建一个包路径,保持和 commonMainexpect 函数包一致:

1.png

注意: 包路径一定要一致,不然会编译失败,我就在这里踩了坑,没注意到包名不一样, debug 了好久。

这个项目中的平台差异函数主要有四个:

控制振动

因为我对 iOS 一窍不通,所以不知道怎么写,索性直接留空了:

actual fun vibrateOnClick() {}actual fun vibrateOnError() {}actual fun vibrateOnClear() {}actual fun vibrateOnEqual() {}

控制屏幕旋转和显示小窗

这里同上,不知道怎么写,直接留空:

actual fun showFloatWindows() {}actual fun changeKeyBoardType(changeTo: Int) {}

数据库(sqldelight)

actual fun createDriver(): SqlDriver {return NativeSqliteDriver(HistoryDatabase.Schema, "history.db")
}

关于使用 sqldelight 的详细介绍,可以看前言中的前置文章了解。

其实这里这样写是编译不通过的,因为还没加 sqldelight 依赖,下面介绍一下怎么加依赖,这里又是一个大坑。

给 iOS 添加 sqldelight 支持

首先,在 shared 模块下的 build.gradle.kts 文件中的 kotlin -> sourceSets -> iosMain 添加 sqldelight 的 驱动依赖:

kotlin {// ……sourceSets {// ……val iosMain by creating {// ……dependencies {implementation("app.cash.sqldelight:native-driver:2.0.0")}}}
}

此时如果你直接 sync gradle 后编译运行,大概率会报错:

Undefined symbols for architecture arm64:
"_sqlite3_bind_text16", referenced from:
_SQLiter_SQLiteStatement_nativeBindString in app(combined.o)
"_sqlite3_bind_int64", referenced from:
_SQLiter_SQLiteStatement_nativeBindLong in app(combined.o)
"_sqlite3_last_insert_rowid", referenced from:
_SQLiter_SQLiteStatement_nativeExecuteForLastInsertedRowId in app(combined.o)
"_sqlite3_reset", referenced from:
_SQLiter_SQLiteConnection_nativeResetStatement in app(combined.o)
"_sqlite3_changes", referenced from:
_SQLiter_SQLiteStatement_nativeExecuteForChangedRowCount in app(combined.o)
_SQLiter_SQLiteStatement_nativeExecuteForLastInsertedRowId in app(combined.o)
"_sqlite3_open_v2", referenced from:
_SQLiter_SQLiteConnection_nativeOpen in app(combined.o)
"_sqlite3_db_config", referenced from:
_SQLiter_SQLiteConnection_nativeOpen in app(combined.o)
"_sqlite3_busy_timeout", referenced from:
_SQLiter_SQLiteConnection_nativeOpen in app(combined.o)
"_sqlite3_trace", referenced from:
_SQLiter_SQLiteConnection_nativeOpen in app(combined.o)
"_sqlite3_bind_parameter_index", referenced from:
_SQLiter_SQLiteConnection_nativeBindParameterIndex in app(combined.o)
"_sqlite3_column_bytes", referenced from:
_SQLiter_SQLiteConnection_nativeColumnGetString in app(combined.o)
_SQLiter_SQLiteConnection_nativeColumnGetBlob in app(combined.o)
"_sqlite3_finalize", referenced from:
_SQLiter_SQLiteStatement_nativeFinalizeStatement in app(combined.o)
"_sqlite3_column_text", referenced from:
_SQLiter_SQLiteConnection_nativeColumnGetString in app(combined.o)
"_sqlite3_column_name", referenced from:
_SQLiter_SQLiteConnection_nativeColumnName in app(combined.o)
"_sqlite3_bind_double", referenced from:
_SQLiter_SQLiteStatement_nativeBindDouble in app(combined.o)
"_sqlite3_profile", referenced from:
_SQLiter_SQLiteConnection_nativeOpen in app(combined.o)
"_sqlite3_close", referenced from:
_SQLiter_SQLiteConnection_nativeClose in app(combined.o)
_SQLiter_SQLiteConnection_nativeOpen in app(combined.o)
"_sqlite3_prepare16_v2", referenced from:
_SQLiter_SQLiteConnection_nativePrepareStatement in app(combined.o)
"_sqlite3_column_type", referenced from:
_SQLiter_SQLiteConnection_nativeColumnIsNull in app(combined.o)
_SQLiter_SQLiteConnection_nativeColumnType in app(combined.o)
"_sqlite3_column_count", referenced from:
_SQLiter_SQLiteConnection_nativeColumnCount in app(combined.o)
"_sqlite3_bind_blob", referenced from:
_SQLiter_SQLiteStatement_nativeBindBlob in app(combined.o)
"_sqlite3_db_readonly", referenced from:
_SQLiter_SQLiteConnection_nativeOpen in app(combined.o)
"_sqlite3_column_int64", referenced from:
_SQLiter_SQLiteConnection_nativeColumnGetLong in app(combined.o)
"_sqlite3_bind_null", referenced from:
_SQLiter_SQLiteStatement_nativeBindNull in app(combined.o)
"_sqlite3_extended_errcode", referenced from:
android::throw_sqlite3_exception(sqlite3*) in app(combined.o)
android::throw_sqlite3_exception(sqlite3*, char const*) in app(combined.o)
"_sqlite3_column_double", referenced from:
_SQLiter_SQLiteConnection_nativeColumnGetDouble in app(combined.o)
"_sqlite3_column_blob", referenced from:
_SQLiter_SQLiteConnection_nativeColumnGetBlob in app(combined.o)
"_sqlite3_step", referenced from:
_SQLiter_SQLiteConnection_nativeStep in app(combined.o)
_SQLiter_SQLiteStatement_nativeExecute in app(combined.o)
_SQLiter_SQLiteStatement_nativeExecuteForChangedRowCount in app(combined.o)
_SQLiter_SQLiteStatement_nativeExecuteForLastInsertedRowId in app(combined.o)
"_sqlite3_clear_bindings", referenced from:
_SQLiter_SQLiteConnection_nativeClearBindings in app(combined.o)
"_sqlite3_errmsg", referenced from:
android::throw_sqlite3_exception(sqlite3*) in app(combined.o)
android::throw_sqlite3_exception(sqlite3*, char const*) in app(combined.o)
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

这是因为 ios 的 Xcode 项目没有添加 sqlite 依赖,我们还需要为 ios 单独添加 sqlite 依赖。

ios 使用的是 cocoapods 进行依赖管理,我们需要使用 pod 添加依赖。

我们有两种选择:

一是在 shared 模块的 build.gradle.kts 中相应的位置添加 pod 依赖配置。

二是直接在 pod 配置文件中添加。

这里我们就选择直接改 pod 的配置文件。

打开项目根目录下的 iosAPP 目录中的 Podfile 文件,在其中添加 sqlite3 依赖:

target 'iosApp' do# ……pod 'sqlite3', '~> 3.42.0'# ……end

添加完记得需要 sync 一下 gradle。

此时再编译运行,大概率还是会报错:

ld: file not found: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/arc/libarclite_iphonesimulator.a

不用担心,再在刚才的配置文件中加上这么一段:

# iosApp's podfile
post_install do |installer|installer.pods_project.targets.each do |target|target.build_configurations.each do |config|config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '14.1'endend
end

此时应该就不会有任何问题了。

适配 jvm 相关代码

正如我们在上一节所说,由于 iOS 使用 native 代码,所以项目中就不能再使用 java 代码,包括引用的第三方库也是。

在我这个项目中涉及到需要适配的主要有两个地方。一个是进制转换时使用到了 java 的 Long 类的方法;另一个就是运算时使用的是 BigInteger BigDecimal

进制转换

之前的代码使用的是 java 中的 java.lang.Long.toXXXString

这里适配起来其实很简单,要么自己使用 kotlin 实现一个进制转换工具类,要么就像我一样,直接把 Long.java 中需要的部分 CV 一下,然后使用 Android studio 的 java 转 kotlin 一键转换就行了。

下面就是我转好的工具类:

package com.equationl.common.utilsimport kotlin.math.maxobject LongUtil {val digits = charArrayOf('0', '1', '2', '3', '4', '5','6', '7', '8', '9', 'a', 'b','c', 'd', 'e', 'f', 'g', 'h','i', 'j', 'k', 'l', 'm', 'n','o', 'p', 'q', 'r', 's', 't','u', 'v', 'w', 'x', 'y', 'z')fun toBinaryString(i: Long): String {return toUnsignedString0(i, 1)}fun toHexString(i: Long): String {return toUnsignedString0(i, 4)}fun toOctalString(i: Long): String {return toUnsignedString0(i, 3)}fun toUnsignedString0(`val`: Long, shift: Int): String {// assert shift > 0 && shift <=5 : "Illegal shift value";val mag: Int = Long.SIZE_BITS - numberOfLeadingZeros(`val`)val chars: Int = max((mag + (shift - 1)) / shift, 1)//if (COMPACT_STRINGS) {val buf = ByteArray(chars)formatUnsignedLong0(`val`, shift, buf, 0, chars)return buf.map { it.toInt().toChar() }.toCharArray().concatToString()
//        } else {
//            val buf = ByteArray(chars * 2)
//            java.lang.Long.formatUnsignedLong0UTF16(`val`, shift, buf, 0, chars)
//            return String(buf, UTF16)
//        }}private fun formatUnsignedLong0(`val`: Long,shift: Int,buf: ByteArray,offset: Int,len: Int) {var `val` = `val`var charPos = offset + lenval radix = 1 shl shiftval mask = radix - 1do {buf[--charPos] = digits[`val`.toInt() and mask].code.toByte()`val` = `val` ushr shift} while (charPos > offset)}fun numberOfLeadingZeros(i: Long): Int {val x = (i ushr 32).toInt()return if (x == 0) 32 + numberOfLeadingZeros(i.toInt()) else numberOfLeadingZeros(x)}fun numberOfLeadingZeros(i: Int): Int {// HD, Count leading 0'svar i = iif (i <= 0) return if (i == 0) 32 else 0var n = 31if (i >= 1 shl 16) {n -= 16i = i ushr 16}if (i >= 1 shl 8) {n -= 8i = i ushr 8}if (i >= 1 shl 4) {n -= 4i = i ushr 4}if (i >= 1 shl 2) {n -= 2i = i ushr 2}return n - (i ushr 1)}
}

然后更改我们的代码中使用到的地方即可,例如:

Long.toBinaryString 改为 LongUtil.toBinaryString(long)

记得把导入的包也改了:

import java.lang.Long 改为 import com.equationl.common.utils.LongUtil

当然,如果你的工具类直接取名叫 Long 的话,那么调用代码就不用改了,改导入包就行了。

BigInteger 和 BigDecimal

接下来就是 BigInteger 和 BigInteger,同样的思路,我们可以选择自己使用 kotlin 写一个功能相同的工具类,但是显然,这两个类可不同于进制转换,它涉及到的代码量可要大多了。

好在已经有大神写好了纯 kotlin 的支持跨平台的 BigInteger 和 BigDecimal: kotlin-multiplatform-bignum 。我们只需要简单的引用它就可以了。

shared 模块下的 build.gradle.kts 文件中的 kotlin -> sourceSets -> commonMain -> dependencies 添加依赖

kotlin {sourceSets {val commonMain by getting {dependencies {implementation("com.ionspin.kotlin:bignum:0.3.8")}}}
}

sync gradle 后,依次修改项目中使用到 BigInteger 和 BigDecimal 地方的代码即可。

需要注意的是,这个库的 API 和 java 的 BigInteger 以及 BigDecimal 并非完全一致,因此需要我们逐个检查并修改。

例如,在 java 的 BigDecimal 中,除法的 API 是: divide(BigDecimal divisor, int scale, RoundingMode roundingMode)

而在这个库中则变为了 divide(other: BigDecimal, decimalMode: DecimalMode? = null)

除此之外,还有一些小地方的代码可能引用的是 java 代码,这里就不再赘述了,按照上述两种思路逐个适配即可。

总结

自此,我们的项目就完全移植到了完整形态的 Compose Multiplatform 中了!现在它已经完全支持 Android、iOS 和 desktop 了!

不知道你们有没有发现,在全文中,我几乎都是在说怎么适配和移植逻辑代码,并没有说到有关 UI 的代码。

哈哈,不是因为我忘记说了,而是因为 Compose Multiplatform 代码真的做到了一套代码,多平台通用。新增加 iOS 支持完全不用动 UI 部分的代码。

完整项目代码: calculator-Compose-MultiPlatform

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/162557.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

计算机网络-计算机网络体系结构-网络层

目录 一、IPV4 IP数据报格式 *IP 数据报分片 *IPV4地址 分类 网络地址转换(NAT) 二、子网划分与子网掩码 *CIDR *超网 协议 ARP协议 DHCP协议 ICMP协议 三、IPV6 格式 IPV4和IPV6区别 地址表示形式 四、路由选择协议 RIP(路由信息协议) OPSF(开发最短路径优…

SpringBoot整合Caffeine实现缓存

Caffeine Caffeine是一种基于Java的高性能缓存库&#xff0c;它提供了可配置、快速、灵活的缓存实现。Caffeine具有以下特点&#xff1a; 高性能&#xff1a;Caffeine使用了一些优化技术&#xff0c;如基于链表的并发哈希表和无锁算法&#xff0c;以提供卓越的读写性能。容量…

nodejs+vue 学生宿舍管理系统设计与实现

可将教师信息、宿管信息、学生信息、楼栋信息等输入到系统中。只有管理员才能录入相关的资料&#xff0c;按照提示&#xff0c;输入相应的资料&#xff0c;而“导入”则可以通过上传档案&#xff0c;导入成功后&#xff0c;相应的寝室就会相应的减少。在录入大楼的时候&#xf…

Filebeat+Kafka+ELK日志分析架构

目录 一、zookeeper&#xff1a; 1. zookeeper 定义&#xff1a; 2. Zookeeper 工作机制: 3. Zookeeper 特点: 4. Zookeeper 数据结构: 5. Zookeeper 应用场景: 5.1 统一命名服务: 5.2 统一配置管理: 5.3 统一集群管理: 5.4 服务器动态上下线: 5.5 软负载均衡: 6. Zookeeper 选…

MAYA教程之建模基础命令介绍

基础命令 视图相关操作 旋转视图 : ALT 鼠标左键平移视图 : ALT 鼠标中键缩放视图 : 滚动鼠标滚轮 或者 ALT 鼠标右键切换视图 : 空格键回到模型 : F 视图状态 选择状态 : Q移动状态 : W旋转状态 : E缩放状态 : R 视图显示 正常显示 : 1正常圆滑同时显示 : 2圆滑显示 …

C#中List、Dictionary、HashSet用法以及区别

前言 在C#编程中&#xff0c;List、Dictionary和HashSet是常用的集合类型,它们都有自己的特点和适用场景。本篇博客将介绍它们的用法、区别及常见操作。深入了解这些集合类型&#xff0c;能够帮助我们更好地组织和处理数据。 1. List: List是一个有序可重复集合&#xff0c;可…

Hadoop3教程(二十二):Yarn的基础架构与工作流程

文章目录 &#xff08;126&#xff09;基础架构&#xff08;127&#xff09;YARN的工作机制&#xff08;128&#xff09;作业全流程参考文献 &#xff08;126&#xff09;基础架构 之前基本介绍完了Hadoop的几个核心组件&#xff0c;接下来可以思考下&#xff0c;在MR程序运行…

Camera BSP之GPIO/I2C/PMIC简介

和你一起终身学习&#xff0c;这里是程序员Android 经典好文推荐&#xff0c;通过阅读本文&#xff0c;您将收获以下知识点: 一、GPIO介绍二、IC 总线概括三、PMIC 概括四、思考 一、GPIO介绍 GPIO&#xff1a;General Purpose Input Output &#xff08;通用输入/输出&#xf…

【iOS】JSONModel的基本使用

文章目录 前言一、导入JSONModel二、JSONModel的基本使用1.基本用法2.模型集合3.模型导出为NSDictionary或JSON4.设置所有属性可选&#xff08;所有属性值可以为空&#xff09;5.下划线(蛇式)转驼峰命名法 前言 JSONModel 是一个用于 Objective-C 的开源库&#xff0c;它用于简…

(三十四)大数据实战——scala运行环境安装配置及IDEA开发工具集成

前言 本节内容我们主要介绍一下scala运行环境的安装配置以及在idea开发工具中集成scala插件&#xff0c;便于scala项目的开发。 在开始scala安装配置之前&#xff0c;我们要先安装好jvm运行环境&#xff0c;scala运行于Java虚拟机&#xff08;JVM&#xff09;上&#xff0c;并…

TCP/IP五元组

什么是五元组规则&#xff1f; 五元组是通信术语&#xff0c;英文名称为five-tuple,或5-tuple&#xff0c;五元组包括源IP地址(source IP)、源端口(source port)、目的IP地址(destination IP)、目的端口(destination port) 和 传输层协议(the layer 4 protocol)的五个量集合。…

【API篇】四、Flink物理分区算子API

文章目录 1、 分区算子&#xff1a;随机分区2、分区算子&#xff1a;轮询分区3、分区算子&#xff1a;重缩放分区4、分区算子&#xff1a;广播5、分区算子&#xff1a;全局分区6、自定义分区 重分区&#xff0c;即数据"洗牌"&#xff0c;将数据分配到下游算子的并行子…

互联网Java工程师面试题·Java 总结篇·第八弹

目录 72、用 Java 的套接字编程实现一个多线程的回显&#xff08;echo&#xff09;服务器。 73、XML 文档定义有几种形式&#xff1f;它们之间有何本质区别&#xff1f;解析XML 文档有哪几种方式&#xff1f; 74、你在项目中哪些地方用到了 XML&#xff1f; 72、用 Java 的套…

如何注册微信小程序

如何注册微信小程序 前言 因为最近沉迷和朋友们一起下班去打麻将&#xff0c;他们推荐了一个计分的小程序&#xff0c;就不需要每局都转账或者用扑克牌记录了&#xff0c;但是这个小程序不仅打开有广告&#xff0c;各个页面都植入了广告&#xff0c;用起来十分不适。 于是我…

Freemarker快速入门(SpringBoot版)

文章目录 Freemarker快速入门1、Freemarker概述2、Freemarker初体验3、数据类型3.0 常见操作符3.1 数值类型3.2 字符串类型3.3 布尔类型3.4 日期类型3.5 序列类型3.6 哈希类型 4、运算符4.1 算术运算符4.2 逻辑运算符4.3 比较运算符4.4 空值运算符 5、常用指令5.1 assign 自定义…

热门影视APP系统源码 可二开 后端+app+搭建教程

影视APP源码绿豆二开版 后端app搭建教程都在压缩包里&#xff0c;搭建步骤和绿豆一样 安装宝塔 yum install -y wget && wget -O install.sh http://download.bt.cn/install/install_6.0.sh && sh install.sh 安装环境 Nginx 1.20.2 MySQL5.6-5.7 php7.0-7.…

使用 VS Code 作为 VC++ 6.0 的编辑器

使用 VS Code 作为 VC 6.0 的编辑器 由于一些众所周知的原因&#xff0c;我们不得不使用经典&#xff08;过时&#xff09;的比我们年龄还大的已有 25 年历史的 VC 6.0 来学习 C 语言。而对于现在来说&#xff0c;这个经典的 IDE 过于简陋&#xff0c;并且早已不兼容新的操作系…

tomcat、nginx实现四层转发+七层代理+动静分离实验

实验环境&#xff1a; nginx1——20.0.0.11——客户端 静态页面&#xff1a; nginx2——20.0.0.21——代理服务器1 nginx3——20.0.0.31——代理服务器2 动态页面&#xff1a; tomcat1——20.0.0.12——后端服务器1 tomcat2——20.0.0.22——后端服务器2 实验步骤&…

基于LSTM-Adaboost的电力负荷预测(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

基于SSM的仓库管理系统设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用JSP技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…