如何在2024年编写Android应用程序

news2024/11/28 22:43:23

如何在2024年编写Android应用程序

本文将介绍以下内容:

  • 针对性能进行优化的单活动多屏幕应用程序 🤫(没有片段)。
  • 应用程序架构和模块化 → 每个层面。
  • Jetpack Compose 导航。
  • Firestore。
  • 应用程序架构(模块化特征驱动的清晰架构)。

为什么要在应用程序中遵循架构?

应用程序架构就像您代码的蓝图。它可以智能地组织事物,使您的应用程序具有以下优点:

  • 易于理解和修复:没有混乱的代码。您可以快速找到并编辑所需内容。例如,如果您想添加一个按钮(转到 UI 层)。如果您想更改 UI 组件的验证逻辑(转到用例层)。
  • 可扩展和具有未来性:添加新功能非常简单,并且在大多数情况下,应用程序可以轻松适应新技术。
  • 可测试和可靠:可以早期发现错误,从而使应用程序更加平稳、可靠。提示:尝试为核心层编写更多测试。
  • 有益于团队协作:开发人员可以同时处理不同部分,而不会相互干扰,特别是如果他们正在处理不同的功能。
  • 可持续和长久:您的应用程序将保持健康并保持相关性多年。

投资于架构,观察您的应用程序繁荣!

App Screens

该应用程序有2个屏幕:登录和注销。
Login Screen
Post-Login Screen

面向未来的灵活性

使用 Kotlin 构建的本机 Android 应用程序可以无缝地过渡到各种平台,包括 iOS、Android、macOS、Windows、Linux 等。这要归功于 Kotlin Multiplatform。

该应用程序模块化以实现其未来的灵活性。

模块化

以下是应用程序中的模块:

  • app
  • feature
  • core
  • utility
  • buildSrc

维护代码的分层设计


应用程序模块

职责
它就像我们应用程序的船长。它会将您带到应用程序的正确屏幕,适应不同的设备(手机、平板电脑、智能手表、电视等),甚至计划在不同的平台(Android、iOS 等)上进行冒险 —— 这都归功于 Kotlin 的魔法。

应用程序模块包含主活动(单个活动)和 Jetpack Compose 导航的 RootHost

Jetpack Compose 导航是什么?

在 Jetpack Compose 中,导航通常是使用 Navigation 组件管理的,就像在传统的基于 XML 的 Android UI 中一样。导航组件有助于在应用程序中的不同可组合(UI 组件)之间导航。
app module

应用程序包

应用程序包含 Koin 依赖注入框架的初始化代码和 Firebase 初始化代码。Firestore 是该应用程序的后端。在下面的文章中可以了解更多关于 Koin 的信息。

https://levelup.gitconnected.com/koin-the-kotlin-kmp-dependency-injection-framework-dafaf9b85639?source=post_page-----04c9d8614c27--------------------------------

package com.evicky.financeledger.application

import android.app.Application
import com.evicky.core.di.useCaseModule
import com.evicky.feature.di.viewModelModule
import org.koin.core.context.startKoin

class FinanceLedgerApplication: Application() {
    override fun onCreate() {
        super.onCreate()
//        FirebaseApp.initializeApp(this)
        startKoin {
            modules(
                dataSourceModule, repoModule, useCaseModule, viewModelModule, coroutineDispatcherProviderModule
            )
        }
    }
}

基于安全原因,Firestore 从 GitHub 存储库中删除。代码已被注释以供参考。

//MainActivity.kt
package com.evicky.financeledger

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import com.evicky.financeledger.root.RootHost
import com.evicky.financeledger.ui.theme.FinanceLedgerTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            FinanceLedgerTheme {
                RootHost()
            }
        }
    }
}
  • 你是否注意到了上面的代码,MainActivity 没有像 Android 的 View 系统(基于 XML 的旧视图系统)一样扩展 AppCompatActivity。它是扩展了 ComponentActivity,这意味着该代码使用了 Jetpack Compose 库来构建 UI。

  • onCreate 事件管理器具有 androidx.activity:activity-compose 库的 setContent lambda 函数。

  • setContent 是使用 Jetpack Compose 定义屏幕 UI 的入口点。它接受一个描述要显示的 UI 元素层次结构的 composable 函数作为参数。

  • FinanceLedgerThemeRootHost 是可组合函数。

  • setContent 使用了一个特定于此应用程序的 material 3 主题,名为 FinanceLedgerTheme

该主题包含了 RootHost 的可组合函数。

让我们看一下 RootHost 的实现。

Root package

package com.evicky.financeledger.root

import androidx.compose.runtime.Composable
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.rememberNavController
import com.evicky.feature.login.signInScreen
import com.evicky.feature.register.navigateToRegisterScreen
import com.evicky.feature.register.registerScreen
import com.evicky.feature.util.SIGNIN_ROUTE
import com.evicky.utility.logger.Log

@Composable
internal fun RootHost() {
    val rootController = rememberNavController()
    NavHost(
        navController = rootController,
        startDestination = SIGNIN_ROUTE,
    ) {
        signInNavGraph(
            onSignInPress = {
                rootController.navigateToPostLoginScreen(it)
            }
        )
        postLoginNavGraph()
    }

}
  • @Composable 注解告诉编译器 RootHost 是一个可组合函数。

什么是注解?

在 Kotlin 中,注解是一种元数据形式,您可以将其添加到代码中,以提供有关代码元素(如类、函数或属性)的额外信息。Kotlin 中的注解类似于 Java 中的注解。

在这里,@Composable 将函数标记为 UI 组件。这些函数被称为可组合函数,它们定义了用户界面的特定部分,并且可以嵌套使用以创建 UI 元素的层次结构。

RootHost() 函数包含了组合导航代码。它将引导您到应用程序中的正确屏幕,成为您应用程序的导航器!

在 Jetpack Compose 中,通常使用导航组件来管理导航,就像在传统的基于 XML 的 Android UI 中一样。导航组件有助于在应用程序中的不同可组合(UI 组件)之间进行导航。

让我们稍后继续讨论代码!

Utility module

职责:

实用程序或助手模块的目的是为所有模块提供应用程序中常见或可重复使用的功能。

utility module

Logger package

为什么我们需要有一个日志记录器包,当我们在 Android 中有默认的 Logging 库时呢?

一些第三方开源日志记录库具有很好的功能。有时您的应用程序可能有多个日志记录库。如果我们以这种方式拥有它,那么日志记录库将很容易地添加/替换。无需更改应用程序的每个文件中都有日志的文件。为了更容易更换日志记录库。

为什么要更换日志记录库?

回到2021年,您可能听说过著名的开源日志记录库“Log4j”中报告了一个关键漏洞。全世界的开发人员都因听到这个漏洞而感到困惑,并且在短时间内难以更换他们的日志记录库。

Log4j 漏洞

  • 发现:在广泛用于 Java 应用程序中的受欢迎的 Log4j 日志记录库中发现了一个关键漏洞(CVE-2021-44228)。
  • 影响:它允许攻击者在受影响的系统上执行任意代码,可能导致数据盗窃、勒索软件攻击或完全接管系统。
  • 严重性:由于易于利用和广泛使用,被评为“关键”。
package com.evicky.utility.logger

import android.util.Log

object Log {

    fun v(tag: String, msg: String?, throwable: Throwable? = null) =
        msg?.run {
            Log.v(tag, this, throwable)
        }

    fun d(tag: String, msg: String?, throwable: Throwable? = null) =
        msg?.run {
            Log.d(tag, this, throwable)
        }

    fun i(tag: String, msg: String?, throwable: Throwable? = null) =
        msg?.run {
            Log.i(tag, this, throwable)
        }

    fun w(tag: String, msg: String?, throwable: Throwable? = null) =
        msg?.run {
            Log.w(tag, this, throwable)
        }

    fun e(tag: String, msg: String?, throwable: Throwable? = null) =
        msg?.run {
            Log.e(tag, this, throwable)
        }

}

Network package

网络包包含用于检查互联网连接性的代码。它会 ping Google 的轻量级服务器以了解网络连接状态。在应用程序中了解互联网连接性至关重要。

请参考 GitHub 存储库获取代码。

Utils package

此包包含帮助程序类。

为什么我们应该在 helper 类中有 Coroutine Dispatcher Provider

协程中的硬编码分发器使协程的可测试性更加困难。如果您在 Android Studio 中集成了它,Sonar Lint 将报告警告。因此,协程的分发器从此辅助类中注入。

请参考下面的文章系列,以了解有关 Kotlin 协程的更多信息。

https://levelup.gitconnected.com/the-ultimate-kotlin-coroutine-concept-test-on-android-part-1-826d8066d0c4?source=post_page-----04c9d8614c27--------------------------------

Core module

职责:
核心模块包括用例、存储库、数据模型和数据源。您甚至可以有一个以上的模块,每个模块有两个层级。决策应根据您的需求进行。
Use cases(用例)→ 封装业务逻辑并协调交互。
Repositories(存储库)→ 充当数据访问的抽象层,决定使用哪个数据源。
Data models(数据模型)→ 表示应用程序数据的结构。
Data sources(数据源)→ 处理与底层存储的直接交互,管理读写操作。
这种分层方法促进了Android应用程序中的模块化、可扩展性,并且便于进行测试。即使您使用iOS或Android,此核心模块也不会被大幅修改。它独立于设备形态、操作系统和本地化。

core module

DataSource package

DataSource → 处理与底层存储的直接交互,管理读写操作。

package com.evicky.core.dataSource

import com.evicky.utility.logger.Log
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume

interface IDataSource {
    suspend fun writeData(
        documentPath: String,
        data: Any,
        logTag: String,
        writeSuccessLogMessage: String,
        writeFailureLogMessage: String
    ): Boolean

    suspend fun readData(logTag: String, documentPath: String): Map<String, Any>?

    suspend fun deleteData(
        documentPath: String,
        logTag: String,
        writeFailureLogMessage: String
    ): Boolean

}

class Firestore : IDataSource {
//    private val fireStore: FirebaseFirestore = FirebaseFirestore.getInstance()
    override suspend fun writeData(
        documentPath: String,
        data: Any,
        logTag: String,
        writeSuccessLogMessage: String,
        writeFailureLogMessage: String,
    ) = suspendCancellableCoroutine { continuation ->
        try {
//            fireStore.document(documentPath).set(data)
//                .addOnCompleteListener { task ->
//                    if (task.isSuccessful) {
//                        Log.i(logTag, "Data write success to firestore db $writeSuccessLogMessage: $documentPath")
//                        if (continuation.isActive) continuation.resume(true)
//                    } else {
//                        Log.e(logTag,
//                            "Data write failed to firestore db $writeFailureLogMessage: $documentPath. Error Message: ${task.exception?.message}",
//                            task.exception)
//                        if (continuation.isActive) continuation.resume(false)
//                    }
//                }
//            sendSuccessResultIfOfflineForDocumentWrite(fireStore.document(documentPath), continuation)
        } catch (exception: Exception) {
            Log.e(logTag,
                "Exception from execution: Data write failed to firestore db. $writeFailureLogMessage: $documentPath. Error Message: ${exception.message}",
                exception)
            if (continuation.isActive) continuation.resume(false)
        }
    }

    override suspend fun readData(logTag: String, documentPath: String): Map<String, Any>? =
//        fireStore.document(documentPath).get().await().data
        null
    override suspend fun deleteData(
        documentPath: String,
        logTag: String,
        writeFailureLogMessage: String,
    ) = suspendCancellableCoroutine { continuation ->
        try {
//            fireStore.document(documentPath).delete()
//                .addOnCompleteListener { task ->
//                    if (task.isSuccessful) {
//                        Log.i(logTag, "Delete Success in firestore db: $documentPath")
//                        if (continuation.isActive) continuation.resume(true)
//                    } else {
//                        Log.e(logTag,
//                            "Delete Failure in firestore db: $writeFailureLogMessage: $documentPath. Error Message: ${task.exception?.message}",
//                            task.exception)
//                        if (continuation.isActive) continuation.resume(false)
//                    }
//
//                }
        } catch (exception: Exception) {
            Log.e(logTag,
                "Exception from execution: Delete Failure in firestore db: $writeFailureLogMessage: $documentPath. Error Message: ${exception.message}",
                exception)
            if (continuation.isActive) continuation.resume(false)
        }

    }
}

/**
 * While the device is in offline we won't get success or failure task result for writes to firestore.
 * So getting the meta data to send the result back to the caller if the data is from cache.
 */
//fun sendSuccessResultIfOfflineForDocumentWrite(
//    documentReference: DocumentReference,
//    continuation: CancellableContinuation<Boolean>
//) {
//    documentReference.get().addOnSuccessListener {
//        if (it.metadata.isFromCache && continuation.isActive) continuation.resume(true)
//    }
//}

class DynamoDb : IDataSource {
    override suspend fun writeData(
        documentPath: String,
        data: Any,
        logTag: String,
        writeSuccessLogMessage: String,
        writeFailureLogMessage: String,
    ): Boolean {
        Log.i(logTag, "Data write success to dynamo db $writeSuccessLogMessage: $documentPath")
        return true
    }

    override suspend fun readData(logTag: String, documentPath: String): Map<String, Any>? {
        Log.i(logTag, "Data read success from dynamo db $documentPath")
        return null
    }

    override suspend fun deleteData(
        documentPath: String,
        logTag: String,
        writeFailureLogMessage: String,
    ): Boolean {
        Log.i(logTag, "Data delete success in dynamo db $documentPath")
        return true
    }
}

为什么我们应该为 dataSource 有一个接口和类?难道只使用类就不够了吗?因为 repository 将为测试模拟拥有接口和类。

repositorydataSource 是否应该有专门的接口和类?

虽然在存储库中只有一个类对数据源有一个接口也是可行的,但是在 Android Clean Architecture 中为存储库和数据源引入接口提供以下优点:

1)可测试性:

对于存储库:拥有存储库接口使得更容易创建模拟实现以测试更高级别的组件。
对于数据源:拥有数据源接口允许更容易地测试依赖于数据源的组件,因为您可以提供模拟实现。

2)多个实现:

对于存储库:存储库接口允许多个存储库实现(例如远程和本地),轻松切换或合并数据源而不影响更高级别的组件(例如 usecaseviewmodels 等)。
对于数据源:数据源接口允许多个数据源实现(例如 API 和本地数据库),提供灵活性。

3)易于重构:

对于存储库:如果底层数据源实现发生更改,拥有存储库接口可以使您在不影响更高级别的组件的情况下进行更改。
对于数据源:如果数据源实现发生更改,则数据源接口提供了明确的合同,使得更容易更新实现。
在此应用程序中有两个数据源。Google 的 Firestore 和 Amazon 的 DynamoDB。根据用户类型,数据源会动态地绑定到应用程序。用户类型决策是在存储库层中进行的。稍后将讨论这个问题。

出于安全原因,Firestore 已从 GitHub 存储库中删除。DynamoDb 实现没有实际代码,如果需要,您可以实现它。

我相信以上代码对开发人员很友好。如果不是,请告诉我。我会尝试解释一下。

Repo package

Repositories → 充当数据访问的抽象层,决定使用哪个数据源。

package com.evicky.core.repo

import com.evicky.core.dataSource.DynamoDb
import com.evicky.core.dataSource.Firestore
import com.evicky.core.dataSource.IDataSource
import com.evicky.core.model.local.SignInLocalData

interface ISignInRepo {
    suspend fun writeData(documentPath: String, data: SignInLocalData, logTag: String): Boolean
    suspend fun readData(logTag: String, path: String): Map<String, Any>
}

class SignInRepo(firestore: Firestore, dynamoDb: DynamoDb): ISignInRepo {

    private val isPremiumUser = false // This data may come from some other data source
    private val dataSource: IDataSource = if (isPremiumUser) dynamoDb else firestore

    override suspend fun writeData(documentPath: String, data: SignInLocalData, logTag: String): Boolean = dataSource.writeData(
            documentPath = documentPath, data =  data, logTag = logTag, writeSuccessLogMessage = "Data written successfully", writeFailureLogMessage = "Data write failed")

    override suspend fun readData(logTag: String, path: String): Map<String, Any> = dataSource.readData(logTag, documentPath = path) ?: mapOf()

}

在数据源包部分,为什么同时需要接口和类的原因已经说明。在这里,接口用于关注点分离和测试。

数据源的选择是由存储库层决定的。在这里,数据源是通过 isPremiumUser 布尔状态的帮助来选择的。这些数据来自其他数据源。它可以来自 jetpack datastore,或者像 mongoDB 或 Firestore 或 DynamoDB 这样的单独数据库,或者来自应用内购买信息。在这里,为了简单起见,它是硬编码的。

数据源是单例,由 koin di 框架注入。

Firestore 在此处被选为数据源。

Model package

Data models → 表示应用程序数据的结构。

package com.evicky.core.model.local

data class SignInLocalData(val id: Long, val name: String)
package com.evicky.core.model.remote

import android.os.Parcelable
import kotlinx.parcelize.Parcelize

@Parcelize
data class SignInRemoteData(val id: Long, val name: String, val address: String, val occupat

在登录过程中有两个数据模型。

SignInRemoteData 是从数据源获取的,它包含了已登录用户的所有详细信息。

SignInLocalData 是从 SignInRemoteData 派生而来的。因为用户界面不需要已登录用户的每个细节,所以在更高级别的组件中(如用例、视图模型、组合等)使用 SignInLocalData。

Usecase package

Use cases → 封装业务逻辑并协调交互。

package com.evicky.core.usecase

import com.evicky.core.model.local.SignInLocalData
import com.evicky.core.model.remote.SignInRemoteData
import com.evicky.core.repo.ISignInRepo
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json

class SignInUseCase(private val signInRepo: ISignInRepo) {

    suspend fun writeData(data: SignInLocalData, logTag: String): Boolean {
        return signInRepo.writeData(documentPath = "Users/UserId", data = data, logTag = logTag)
    }

    suspend fun readData(path: String, logTag: String): SignInLocalData {
        val signInRemoteData = Json.decodeFromString<SignInRemoteData>(signInRepo.readData(logTag = logTag, path = path).toString())
        return SignInLocalData(id = signInRemoteData.id, name = signInRemoteData.name)
    }

}

存储库是一个工厂(每个注入将给出一个新实例),由 koin di 框架注入。

用例具有纯业务逻辑。它将数据源的 SignInRemoteData 转换为更高级别组件的 SignInLocalData

这里使用 Kotlinx-serialization 库进行 Json 数据解析。

https://github.com/Kotlin/kotlinx.serialization

什么是业务逻辑?

业务逻辑是指特定的规则、计算和操作,定义了应用程序如何处理和操作数据以实现业务目标。在软件开发的背景下,业务逻辑通常封装在应用程序的后端中,远离用户界面,并负责实现系统的核心功能。

在清洁架构或类似的架构模式中,业务逻辑通常被组织成用例。用例表示系统向其用户提供的特定、离散的功能。用例封装了执行特定操作或实现应用程序中特定目标所需的逻辑。

Feature module

职责:
feature module充当表示层。它负责向用户呈现用户界面。
它包含了每个应用程序特性的视图模型(状态持有者(state holder))、Compose 导航图和组合(用户界面)(compose navgraph and the composable (UI) )。

feature module
SignIn Screen
包含上面Screen的ui 和 state holder代码。

什么是状态持有者?

“state holder”通常指的是一种机制,用于在配置更改(如屏幕旋转)或 Android 组件(如 Activity 或 Fragment)的生命周期中管理和保留与 UI 相关的数据。

ViewModel 旨在以适应生命周期的方式持有和管理与 UI 相关的数据,使数据能够在配置更改时保留。这是通过将 ViewModel 与特定的生命周期关联起来实现的,通常与关联的 UI 组件的生命周期相关联。

这并不意味着只有 viewModel 才能作为状态持有者。一个普通的类也可以充当状态持有者。请参考下面的链接。

https://developer.android.com/jetpack/compose/state-hoisting?#classes-as-state-owner
https://developer.android.com/jetpack/compose/state-hoisting?#viewmodels-as-state-owner

SignInGraph:

是否需要为多个屏幕创建多个活动或片段?

不需要。在引入Jetpack Compose及其导航组件后,这个说法已经过时。

使用Jetpack Compose进行导航

导航组件为Jetpack Compose应用程序提供支持。您可以在不同的组合项之间进行导航。

在此应用程序中,使用了Compose导航进行屏幕导航。

以下是Jetpack Compose导航的工作原理概述:

  • NavHost: NavHost作为可导航到的屏幕(组合项)的容器。
  • NavGraph: NavHost包含定义导航路由的NavGraph。它指定了应用程序中不同组合项之间的连接。
  • Navigation Actions: 通过调用导航操作来触发导航。例如,您可以使用navigate函数从一个组合项跳转到另一个组合项。
  • Back Navigation: Jetpack Compose还提供了使用navController的popBackStack()函数处理返回导航的方法。

现在您已经了解了导航的工作原理。让我们进入代码。

package com.evicky.feature.signIn

import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import com.evicky.feature.util.SIGNIN_ROUTE
import org.koin.androidx.compose.koinViewModel

fun NavGraphBuilder.signInNavGraph(
    onSignInPress: (String) -> Unit
) {
    composable(route = SIGNIN_ROUTE) {
        val viewModel: SignInViewModel =  koinViewModel()
        val loginUiState by viewModel.signInUiState.collectAsStateWithLifecycle()
        SignInScreen(
            signInUiState = loginUiState,
            onSignInPress = { if (loginUiState.errorMessage.isEmpty() && loginUiState.phoneNumber.isNotEmpty()) onSignInPress.invoke(it) },
            onPhoneNumberChange = { updatedValue ->
                viewModel.onPhoneNumberChange(updatedValue)
            }
        )
    }
}

这个NavGraphBuilder扩展函数的调用者位于App模块的Root包中。

MainActivity中的RootHost()函数调用了NavHostNavHost调用了这个SignInNavGraph

为了简洁起见,我在这里再次粘贴代码:

@Composable
internal fun RootHost() {
    val rootController = rememberNavController()
    NavHost(
        navController = rootController,
        startDestination = SIGNIN_ROUTE,
    ) {
        signInNavGraph(
            onSignInPress = {
                rootController.navigateToPostLoginScreen(it)
            }
        )
        postLoginNavGraph()
    }

}
  • NavHost应该只有一个NavController。这是通过上面的rememberNavController()函数创建的。
  • NavHost具有控制器和起始目标。起始目标是一个必需的参数,如果没有它,代码将无法运行。
  • 起始目标的NavGraph应该在NavHost的lambda表达式中给出,否则代码将无法运行。
  • SIGNIN_ROUTE是一个字符串,它充当路由字符串,就像Web应用程序的URL一样。例如,app://com.evicky.finance/jetpack。在这里,"jetpack"是特定组合函数的路由,调用导航操作时将呈现屏幕。
  • NavGraph应该至少有一个组合函数,并且该函数应该具有路由。路由应该与NavHost的起始目标相同。否则,应用程序将无法运行。
  • 在这里,NavHost有两个相当于两个屏幕的导航图。
  • NavGraphBuilder.signInNavGraph()有一个名为SignInScreen的屏幕。
  • SignInScreen包含渲染UI的代码。
  • SignInScreen由嵌套的组合函数组成,这些函数将为用户渲染UI。

本文不讨论SignInScreen的代码,因为它超出了本文的范围。

未来将会为Jetpack Compose制作专门的文章系列。请继续关注作者的最新动态。

BuildSrc模块

该模块包含所有模块的Gradle文件的代码。该模块利用TOML文件和Gradle约定插件的功能,提供简洁且可重用的Gradle代码。

查看更多内容:

https://levelup.gitconnected.com/say-goodbye-to-script-duplication-gradle-convention-plugin-for-android-code-reuse-multi-module-21c0eb24447d?source=post_page-----04c9d8614c27--------------------------------

Github

https://github.com/evignesh/FinLedgerPublic

参考

https://developer.android.com/topic/modularization
https://developer.android.com/topic/architecture/intro
https://developer.android.com/jetpack/compose/documentation

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

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

相关文章

软件测试/测试开发丨Python 模块与包 学习笔记

python的程序结构 组成&#xff1a; packagemodulefunction 模块 模块是在代码量变得相当⼤了之后&#xff0c;为了将需要重复使⽤的有组织的代码放在⼀起&#xff0c;这部分代码可以被其他程序引⽤&#xff0c;从⽽使⽤该模块⾥的函数等功能&#xff0c;引⽤的过程叫做导…

NodeJs - Chrome内存分析工具使用

NodeJs - Chrome内存分析工具使用 一. 前期准备二. Chrome 内存分析工具使用2.1 查看快照2.2 使用案例 一. 前期准备 我们下载好相关依赖&#xff1a; npm i v8-profiler-next测试代码&#xff1a; const v8Profiler require(v8-profiler-next) const fs require(fs)funct…

从入门到精通UNet: 让你快速掌握图像分割算法

文章目录 一、UNet 算法简介1.1 什么是 UNet 算法1.2 UNet 的优缺点1.3 UNet 在图像分割领域的应用 二、准备工作2.1 Python 环境配置2.2 相关库的安装 三、数据处理3.1 数据的获取与预处理3.2 数据的可视化与分析 四、网络结构五、训练模型5.1 模型训练流程5.2 模型评估指标5.…

服务器硬件及RAID配置实战

目录 1、RAID的概念 2、RAID的实现方式 3、标准的RAID 3.1 RAID 0 3.2 RAID 1 3.3 RAID 5 3.4 RAID 10 4、建立硬件 RAID的过程步骤 1、进入RAID 1.1 重启服务器 1.2 进入RAID界面 1.3 在RAID界面切换目录 2、创建RAID 2.1 移动到RAID卡 2.2 按F2&#xff0c;选择…

【嵌入式学习笔记-01】什么是UC,操作系统历史介绍,计算机系统分层,环境变量(PATH),错误

【嵌入式学习笔记】什么是UC&#xff0c;操作系统历史介绍&#xff0c;计算机系统分层&#xff0c;环境变量&#xff08;PATH&#xff09;&#xff0c;错误 文章目录 计算机系统分层什么是操作系统&#xff1f; 环境变量什么是环境变量&#xff1f;环境变量的添加&#xff1f;常…

java 纯代码导出pdf合并单元格

java 纯代码导出pdf合并单元格 接上篇博客 java导出pdf&#xff08;纯代码实现&#xff09; 后有一部分猿友叫我提供一下源码&#xff0c;实际上我的源码已经贴在帖子上了&#xff0c;都是同样的步骤&#xff0c;只是加多一点设置就可以了。今天我再次上传一下相对情况比较完整…

WINDOWS 批量修改图片文件名称

博主家里有一台电脑&#xff0c;存放家庭全部的照片和视频&#xff0c;从智能手机和3G网络发展开始&#xff0c;家里的照片和视频越来越多&#xff0c;已经达到上万个文件。终于&#xff0c;博主找到一个方法整理和保存这些珍贵的数据资料。 一、按年代目录整理照片和视频 按年…

【大数据面试知识点】Spark的DAGScheduler

Spark数据本地化是在哪个阶段计算首选位置的&#xff1f; 先看一下DAGScheduler的注释&#xff0c;可以看到DAGScheduler除了Stage和Task的划分外&#xff0c;还做了缓存的跟踪和首选运行位置的计算。 DAGScheduler注释&#xff1a; The high-level scheduling layer that i…

【随口一说】最近的CSDN

这段时间随便发的一篇博文很快就有“点赞”、“收藏”、“关注”的信息&#xff0c; 而且简单看了一眼用户&#xff0c;很多都是空的或者一堆转载&#xff0c; 机器人也太明显了点&#xff0c;很让人不舒服&#xff0c; 不花点心思设计文章评优推送算法反倒用机器人刷热门&…

【C++】类与对象

文章目录 1. 面向过程和面向对象的初步认识2. 类的引入3. 类的定义4. 类的访问限定符及封装4.1 访问限定符4.2 封装 5. 类的作用域6. 类的实例化7. 类对象模型7.1 如何计算类对象的大小7.2 类对象的存储方式猜测7.3 结构体内存对齐规则 8. this指针8.1 this指针的引出8.2 this指…

3294 李白的酒

#include<bits/stdc.h> using namespace std; int main(){int n;double ans;scanf("%d",&n);for(int i1;i<n;i)ans1,ans/2;printf("%.5f",ans); }

一元函数微分学——刷题(10

目录 1.题目&#xff1a;2.解题思路和步骤&#xff1a;3.总结&#xff1a;小结&#xff1a; 1.题目&#xff1a; 2.解题思路和步骤&#xff1a; 首先题目中给了一个要点&#xff0c;就是周期为5&#xff0c;显然要求的那个点和题目没任何关系&#xff0c;所以利用周期为5&…

[DAU-FI Net开源 | Dual Attention UNet+特征融合+Sobel和Canny等算子解决语义分割痛点]

文章目录 概要I Introduction小结 概要 提出的架构&#xff0c;双注意力U-Net与特征融合&#xff08;DAU-FI Net&#xff09;&#xff0c;解决了语义分割中的挑战&#xff0c;特别是在多类不平衡数据集上&#xff0c;这些数据集具有有限的样本。DAU-FI Net 整合了多尺度空间-通…

数据的分片和路由

之前我们讲解了数据的复制&#xff0c;也就是说让多台机器保留相同的副本&#xff0c;适合用于读多写少的问题&#xff0c;我们这里讲的是数据的分片&#xff0c;分片的目的是将数据进行切分让他们分布到不同的机器上&#xff0c;其中一个动机是数据可能很大如果单纯存放到一个…

macos系统中,获取系统的“文件夹”图标,比如“下载文件夹”有不同的文件夹图标,怎么获得这个文件夹图标呢

在macOS系统中&#xff0c;如果您想要获取“下载”文件夹的图标以用作其他用途&#xff08;例如制作快捷方式、自定义应用图标等&#xff09;&#xff0c;可以按照以下步骤操作&#xff1a; 打开Finder。使用Spotlight搜索或者直接导航到 /System/Library/CoreServices/CoreTy…

记矩阵基础概念

转自up&#xff1a;Naruto_Qcsdn&#xff1a;三维空间几何变换矩阵 先贴个站里分享的基础概念。 learn form 肥猫同学VFX b站&#xff1a;会用transform就会用矩阵 移动 旋转 缩放 1.transofrm ——输出变化矩阵 可以移动transform查看变化去理解 位移 缩放 旋转 由此—…

B+树的插入删除

操作 插入 case2的原理,非叶子节点永远和最右边的最左边的节点的值相等。 case3:的基本原理 非叶子节点都是索引节点 底层的数据分裂之后 相当于向上方插入一个新的索引(你可以认为非叶子节点都是索引),反正第二层插入160 都要分裂,然后也需要再插入(因为索引部分不需要重…

redisson作为分布式锁的底层实现

1. redisson如何实现尝试获取锁的逻辑 如何实现在一段的时间内不断的尝试获取锁 其实就是搞了个while循环&#xff0c;不断的去尝试获取锁资源。但是因为latch的存在会在给定的时间内处于休眠状态。这个事件&#xff0c;监听的是解锁动作&#xff0c;如果解锁动作发生。会调用…

YOLOv8改进 | 检测头篇 | ASFF改进YOLOv8检测头(全网首发)

一、本文介绍 本文给大家带来的改进机制是利用ASFF改进YOLOv8的检测头形成新的检测头Detect_ASFF&#xff0c;其主要创新是引入了一种自适应的空间特征融合方式&#xff0c;有效地过滤掉冲突信息&#xff0c;从而增强了尺度不变性。经过我的实验验证&#xff0c;修改后的检测头…

12/31

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 摘要Abstract文献阅读&#xff1a;用于密集预测的多路径视觉Transformer1、研究背景2、方法提出3、相关方法3.1、Vision Transformers for dense predictions3.2、C…