Kotlin 移动端多平台

支持多平台编程是 Kotlin 的主要优势之一。它减少了为不同平台编写和维护相同代码所花费的时间,同时保留了本机编程的灵活性和优势。

1. 基本概念

  • KMM:Kotlin Multiplatform for mobile(移动设备的 Kotlin 多平台)

    • KMM 多平台的主要用例之一是在移动平台之间共享应用程序逻辑代码

    • 如果要实现本机 UI 或使用平台 API 时,需要编写特定于平台的代码

    • KMM 当前处于 beta 阶段,已经几乎稳定

    • Kotlin roadmap

          以下为 Kotlin 团队的优先事项

      • K2 compiler:对 Kotlin 编译器的重写,针对速度、并行性和统一性进行了优化。它还将让我们介绍许多预期的语言功能。在 1.9.0 中,K2 编译器已经达到了 JVM 的 Beta 阶段。此版本还增加了对 Kotlin/Native 的支持,并改进了对 Kotlin/JS 的支持,下一步是使 K2 编译器稳定并发布 Kotlin 2.0

      • K2-based IntelliJ plugin:基于 K2 的 IntelliJ 插件:更快的代码完成、突出显示和搜索,以及更稳定的代码分析。

      • Kotlin Multiplatform:通过改进工具链稳定性和文档,并确保兼容性保证,将技术推广到 Stable

        • Kotlin 多平台移动版于 2022 年 10 月进入测试版。到 2023 年底,我们希望将其提升为稳定版,这意味着即使在保守的情况下也可以安全使用。

        • 与 Google 合作开发了新的内存分配器,该分配器应该可以提高运行时性能和内存消耗。开发已经完成,分配器在 Kotlin 1.9.0 中可通过选择加入获得,并将在 Kotlin 1.9.20 (KT-55364) 中默认启用(致力于通过并行进行更多垃圾收集工作来减少垃圾收集暂停)。旧内存管理器在 Kotlin 1.8.20 中已弃用,并将在 Kotlin 1.9.20 中删除

  • Compose Multiplatform

    • Compose Multiplatform 是 JetBrains 基于 Kotlin 和 Jetpack Compose 的声明式 UI 框架

    • iOS 处于 Alpha 状态

2. 环境准备

  • 安装必要的工具

    • Android Studio

    • Xcode

    • JDK(推荐 17 LTS)

    • Kotlin Multiplatform Mobile 插件

      • Android Studio Settings/Preferences | Plugins

      • 搜索 Kotlin Multiplatform Mobile,安装重启

    • Kotlin 插件

           Kotlin 1.9.10 -> compose compiler 1.5.3

           Kotlin 1.9.0 -> compose compiler 1.5.0

      • 通常和 Android Studio 捆绑,建议升级到 1.9.0 或以上

      • 如果使用了 Jetpack Compose,注意 compose compiler 和 Kotlin 版本的兼容,例如:

  • 检查环境的工具 - KDoctor

    • KDoctor 仅适用于 macOS

    • 安装:brew install kdoctor

    • 执行:kdoctor

    • 根据输出结果,安装缺失的工具

      • 通常需要安装:Cocoapods,Ruby

3. 创建工程

  • KMM 工程使用 gradle 来构建

  • 工程整体结构,这里使用 kmm 重写 UpStorage 库为例:

      ├── androidApp

      ├── build.gradle.kts

      ├── gradle

      ├── gradle.properties

      ├── iosApp

      ├── kmmStorageShared

      └── settings.gradle.kts

    • 整个工程分为全局配置部分(build.gradle.kts,gradle,gradle.properties,settings.gradle.kts)和 Modules 部分(kmmStorageShared,androidApp,iosApp)

      • settings.gradle.kts:主要用来引入其他模块

        rootProject.name = "UpStorage"
        include(":androidApp")
        include(":kmmStorageShared")
      • build.gradle.kts:主要用来引入 Maven 源,和 gradle 插件

        buildscript {
            repositories {
                maven(url = "https://mdpm.haier.net/nexus/repository/public")
                。。。
            }
            dependencies {
                classpath(libs.bundles.plugins)
                // 包含的插件有:
                // AGP: com.android.tools.build:gradle:8.1.1
                // Kotlin: org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.10
            }
        }
        
        allprojects {
            repositories {
                maven(url = "https://mdpm.haier.net/nexus/repository/public")
                。。。
            }
        }
        
      • gradle.properties

        # 共享本机代码中使用自定义 cinterop 库
        kotlin.mpp.enableCInteropCommonization=true
        # 新的源码布局
        kotlin.mpp.androidSourceSetLayoutVersion=2
        ###############################################################################################
        *                                                                     common
        *                                                                        |
        *                                                      +-----------------+-------------------+
        *                                                      |                                     |
        *
        *                                                    native                                 ...
        *
        *                                                     |
        *                                                     |
        *                                                     |
        *         +----------------------+--------------------+-----------------------+
        *         |                      |                    |                       |
        *
        *       apple                  linux                mingw              androidNative
        *
        *         |
        *  +-----------+------------+------------+
        *  |           |            |            |
        *
        * macos       ios         tvos        watchos
      • gradle

        建议使用 8.0 或以上版本
        distributionBase=GRADLE_USER_HOME
        distributionPath=wrapper/dists
        distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
        zipStoreBase=GRADLE_USER_HOME
        zipStorePath=wrapper/dists
    • Modules 部分介绍

      • kmmStorageShared:是一个纯的 Kotlin 模块,中包含 Android 和 iOS 应用程序通用的逻辑

              模块结构如下:

              ├── build.gradle.kts

              ├── kmmStorageShared.podspec

              └── src

               ├── androidMain

               ├── commonMain

               └── iosMain

        • build.gradle.kts:

          • 引入插件

          • plugins {
                kotlin("multiplatform")    // kotlin 多平台插件
                kotlin("native.cocoapods") // 如果 IOS 使用 cocoapods,引入此插件
                id("com.android.library")  // 安卓库
            }
          • 其它配置

          • kotlin {
                targetHierarchy.default() // 新的默认的源码布局
                // 支持的 IOS 平台
                iosX64()
                iosArm64()
                iosSimulatorArm64()
            
                cocoapods {
                    summary = "Some description for the Shared Module"
                    homepage = "Link to the Shared Module homepage"
                    version = "1.0"
                    license = "MIT"
                    ios.deploymentTarget = "11.0"
                    podfile = project.file("../iosApp/Podfile"
                    framework {
                        baseName = "kmmStorageShared"
                        isStatic = true
                    }
                    // 源
                    specRepos {
                        url("https://git.haier.net/uplus/shell/cocoapods/Specs.git")
                    }
                    // IOS 引入其它模块
                    pod("FMDB") {
                        version = "~> 2.7.5"
                    }
                    pod("uplog") {
                        source = git("https://git.haier.net/uplus/ios/uplog.git") {
                            branch = "kmm_1.7.1"
                        }
                    }
                }
            
                sourceSets {
                    val commonMain by getting {
                        dependencies {
                            implementation(libs.kotlinx.coroutines.core)
                        }
                    }
                    val androidMain by getting {
                        dependencies {
                            api("com.haier.uhome:UpLog:3.6.0")
                            api("com.haier.uhome:uplog-core:3.4.0")
                        }
                    }
                    val iosMain by getting {
                        dependencies {
                        }
                    }
                }
            }
      • androidApp:通常作为库开发的 demo,是一个安卓模块

      • iosApp:通常作为库开发的 demo,是一个 xcode 工程

4. 代码说明

在 kmm 共享模块中

  • src/commonMain 是共享代码

  • src/androidMain 是安卓差异化代码,可以调用 JDK、AndroidSDK 中的 API

  • src/iosMain 是苹果手机的差异化代码,可以调用 IOS 平台的 API

4.1 举例

我们基于现有的日志库,实现一个跨平台的日志接口:安卓和 IOS 平台有各自的 UpLog 库,通过使用 Kotlin 的 expect 和 actual 关键字来实现差异代码和通用逻辑

  • commonMain:

    internal expect object UpLog {
        fun d(tag: String, msg: String, vararg args: Any)
    
        fun d(tag: String, msg: String, t: Throwable)
    
        fun i(tag: String, msg: String, vararg args: Any)
    
        fun i(tag: String, msg: String, t: Throwable)
    
        fun w(tag: String, msg: String, vararg args: Any)
    
        fun w(tag: String, msg: String, t: Throwable)
    
        fun e(tag: String, msg: String, vararg args: Any)
    
        fun e(tag: String, msg: String, t: Throwable)
    }
  • androidMain:

      这里我们可以发现,使用了 UpLoggerManager,其本质上是 UpLog 库中的 API

    internal actual object UpLog {
        private const val LOGGER_NAME = "UpStorage"
        private val initialized = AtomicBoolean(false)
        private lateinit var logger: Logger
    
        actual fun d(tag: String, msg: String, vararg args: Any) {
            logger.debug("$tag: $msg", *args)
        }
    
        actual fun d(tag: String, msg: String, t: Throwable) {
            logger.debug("$tag: $msg", t)
        }
    
        actual fun i(tag: String, msg: String, vararg args: Any) {
            logger.info("$tag: $msg", *args)
        }
    
        actual fun i(tag: String, msg: String, t: Throwable) {
            logger.info("$tag: $msg", t)
        }
    
        actual fun w(tag: String, msg: String, vararg args: Any) {
            logger.warn("$tag: $msg", *args)
        }
    
        actual fun w(tag: String, msg: String, t: Throwable) {
            logger.warn("$tag: $msg", t)
        }
    
        actual fun e(tag: String, msg: String, vararg args: Any) {
            logger.error("$tag: $msg", *args)
        }
    
        actual fun e(tag: String, msg: String, t: Throwable) {
            logger.error("$tag: $msg", t)
        }
    
        init {
            if (initialized.compareAndSet(false, true)) {
                logger = UpLoggerManager.getInstance().createLogger(LOGGER_NAME)
            }
        }
    }
  • iosMain:

      这里使用的是 cocoapods 生成库中的 iOS api

    package com.haier.uplus.kmm.storage.platform
    
    import cocoapods.uplog.*
    
    internal actual object UpLog {
        private const val MODULE_NAME = "KmmStorage"
        private val logger = UPLog.getInstance()!!.createLogger(MODULE_NAME)!!
    
        actual fun d(tag: String, msg: String, vararg args: Any) = logger.logWithLevel(UPLogLevelDebug, msg)
    
        actual fun d(tag: String, msg: String, t: Throwable) = logger.logWithLevel(UPLogLevelDebug, msg)
    
        actual fun i(tag: String, msg: String, vararg args: Any) = logger.logWithLevel(UPLogLevelInfo, msg)
    
        actual fun i(tag: String, msg: String, t: Throwable) = logger.logWithLevel(UPLogLevelInfo, msg)
    
        actual fun w(tag: String, msg: String, vararg args: Any) = logger.logWithLevel(UPLogLevelWarning, msg)
    
        actual fun w(tag: String, msg: String, t: Throwable) = logger.logWithLevel(UPLogLevelWarning, msg)
    
        actual fun e(tag: String, msg: String, vararg args: Any) = logger.logWithLevel(UPLogLevelError, msg)
    
        actual fun e(tag: String, msg: String, t: Throwable) = logger.logWithLevel(UPLogLevelError, msg)
    }

如果要使用这个 UpLog 单例,不管在 common 中,还是在 android/ios 中都可以直接引用并调用

4.2 基本原理

Kotlin Native是一种将Kotlin源码编译成不需要任何VM支持的目标平台二进制数据的技术,编译后的二进制数据可以直接运行在目标平台上,它主要包含一个基于LLVM的后端编译器的和一个Kotlin本地运行时库。设计Kotlin Native的目的是为了支持在非JVM环境下进行编程,如在嵌入式平台和iOS环境下,如此一来,Kotlin就可以运行在非JVM平台环境下。

Kotlin Native 内部使用 cinterop 来对 Apple Framework 进行扫描,根据其头文件(.h)获取可以调用的类、方法、变量、常量以及他们的类型,最终生成 klib 文件。而 klib 文件中含着针对不同 CPU 架构所编译的二进制文件,以及可供 Kotlin Native 调用的 knm 文件, knm 文件类似 Jar 包中的 。class 文件,是被编译后的 Kotlin 代码,内部将 cinterop 扫描出来 的 Objective-C 内容转换成了 Kotlin 对应的内容,以便 IDE 可以进行索引,最终在 KMM 模块中使用 Kotlin 代码进行调用。

4.3 模块编译

  • 编译命令:./gradlew clean assemble

  • Android 和 iOS 的产物如下所示

  • 可使用 maven-publish 插件把 kmm 模块的产物上传到 maven 私服:包括安卓端的 aar 文件和 iOS 各平台的 klib 文件

    • 如果单独要把 Framework 导出给 iOS 使用,把编译产物手动上传到 pod 私服即可

5. 可能碰到的问题

  • 使用 pod 引入 iOS 模块的时候,出现找不到头文件的错误,例如:

    > Task :kmmStorageShared:cinteropUplogIosSimulatorArm64
    Exception in thread "main" java.lang.Error: /Users/liuqing.yang/work/haier/kmm/
    UpStorage/kmmStorageShared/build/cocoapods/synthetic/ios/build/
    Release-iphonesimulator/uplog/uplog.framework/Headers/UPLogUpload.h:9:9: 
    fatal error: 'UpLogUploadFileDelegate.h' file not found
            at org.jetbrains.kotlin.native.interop.indexer.ModuleSupportKt.getModulesASTFiles(ModuleSupport.kt:74)
            at org.jetbrains.kotlin.native.interop.indexer.ModuleSupportKt.getModulesInfo(ModuleSupport.kt:14)
            at org.jetbrains.kotlin.native.interop.gen.jvm.MainKt.buildNativeLibrary(main.kt:563)
            at org.jetbrains.kotlin.native.interop.gen.jvm.MainKt.processCLib(main.kt:317)
            at org.jetbrains.kotlin.native.interop.gen.jvm.MainKt.processCLibSafe(main.kt:242)
            at org.jetbrains.kotlin.native.interop.gen.jvm.MainKt.access$processCLibSafe(main.kt:1)
            at org.jetbrains.kotlin.native.interop.gen.jvm.Interop.interop(main.kt:100)
            at org.jetbrains.kotlin.cli.utilities.InteropCompilerKt.invokeInterop(InteropCompiler.kt:45)
            at org.jetbrains.kotlin.cli.utilities.MainKt.mainImpl(main.kt:23)
            at org.jetbrains.kotlin.cli.utilities.MainKt.main(main.kt:45)

      或者提示 klib 不存在,例如:

    :kmmStorageShared:iosArm64Main: cinterop file: /Users/liuqing.yang/work/haier/kmm/UpStorage/kmmStorageShared/build/classes/kotlin/iosArm64/main/cinterop/kmmStorageShared-cinterop-uplog.klib does not exist

      如果出现 klib 不存在,需要删除工程中的 .gradle 文件夹,然后重新 sync 工程就会看到找不到头文件的真实原因

      最后根据提示,打开 IOS 工程,修复对应错误即可

  • 根据官方的描述,纯的 Swift 模块,目前还不支持双向互操作,Swift 调用 Kotlin 没有问题。

Kotlin/Native 与 Objective-C 支持双向互操作。见 Interoperability with Swift/Objective-C | Kotlin

  • 安卓 Kotlin 版本不一致问题

    • 测试 Demo 的 Kotlin 版本目前是1.3.61,然而 Kmm 工程的版本为1.9.10

    • 可能会碰到如下错误:

    • e: Incompatible classes were found in dependencies.
      e: /Users/liuqing.yang/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib-common/1.9.10/dafaf2c27f27c09220cee312df10917d9a5d97ce/kotlin-stdlib-common-1.9.10.jar!/META-INF/kotlin-stdlib-common.kotlin_module: Module was compiled with an incompatible version of Kotlin. The binary version of its metadata is 1.9.0
    • 升级 Demo 中的 Kotlin 版本到 1.9.10,可能会碰到:

    • > java.lang.NoClassDefFoundError: org/jetbrains/kotlin/gradle/plugin/KotlinBasePlugin

因为 Demo 工程的 AGP 版本太低,建议升级到 4.2.2 或更新,另外 gradle 版本升级到 7.2 或更新

   另外如果提示

Class 'xxxxxxxx' is compiled by a pre-release version of Kotlin and cannot be loaded by this version of the compiler

  在工程根目录的 gradle.properties 中加入

kotlin.experimental.tryK2=true
  • 新的 Kotlin 版本中,kotlin-android-extensions 已移除,建议使用 viewBinding

android {
    buildFeatures {
        viewBinding true
    }
}

6. 单元测试

6.1 集成 Cucumber

Setup
  • 依赖导入

dependencies {
    androidTestImplementation(libs.androidx.test.core.ktx)
    androidTestImplementation(libs.androidx.test.rules)
    androidTestUtil(libs.androidx.test.orchestrator)
    androidTestImplementation(libs.test.cucumber.android)
}
  • KmmAndroidJUnitRunner

package com.haier.uplus.kmm.storage.android.test

import android.os.Bundle
import io.cucumber.android.runner.CucumberAndroidJUnitRunner
import io.cucumber.junit.CucumberOptions
import java.io.File

/**
 * Created by liuqing.yang
 * 2023/9/11.
 */
@CucumberOptions(
    features = ["features"],
    strict = true,
)
class KmmAndroidJUnitRunner : CucumberAndroidJUnitRunner() {

    override fun onCreate(bundle: Bundle?) {
        bundle?.putString("plugin", getPluginConfigurationString())
        //it crashes on Android R without it
        File(getAbsoluteFilesPath()).mkdirs()
        super.onCreate(bundle)
    }

    /**
     * Since we want to checkout the external storage directory programmatically, we create the plugin configuration
     * here, instead of the {@link CucumberOptions} annotation.
     *
     * @return the plugin string for the configuration, which contains XML, HTML and JSON paths
     */
    @Suppress("SameParameterValue")
    private fun getPluginConfigurationString(): String {
        val cucumber = "cucumber"
        val separator = "--"
        return "junit:" + getCucumberXml(cucumber) + separator +
                "html:" + getCucumberHtml(cucumber)
    }

    @Suppress("SameParameterValue")
    private fun getCucumberHtml(cucumber: String) = "${getAbsoluteFilesPath()}/$cucumber/.html"

    @Suppress("SameParameterValue")
    private fun getCucumberXml(cucumber: String) = "${getAbsoluteFilesPath()}/$cucumber/.xml"

    private fun getAbsoluteFilesPath() =
        File(targetContext.getExternalFilesDir(null), "reports").absolutePath
}
  • Feature 文件

Feature: Kmm storage

  Scenario Outline: auto insert some data to db
    Given insertKeyValue"<Key>""<Value>"
    Then insertSuccess"true"

    Examples:
      | Key | Value |
      | a   | 1     |
      | b   | 2     |
      | c   | 3     |
  • Steps

package com.haier.uplus.kmm.storage.android.test

import android.util.Log
import com.haier.uplus.kmm.storage.manager.UpStorage
import io.cucumber.java.en.Given
import io.cucumber.java.en.Then
import org.junit.Assert

/**
 * Created by liuqing.yang
 * 2023/9/11.
 */
class StorageStep {
    companion object {
        private const val TAG = "StorageStep"
    }

    @Volatile
    private var insertRet: Boolean = false

    @Given("insertKeyValue{string}{string}")
    fun insertkeyvalue(key: String, value: String) {
        insertRet = UpStorage.putIntValue(key, value.toInt())
        Log.d(TAG, "insertkeyvalue: $insertRet")
    }

    @Then("insertSuccess{string}")
    fun insertSuccess(ret: String) {
        Log.d(TAG, "insertSuccess: $insertRet, $ret")
        Assert.assertEquals(ret.toBoolean(), insertRet)
    }
}
Running the tests

使用 Android Studio IDE

  1. Run > Edit Configurations

  2. 点击 + 按钮,选择 Android Instrumented Tests

  3. 指定测试名称,选择测试模块,点击 OK,最后点击运行按钮

执行结果和日志可在 build 目录中查看

6.2 集成 Jacoco

  • 引入 jacoco 插件

plugins {
    id("jacoco")
}
  • 自定义扫描的源码目录和 class 文件目录

tasks.register(kmmJacoco, JacocoReport::class.java) {
    group = reporting
    description = jacocoDesc
    dependsOn(createDebugAndroidTestCoverageReport)
    reports {
        xml.enabled = true
        html.enabled = true
    }
    def coverageClassDirs = fileTree(
            //检测覆盖率的class所在目录(以项目class所在目录为准)
            dir: '../../UpBluetoothPlugin/build/intermediates/javac/debug',
            //增加以上目录中不需要检测的文件列表
            excludes: [
                    '**/BuildConfig.class',
                    '**/impl/**.class'
            ]
    )
    getClassDirectories().setFrom(coverageClassDirs)
    getSourceDirectories().setFrom(files(coverageSourceDirs))
    File ecFile = new File("$buildDir/outputs/code_coverage/debugAndroidTest/connected");
    ecFile.listFiles().each {
        println it.name
        getExecutionData().setFrom(files(it))
    }
}
  • 执行结果:

    • HTML:build/reports/jacoco/kmmJacoco/html 

    • XML:build/reports/jacoco/kmmJacoco/kmmJacoco.xml 

7. 团队介绍

「三翼鸟数字化技术平台-智家APP平台」通过持续迭代演进移动端一站式接入平台为三翼鸟APP、智家APP等多个APP提供基础运行框架、系统通用能力API、日志、网络访问、页面路由、动态化框架、UI组件库等移动端开发通用基础设施;通过Z·ONE平台为三翼鸟子领域提供项目管理和技术实践支撑能力,完成从代码托管、CI/CD系统、业务发布、线上实时监控等Devops与工程效能基础设施搭建。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

vue3有了解过吗?能说说跟vue2的区别吗?

一、Vue3介绍 关于vue3的重构背景&#xff0c;尤大是这样说的&#xff1a; 「Vue 新版本的理念成型于 2018 年末&#xff0c;当时 Vue 2 的代码库已经有两岁半了。比起通用软件的生命周期来这好像也没那么久&#xff0c;但在这段时期&#xff0c;前端世界已经今昔非比了 在我…

Redis 笔记一

概览 1.Redis核心数据存储结构 2.Redis底层String编码int&embstr&raw 3.Redis底层压缩列表&跳表&哈希表 4.Redis底层Zset实现压缩列表和跳表如何选择 5.基于Redis实现微博&抢红包&12306核心业务 辅助学习&#xff1a;Redis 教程 | 菜鸟教程 1.Redis为什…

web3.0基本概念简析

web3.0概念简析 web3.0的发展史 web1.0 仅用于展示&#xff0c;无法进行点赞评论等交互 web2.0 不仅可以展示&#xff0c;还可以上传视频、图片等&#xff0c;用户可以参与创作内容并获取收益。但还是中心化的模型 缺点 1 机械化的人机验证 2 账户安全无法保证 多年未登陆…

dp--64. 最小路径和/medium 理解度A

64. 最小路径和 1、题目2、题目分析3、复杂度最优解代码示例4、抽象与扩展 1、题目 给定一个包含非负整数的 m x n 网格 grid &#xff0c;请找出一条从左上角到右下角的路径&#xff0c;使得路径上的数字总和为最小。 说明&#xff1a;每次只能向下或者向右移动一步。 示例 …

Dockerfile镜像实战

目录 一 构建SSH镜像 1.开启ip转发功能 2. 准备工作目录 3.修改配置文件 5.启动容器并修改root密码 二 构建Systemctl镜像 1. 准备工作目录 ​编辑2.修改配置文件 3.生成镜像 4.启动容器&#xff0c;并挂载宿主机目录挂载到容器中&#xff0c;进行初始化 5.进入容器 三…

C++三剑客之std::variant(二):深入剖析

目录 1.概述 2.辅助类介绍 2.1.std::negation 2.2.std::conjunction 2.3.std::is_destructible 2.4.std::is_object 2.5.is_default_constructible 2.6.std::is_trivially_destructible 2.7.std::in_place_type和std::in_place_index 3.原理分析 3.1.存储分析 3.2.…

【昕宝爸爸小模块】深入浅出之针对大Excel做文件读取问题

➡️博客首页 https://blog.csdn.net/Java_Yangxiaoyuan 欢迎优秀的你&#x1f44d;点赞、&#x1f5c2;️收藏、加❤️关注哦。 本文章CSDN首发&#xff0c;欢迎转载&#xff0c;要注明出处哦&#xff01; 先感谢优秀的你能认真的看完本文&…

VUE 中的 v-for 和 v-if 是否可以共存

VUE 中的 v-for 和 v-if 是否可以共存 前言1、面试经2、正确回答3、总结总结&#xff1a; 前言 要成功&#xff0c;先发疯&#xff0c;头脑简单往前冲&#xff01; 三金四银&#xff0c;金九银十&#xff0c;多学知识&#xff0c;也不能埋头苦干&#xff0c;要成功&#xff0c…

uniapp中uview组件库的NoticeBar 滚动通知 使用方法

目录 #平台差异说明 #基本使用 #配置主题 #配置图标 #配置滚动速度 #控制滚动的开始和暂停 #事件回调 #API #Props #Events 该组件用于滚动通告场景&#xff0c;有多种模式可供选择 #平台差异说明 AppH5微信小程序支付宝小程序百度小程序头条小程序QQ小程序√√√√…

2018年认证杯SPSSPRO杯数学建模B题(第一阶段)动态模糊图像全过程文档及程序

2018年认证杯SPSSPRO杯数学建模 B题 动态模糊图像 原题再现&#xff1a; 人眼由于存在视觉暂留效应&#xff0c;所以看运动的物体时&#xff0c;看到的每一帧画面都包含了一段时间内 (大约 1/24 秒) 的运动过程&#xff0c;所以这帧画面事实上是模糊的。对电影的截图来说&…

量化研究员!你应该如何写一手好代码

即使是Quant Researcher&#xff0c; 写一手高质量的代码也是非常重要的。再好的思路&#xff0c;如果不能正确地实现&#xff0c;都是没有意义的。 写一手高质量的代码的意义&#xff0c;对Quant developer来讲就更是自不待言了。这篇笔记就介绍一些python best practice。 始…

Unity Shader 的模板测试效果

模板测试是渲染管线中逐片元操作的一环&#xff0c;它的作用是筛选出指定模板的片元&#xff0c;而不符合模板的片元会被舍弃&#xff0c;从而做到一个遮罩的效果。 以下是Unity中实践的一个效果&#xff1a; 场景中可以看出&#xff0c;熊模型和茶壶模型都在差不多的位置&am…

idea社区版 MybatisCodeHelperPro插件使用介绍

文章目录 一、插件介绍二、idea社区版安装MybatisCodeHelperPro插件三、问题记录1. DatabaseHelper插件 加载不了部分数据库链接的列信息2. DatabaseHelper插件 数据库列显示顺序错乱3. MybatisCodeHelperPro插件 数据库字段不提示4. MybatisCodeHelperPro插件 特殊字段增加反引…

SpringBoot 统计API接口用时该使用过滤器还是拦截器?

统计请求的处理时间&#xff08;用时&#xff09;既可以使用 Servlet 过滤器&#xff08;Filter&#xff09;&#xff0c;也可以使用 Spring 拦截器&#xff08;Interceptor&#xff09;。两者都可以在请求处理前后插入自定义逻辑&#xff0c;从而实现对请求响应时间的统计。 …

汽车芯片「新变量」

编者按&#xff1a;汽车行业的格局重构和技术革新&#xff0c;也在推动芯片赛道进入变革周期。不同商业模式的博弈&#xff0c;持续升温。 对于智能汽车来说&#xff0c;过去几年经历了多轮硬件和软件的性能迭代&#xff0c;甚至是革新&#xff0c;如今&#xff0c;市场正在进…

张驰咨询:六西格玛工具企业如何实现资源优化与效率最大化

在这个百年未有之大变局的时期&#xff0c;我们面临着前所未有的挑战与机遇。我一直在寻找提升效率&#xff0c;减少资源浪费的方法。而六西格玛培训提供了一个系统化的解决方案&#xff0c;它不仅是一套工具&#xff0c;更是一种精益思维。 让我们一起思考一个问题&#xff1a…

VMware workstation安装SUSE Linux Enterprise Server 12 SP5虚拟机并配置网络

VMware workstation安装SUSE Linux Enterprise Server 12 SP5虚拟机并配置网络 SUSE Linux Enterprise Server是企业级Linux系统&#xff0c;适合企业应用。该文档适用于在VMware workstation平台安装SUSE Linux Enterprise Server虚拟机。 1.安装准备 1.1安装平台 Windows…

设计一个网页爬虫

定义 User Case 和 约束 注意&#xff1a;没有一个面试官会阐述清楚问题&#xff0c;我们需要定义Use case和约束 Use cases 我们的作用域只是处理以下Use Case&#xff1a; Service 爬取一批 url 生成包含搜索词的单词到页面的反向索引给页面生成标题和片段– 标题和片段是…

【机器学习】机器学习变量分析第02课

当我们谈论用机器学习来预测咖啡店的销售额时&#xff0c;我们实际上是在处理一系列与咖啡销售相关的变量。这些变量就像是我们用来理解销售情况的“线索”或“指标”。那么&#xff0c;让我们用通俗易懂的方式来聊聊这些变量是怎么工作的。 特征变量&#xff1a;咖啡店的“档…

Spring MVC学习之——自定义日期转化器

日期转换器 在数据库中的日期数据是date类型&#xff0c;而如何我们想在页面自己添加数据&#xff0c;一般是使用年-月-日的形式&#xff0c;这种形式不仅date类型接收不到&#xff0c;而且传来的是String类型&#xff0c;此时&#xff0c;我们就可以自定义日期转换器来接收数…