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

news2025/7/18 23:21:02

前言

几个月前 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 getting
        val iosArm64Main by getting
        val iosSimulatorArm64Main by getting
        val 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.background

        Surface(
            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'
        end
    end
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.utils

import kotlin.math.max

object 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 + len
        val radix = 1 shl shift
        val mask = radix - 1
        do {
            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's
        var i = i
        if (i <= 0) return if (i == 0) 32 else 0
        var n = 31
        if (i >= 1 shl 16) {
            n -= 16
            i = i ushr 16
        }
        if (i >= 1 shl 8) {
            n -= 8
            i = i ushr 8
        }
        if (i >= 1 shl 4) {
            n -= 4
            i = i ushr 4
        }
        if (i >= 1 shl 2) {
            n -= 2
            i = 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.coloradmin.cn/o/1102862.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【配置环境】SQLite数据库安装和编译以及VS下C++访问SQLite数据库

一&#xff0c;环境 Windows 11 家庭中文版&#xff0c;64 位操作系统, 基于 x64 的处理器SQLite - 3.43.2Microsoft Visual Studio Community 2022 (64 位) - Current 版本 17.5.3 二&#xff0c;SQLite简介 简要介绍 SQLite&#xff08;Structured Query Language for Lite&a…

新手最容易触发的10个PHP语言Bug分享

&#x1f3c6;作者简介&#xff0c;黑夜开发者&#xff0c;CSDN领军人物&#xff0c;全栈领域优质创作者✌&#xff0c;CSDN博客专家&#xff0c;阿里云社区专家博主&#xff0c;2023年6月CSDN上海赛道top4。 &#x1f3c6;数年电商行业从业经验&#xff0c;历任核心研发工程师…

Linux性能优化--性能追踪:受CPU限制的应用程序(GIMP)

10.0 概述 本章包含了一个例子&#xff1a;如何用Linux性能工具在受CPU限制的应用程序中寻找并修复性能问题。 阅读本章后&#xff0c;你将能够&#xff1a; 在受CPU限制的应用程序中明确所有的CPU被哪些源代码行使用。用1trace和oprofile弄清楚应用程序调用各种内部与外部函…

基于蜜獾优化的BP神经网络(分类应用) - 附代码

基于蜜獾优化的BP神经网络&#xff08;分类应用&#xff09; - 附代码 文章目录 基于蜜獾优化的BP神经网络&#xff08;分类应用&#xff09; - 附代码1.鸢尾花iris数据介绍2.数据集整理3.蜜獾优化BP神经网络3.1 BP神经网络参数设置3.2 蜜獾算法应用 4.测试结果&#xff1a;5.M…

MySQL双主一从高可用

MySQL双主一从高可用 文章目录 MySQL双主一从高可用环境说明1.配置前的准备工作2.配置yum源 1.在部署NFS服务2.安装主数据库的数据库服务&#xff0c;并挂载nfs3.初始化数据库4.配置两台master主机数据库5.配置m1和m2成为主数据库6.安装、配置keepalived7.安装部署从数据库8.测…

leetcode-200. 岛屿数量

1. 题目 leetcode题目链接 2. 解答 思路&#xff1a; 需要循环遍历每个节点&#xff1b;找到陆地&#xff0c;基于陆地开始遍历陆地的上下左右&#xff1b;数组dirm dirn就可以表示某个区域的上下左右&#xff1b;标记遍历过的节点&#xff1b;设计循环的退出条件&#xf…

Kotlin中的比较运算符

在Kotlin中&#xff0c;我们可以使用比较运算符进行值的比较和判断。下面对Kotlin中的等于、不等于、小于、大于、小于等于和大于等于进行详细介绍&#xff0c;并提供示例代码。 等于运算符&#xff08;&#xff09;&#xff1a; 等于运算符用于判断两个值是否相等。如果两个值…

XMLHttpRequest的readyState状态值

readyState状态值 功能&#xff1a;在Ajax请求与服务器响应中&#xff0c;是通过XMLHttpRequest对象完成。而readyState状态值则是记录XMLHttpRequest对象在这个过程进行变化的状态。 readyState状态值readyState分别有5个状态值 0&#xff1a;请求未初始化&#xff1a;在未点击…

微信小程序--数字化会议OA系统之首页搭建

一、Flex弹性布局 布局的传统解决方案&#xff0c;基于盒状模型&#xff0c;依赖 display属性 position属性 float属性。它对于那些特殊布局非常不方便&#xff0c;比如&#xff0c;垂直居中就不容易实现。 2009年&#xff0c;W3C提出了一种新的方案—-Flex布局&#xff0c;可…

python 运算符的优先级:先算乘除,后算加减,有括号的先算括号里面的。

运算符的优先级 什么是运算符的优先级&#xff1f;其实我们小学就已经接触过了&#xff0c;就是在一个表达式中&#xff0c;我们先算谁的问题。 先算乘除&#xff0c;后算加减&#xff0c;有括号的先算括号里面的。 个人建议&#xff1a; ① 不要把一个表达式****写得过于复杂…

【Java基础面试十五】、 说一说你对多态的理解

文章底部有个人公众号&#xff1a;热爱技术的小郑。主要分享开发知识、学习资料、毕业设计指导等。有兴趣的可以关注一下。为何分享&#xff1f; 踩过的坑没必要让别人在再踩&#xff0c;自己复盘也能加深记忆。利己利人、所谓双赢。 面试官&#xff1a;说一说你对多态的理解 …

【Java基础面试十四】、 封装的目的是什么,为什么要有封装?

文章底部有个人公众号&#xff1a;热爱技术的小郑。主要分享开发知识、学习资料、毕业设计指导等。有兴趣的可以关注一下。为何分享&#xff1f; 踩过的坑没必要让别人在再踩&#xff0c;自己复盘也能加深记忆。利己利人、所谓双赢。 面试官&#xff1a; 封装的目的是什么&…

MySQL事务MVCC详解

一、概述 MVCC (MultiVersion Concurrency Control) 叫做多版本并发控制机制。主要是通过数据多版本来实现读-写分离&#xff0c;做到即使有读写冲突时&#xff0c;也能做到不加锁&#xff0c;非阻塞并发读&#xff0c;从而提高数据库并发性能。 MVCC只在已提交读&#xff08…

CCF CSP认证 历年题目自练Day34

题目一 试题编号&#xff1a; 202303-1 试题名称&#xff1a; 田地丈量 时间限制&#xff1a; 1.0s 内存限制&#xff1a; 512.0MB 问题描述&#xff1a; 问题描述 西西艾弗岛上散落着 n 块田地。每块田地可视为平面直角坐标系下的一块矩形区域&#xff0c;由左下角坐标 (x1,…

KNN-近邻算法 及 模型的选择与调优(facebook签到地点预测)

什么是K-近邻算法&#xff08;K Nearest Neighbors&#xff09; 1、K-近邻算法(KNN) 1.1 定义 如果一个样本在特征空间中的k个最相似(即特征空间中最邻近)的样本中的大多数属于某一个类别&#xff0c;则该样本也属于这个类别。 来源&#xff1a;KNN算法最早是由Cover和Hart提…

【网络协议】聊聊从物理层到MAC层 ARP 交换机

物理层 物理层其实就是电脑、交换器、路由器、光纤等。组成一个局域网的方式可以使用集线器。可以将多台电脑连接起来&#xff0c;然后进行将数据转发给别的端口。 数据链路层 Hub其实就是广播模式&#xff0c;如果A电脑发出一个包&#xff0c;B、C电脑也可以收到。那么数据…

zk的二阶段提交图解

第一阶段&#xff1a;每次的数据写入事件作为提案广播给所有Follower结点&#xff1b;可以写入的结点返回确认信息ACK&#xff1b;第二阶段&#xff1a;Leader收到一半以上的ACK信息后确认写入可以生效&#xff0c;向所有结点广播COMMIT将提案生效。

Unity 实现一个FPS游戏的全过程

Unity是一款功能强大的游戏引擎&#xff0c;它提供了各种各样的工具和功能&#xff0c;以帮助开发者轻松地创建精美的3D游戏和应用程序。在本文中&#xff0c;我们将使用Unity实现一个FPS游戏的全过程&#xff0c;从场景设计、角色控制、敌人AI到最终的打包发布。 对啦&#x…

开源项目汇总

element-plus 人人开源 人人开源 多租户 若依 jeecg https://gitee.com/jeecg/jeecg?_fromgitee_search#https://gitee.com/link?targethttp%3A%2F%2Fidoc.jeecg.com jeeplus JeePlus快速开发平台 j2eefast Sa-Plus

地震勘探原理部分问题解答

1、二维/三维&#xff08;陆地/海洋&#xff09;地震勘探&#xff0c;炮点&#xff08;激发点&#xff09;和检波点&#xff08;接收点&#xff09;的排布位置如何&#xff1f;画图作答&#xff1f; &#xff08;1&#xff09;陆地地震勘探 二维陆地地震野外采集&#xff1a;震…