Jetpack Compose多平台用于Android和IOS

2023-11-14

图片

JetBrains和外部开源贡献者已经努力工作了几年时间来开发Compose Multiplatform,并最近发布了适用于iOS的Alpha版本。自然地,我们对其功能进行了测试,并决定通过使用该框架在iOS上运行我们的Dribbble复制音乐应用程序来进行实验,看看可能会出现什么挑战。

图片

Compose Multiplatform面向桌面和iOS平台,利用Skia的功能,Skia是一个广泛应用于不同平台的开源2D图形库。Google Chrome、ChromeOS、Mozilla Firefox以及JetPack Compose和Flutter等广泛采用Skia作为其引擎。

Compose Multiplatform架构

为了理解Compose Multiplatform的方法,我们首先研究了JetBrains提供的概述,其中包括Kotlin Multiplatform Mobile(KMM)。

图片

 

正如图表所示,Kotlin Multiplatform的一般方法包括:

  1. 1. 为iOS特定的API(如蓝牙、CodeData等)编写代码。

  2. 2. 为业务逻辑创建共享代码。

  3. 3. 在iOS端创建UI。

Compose Multiplatform引入了共享UI代码的能力,不仅可以共享业务逻辑代码,还可以共享UI代码。您可以选择使用本机iOS UI框架(UIKit或SwiftUI),或直接将iOS代码嵌入Compose。我们希望查看我们在Android上复杂的本机UI在iOS上的工作情况,因此我们选择将本机iOS UI代码限制在最小范围内。目前,您只能使用Swift代码编写特定于平台的API,而对于特定于平台的UI,可以使用Kotlin和Jetpack Compose与Android应用程序共享所有其他代码。

如图所示,Kotlin Multiplatform的一般方法包括:

  1. 1. 编写专门针对iOS API(如蓝牙和CodeData)的代码。

  2. 2. 创建用Kotlin编写的共享业务逻辑代码。

  3. 3. 在iOS端创建UI。

Compose Multiplatform扩展了代码共享的功能,现在您不仅可以共享业务逻辑代码,还可以共享UI代码。您仍然可以使用SwiftUI创建UI,或将UIKit直接嵌入Compose,我们将在下面进行讨论。通过这个新的开发方式,您只需要使用Swift代码来处理特定于平台的API和UI,而可以使用Kotlin和Jetpack Compose与Android应用程序共享其他所有代码。现在,让我们深入探讨启动所需的准备工作。

在iOS上运行的先决条件

获取iOS设置说明的最佳位置是官方文档本身。总结如下,以下是开始的所需条件:

  • • Mac电脑

  • • Xcode

  • • Android Studio

  • • Kotlin Multiplatform Mobile插件

  • • CocoaPods依赖管理器

此外,JetBrains存储库中提供了一个模板,可以帮助处理多个Gradle设置。

https://github.com/JetBrains/compose-multiplatform-ios-android-template/#readme

项目结构

设置了基础项目后,您将看到三个主要目录:

  • • androidApp

  • • iosApp

  • • shared

androidApp和shared是模块,因为它们与Android相关并使用build.gradle构建。iosApp是实际iOS应用程序的目录,您可以通过Xcode打开。androidApp模块只是Android应用程序的入口点。以下代码对于任何曾经为Android开发过的人来说都是熟悉的。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MainView()
        }
    }
}

iosApp是iOS应用程序的入口点,其中包含一些样板式的SwiftUI代码:

import SwiftUI

@main
struct iOSApp: App {
 var body: some Scene {
  WindowGroup {
   ContentView()
  }
 }
}

由于这是入口点,您应该在这里实现顶层的更改——例如,我们添加了ignoresSafeArea修饰符,以在全屏显示应用程序:

struct ComposeView: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> UIViewController {
        return Main_iosKt.MainViewController()
    }
    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}

struct ContentView: View {
    var body: some View {
        ComposeView()
            .ignoresSafeArea(.all)
    }
}

上述代码已经可以在iOS上运行您的Android应用程序。在这里,您的ComposeUIViewController被包装在一个UIKit的UIViewController中,并呈现给用户。MainViewController()位于名为main.ios.kt的Kotlin文件中,而App()包含了Compose应用程序的代码。

fun MainViewController() = ComposeUIViewController { App()}

以下是JetBrains提供的另一个示例。

https://github.com/JetBrains/compose-multiplatform/tree/master/examples/chat

如果您需要一些特定于平台的功能,可以使用UIKitView将UIKit嵌入到Compose代码中。以下是JetBrains的一个地图视图示例。在使用UIKit时,与在Compose中使用AndroidView非常相似,如果您已经熟悉该概念的话。

https://github.com/JetBrains/compose-multiplatform/blob/ea310cede5f08f7960957369247a6575f7bc5392/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/view/LocationVisualizer.ios.kt#L7

shared模块是这三个模块中最重要的一个。这个Kotlin模块实质上包含了Android和iOS实现的共享逻辑,促进了在两个平台上使用相同的代码库。在shared模块中,您会发现三个目录,每个目录都有自己的用途:commonMain、androidMain和iosMain。这是一个令人困惑的地方-实际上,实际共享的代码位于commonMain目录中。其他两个目录用于编写特定于平台的Kotlin代码,在Android或iOS上会有不同的行为或外观。这是通过在commonMain代码中编写expect fun,并在相应的平台目录中使用actual Fun来实现的。

Migration

在开始迁移时,我们确信会遇到一些需要特定修复的问题。尽管我们选择迁移的应用程序在逻辑上非常简单(基本上只有UI、动画和过渡),但如预期的那样,我们遇到了相当多的困难。以下是在迁移过程中可能遇到的一些问题。

Resource

我们首先要处理的是资源的使用。没有动态生成的R类,这仅适用于Android。相反,您需要将资源放在资源目录中,并将路径指定为字符串。以下是一个图像的示例:

import org.jetbrains.compose.resources.painterResource

Image(
    painter = painterResource(“image.webp”),
    contentDescription = "",
)

当以这种方式实现资源时,如果资源名称不正确,可能会发生运行时崩溃,而不是编译时崩溃。

此外,如果您在XML文件中引用Android资源,还需要摆脱与Android平台的链接。

<vector xmlns:android="http://schemas.android.com/apk/res/android" 
    android:width="24dp"
    android:height="24dp" 
-   android:tint="?attr/colorControlNormal"     
+   android:tint="#000000"
    android:viewportWidth="24"
    android:viewportHeight="24">
-   <path android:fillColor="@android:color/white"
+   <path android:fillColor="#000000"
        android:pathData="M9.31,6.71c-0.39,0.39 -0.39,1.02 0,1.41L13.19,12l-3.88" />
</vector>

Font

在编写本文时,Compose Multiplatform中没有使用iOS和Android上常用的标准字体加载技术的方法。据我们所见,Jetbrains建议使用字节数组来加载字体,如下所示的iOS代码:

private val cache: MutableMap<String, Font> = mutableMapOf()

@OptIn(ExperimentalResourceApi::class)
@Composable
actual fun font(name: String, res: String, weight: FontWeight, style: FontStyle): Font {
    return cache.getOrPut(res) {
        val byteArray = runBlocking {
            resource("font/$res.ttf").readBytes()
        }
        androidx.compose.ui.text.platform.Font(res, byteArray, weight, style)
    }
}

然而,我们不喜欢异步的方法,也不喜欢在执行过程中阻塞主线程的runBlocking的使用。因此,在Android上,我们决定采用一种更常见的方法,使用整数标识符:

@Composable
actual fun font(name: String, res: String, weight: FontWeight, style: FontStyle): Font {
    val context = LocalContext.current
    val id = context.resources.getIdentifier(res, "font", context.packageName)
    return Font(id, weight, style)
}

使用时创建Font对象:

object Fonts {
    @Composable
    fun abrilFontFamily() = FontFamily(
        font(
            "Abril",
            "abril_fatface",
            FontWeight.Normal,
            FontStyle.Normal
        ),
    )
}

使用Kotlin替换Java

图片

在Compose Multiplatform中不可能使用Java代码,因为它使用Kotlin编译器插件。因此,我们需要重写使用到Java代码的部分。例如,在我们的应用中,一个时间格式化器将音乐曲目的时间从秒转换为更方便的分钟格式。我们不得不放弃使用java.util.concurrent.TimeUnit,但事实证明这是好事,因为它给了我们重构代码并更优雅地编写代码的机会。

fun format(playbackTimeSeconds: Long): String {
-   val minutes = TimeUnit.SECONDS.toMinutes(playbackTimeSeconds)
+   val minutes = playbackTimeSeconds / 60
-   val seconds = if (playbackTimeSeconds < 60) {
-       playbackTimeSeconds
-   } else {
-       (playbackTimeSeconds - TimeUnit.MINUTES.toSeconds(minutes))
-   }
+   val seconds = playbackTimeSeconds % 60
    return buildString {
        if (minutes < 10) append(0)
        append(minutes)
        append(":")
        if (seconds < 10) append(0)
        append(seconds)
    }
}

Native Canvas

有时候,我们会使用Android Native画布来创建绘图。然而,在Compose Multiplatform中,我们无法在通用代码中访问Android原生画布,因此代码必须进行相应的调整。例如,我们有一个动画标题文本,它依赖于本机画布的measureText(letter)函数,以实现逐字动画效果。我们不得不为这个功能寻找替代方法,所以我们使用了Compose画布来重写它,并使用TextMeasurer代替Paint.measureText(letter)

fun format(playbackTimeSeconds: Long): String {
-   val minutes = TimeUnit.SECONDS.toMinutes(playbackTimeSeconds)
+   val minutes = playbackTimeSeconds / 60
-   val seconds = if (playbackTimeSeconds < 60) {
-       playbackTimeSeconds
-   } else {
-       (playbackTimeSeconds - TimeUnit.MINUTES.toSeconds(minutes))
-   }
+   val seconds = playbackTimeSeconds % 60
    return buildString {
        if (minutes < 10) append(0)
        append(minutes)
        append(":")
        if (seconds < 10) append(0)
        append(seconds)
    }
}

图片

图片

 

drawText方法也依赖于本机画布,因此必须进行重写:

图片

Gestures

在Android上,BackHandler始终可用 - 它处理后退手势或后退按钮按下,具体取决于设备可用的导航模式。但是这种方法在Compose Multiplatform中不起作用,因为BackHandler是Android源集的一部分。相反,让我们使用expect fun

@Composable
expect fun BackHandler(isEnabled: Boolean, onBack: ()-> Unit)

//Android implementation
@Composable
actual fun BackHandler(isEnabled: Boolean, onBack: () -> Unit) {
  BackHandler(isEnabled, onBack)
}

在iOS中,可以提出许多不同的方法来实现所需的结果。例如,您可以在Compose中编写自己的后退手势,或者如果应用中有多个屏幕,可以将每个屏幕包装在单独的UIViewController中,并使用包含默认手势的本机iOS导航器UINavigationController

我们选择了一种在iOS侧处理后退手势的实现方式,而无需将单独的屏幕包装在相应的控制器中(因为我们的应用程序中的视图之间的过渡是高度定制的)。这是如何将这两种语言链接在一起的很好的示例。首先,我们添加了一个原生的iOS SwipeGestureViewController来检测手势,并为手势事件添加了处理程序。完整的iOS实现可以在这里看到。

https://github.com/exyte/ComposeMultiplatformDribbbleAudio/blob/main/iosApp/iosApp/ContentView.swift

struct SwipeGestureViewController: UIViewControllerRepresentable {
    var onSwipe: () -> Void
    
    func makeUIViewController(context: Context) -> UIViewController {
        let viewController = Main_iosKt.MainViewController()
        let containerController = ContainerViewController(child: viewController) {
            context.coordinator.startPoint = $0
        }
        
        let swipeGestureRecognizer = UISwipeGestureRecognizer(
            target:
                context.coordinator, action: #selector(Coordinator.handleSwipe)
        )
        swipeGestureRecognizer.direction = .right
        swipeGestureRecognizer.numberOfTouchesRequired = 1
        containerController.view.addGestureRecognizer(swipeGestureRecognizer)
        return containerController
    }
    
    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
    
    func makeCoordinator() -> Coordinator {
        Coordinator(onSwipe: onSwipe)
    }
    
    class Coordinator: NSObject, UIGestureRecognizerDelegate {
        var onSwipe: () -> Void
        var startPoint: CGPoint?
        
        init(onSwipe: @escaping () -> Void) {
            self.onSwipe = onSwipe
        }
        
        @objc func handleSwipe(_ gesture: UISwipeGestureRecognizer) {
            if gesture.state == .ended, let startPoint = startPoint, startPoint.x < 50 {
                onSwipe()
            }
        }
        
        func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
            true
        }
    }
}

然后,在main.ios.kt文件中创建一个相应的函数:

fun onBackGesture() {
    store.send(Action.OnBackPressed)
}

我们可以在Swift中像这样调用这个函数:

public func onBackGesture() {
    Main_iosKt.onBackGesture()
}

我们实现了一个收集动作的存储库。

interface Store {
    fun send(action: Action)
    val events: SharedFlow<Action>
}

fun CoroutineScope.createStore(): Store {
    val events = MutableSharedFlow<Action>()

    return object : Store {
        override fun send(action: Action) {
            launch {
                events.emit(action)
            }
        }
        override val events: SharedFlow<Action> = events.asSharedFlow()
    }
}

该存储库使用store.events.collect方法累积动作。

@Composable
actual fun BackHandler(isEnabled: Boolean, onBack: () -> Unit) {
    LaunchedEffect(isEnabled) {
        store.events.collect {
            if(isEnabled) {
                onBack()
            }
        }
    }
}

这有助于解决两个平台上手势处理的差异,使iOS应用程序在后退导航方面具有原生和直观的体验。

最少Bug

在某些情况下,您可能会遇到一些次要问题,例如在iOS平台上,当点击时,项目会向上滚动以变得可见。您可以将期望的行为(Android)与下面的错误iOS行为进行比较:

图片

这是因为Modifier.clickable在项目被点击时使其获得焦点,从而触发bringIntoView滚动机制。Android和iOS上的焦点管理不同,导致了这种不同的行为。我们通过为项目添加.focusProperties { canFocus = false }修饰符来解决这个问题。

结论

Compose Multiplatform是Kotlin语言在KMM之后的多平台开发的下一个阶段。这项技术为代码共享提供了更多的机会,不仅限于业务逻辑,还包括UI组件。尽管在多平台应用程序中可以结合使用Compose和SwiftUI,但目前看起来并不是很直观。

您应该考虑您的应用程序是否具有可从多个平台共享代码的业务逻辑、UI或功能能力。如果您的应用程序需要许多特定于平台的功能,KMM和Compose Multiplatform可能不是最佳选择。该存储库包含完整的实现。您还可以查看现有的库,以更加了解当前KMM的功能。

https://github.com/terrakok/kmm-awesome

至于我们,我们对Compose Multiplatform印象深刻,并认为一旦发布稳定版本,它可以在我们的实际项目中使用。它最适合于UI较重的应用程序,没有大量特定于硬件的功能。它可能是Flutter和原生开发的可行替代方案,但时间将证明一切。与此同时,我们将继续专注于原生开发-请查看我们的iOS和Android文章!

 

转自:Android Jetpack Compose多平台用于Android和IOS

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Jetpack Compose多平台用于Android和IOS 的相关文章

  • Phonegap从Java代码获取本地存储值?

    我已经使用phonegap在客户端保存了数据本地存储 http docs phonegap com en 1 2 0 phonegap storage storage md html现在我想用java代码访问保存的数据 这可能吗 我怎样才能
  • 设置显式注释处理器

    我正在尝试将 Maven 存储库添加到我的 Android Studio 项目中 当我进行 Gradle 项目同步时 一切都很好 但是 每当我尝试构建我的 apk 时 都会收到此错误 Execution failed for task ap
  • 从具有 Holo 父主题的 Theme.Light 继承 editText

    我想继承editText from android Theme而我的父主题是android Theme Holo Light 有没有什么 干净 的方法可以将资源从 android sdk 文件夹复制到我的项目中 所以我的想法是有一个自定义主
  • android 6.0运行时需要哪些权限

    我有一个 Android 代码 可以在 Android 5 0 版本上正常运行 我的AndroidManifest xml is
  • 在 OpenGL ES 1.1 中将多个纹理绑定到一个网格

    如果我有一个网格 例如有 6 个面的立方体 每个面分别由 4 个顶点组成 总共 24 个顶点 并且我想对每个面应用不同的纹理 我该怎么做 目前 我使用 glDrawElements 一次绘制整个网格 立方体的所有 6 个面 将所有索引提供到
  • C++ OpenCV imread 无法在 Android 中工作

    我正在尝试读取 C 代码中的图像 LOGD Loading image s n inFile c str Mat img imread inFile CV LOAD IMAGE GRAYSCALE CV Assert img data 0
  • “赠送”应用内购买 Android

    有没有办法将 Google Billing 中的应用内购买 赠送 给特定帐户 我把这个问题放在这里是因为如果有一种方法可以以编程方式完成它 那很好 但不是必须的 在官方文档中找不到任何相关内容 我想要这个的原因是因为我的一个应用程序目前处于
  • Android Studio 中过时的 Kotlin 运行时警告

    下载并安装最新的 Kotlin 插件后 我有过时的 Kotlin 运行时来自 Android Studio 的警告告诉我 您在 kotlin stdlib 1 1 2 库中的 Kotlin 运行时版本是 1 1 2 而插件版本是1 1 2
  • 从服务(IntentService)和活动执行的AsyncTask - 有区别吗?

    从 Activity 或 IntentService 启动 AsyncSync 之间有什么区别吗 我正在构建一个通过 http 下载和上传文件的应用程序 我为每次传输使用带有进度条的自定义通知布局 我选择并行传输或将它们放入队列 您会推荐哪
  • 如何布局文本以围绕图像流动

    你能告诉我是否有一种方法来布局文本 围绕图像 像这样 text text text text text text text text text text text text text text text text text 我已经收到一位
  • Android Facebook SDK - 无法接收访问令牌

    我正在尝试在我的 Android 应用程序中使用 Facebook SDK 这是片段 Facebook myFacebook new Facebook 123456789012345 myFacebook authorize LogInSc
  • Android 计费客户端库:如何指定开发人员有效负载(额外数据)

    我正在尝试使用新的Android计费客户端库 1 0 https developer android com google play billing billing library html 以前 在尝试执行购买时 可以选择向意图添加额外的
  • 如何在活动之间传递大型集合(主从流)

    背景 我正在实现一个从网络服务读取有关电影信息的应用程序 该网络服务返回有关每部电影的一些信息 标题 日期 海报网址 导演 演员等 该 Web 服务支持分页 因此电影以 100 部为一组加载 执行 这个想法是显示一个包含所有海报的网格 当用
  • 如何将数据一次性插入sqlite数据库

    我需要将数据添加到 sqlite 数据库一次 也就是说 我希望我的应用程序的用户看到该数据已加载 如何做到这一点 我使用查询执行了它 INSERT INTO TABLE NAME VALUES 值1 值2 值3 值N 但是每次应用程序打开该
  • 在Android Dialog中,如何为startActivityForResult设置onActivityResult?

    从活动中 我可以轻松设置onActivityResult 并打电话startActivityForResult 一切正常 现在 我需要打电话startActivityForResult 从对话框中 但我无法设置onActivityResul
  • registerForActivityResult TakePicture 未触发

    我尝试使用新的 registerForActivityResult 来拍照 我可以打开相机意图 但拍照后 未触发回调 并且我在 logcat 上看不到任何有关 Activity Result 或错误的信息 我也尝试了RequestPermi
  • 如何在android中格式化长整型以始终显示两位数

    我有一个倒计时器 显示从 60 到 0 的秒数 1 分钟倒计时器 当它达到 1 位数字 例如 9 8 7 时 它显示 9 而不是 09 我尝试使用String format B 02d B x 我将 x 从 long 转换为字符串 它不起作
  • Android ProGuard 混淆库:让类无法工作

    Intro 我在 AS 1 项目中有 2 个模型 带有一些 公共 API 类 的 Android 库项目 Android APP依赖上述库 库模块在依赖列表中 Task 我想混淆我的库项目 因为我想将其公开为公共 SDK 但又要保护我的代码
  • 隐式意图和显式意图之间的区别[重复]

    这个问题在这里已经有答案了 我对之间的区别感到困惑implicit and explicit意图 隐式意图和显式意图的目的是什么 为什么使用这些概念 我是 Android 应用程序的新手 所以请提供一些示例 隐式活动调用 使用意图过滤器 您
  • Android 并获取 id 转换为字符串的视图

    在 Android 项目的 Java 代码中 如果您想要视图资源的引用 您可以执行以下操作 View addButton findViewById R id button 0 在上面的 R id button 0 不是一个字符串 是否可以通

随机推荐

  • (下)苹果有开源,但又怎样呢?

    一开始 因为 MacOS X 苹果与 FreeBSD 过往从密 不仅挖来 FreeBSD 创始人 Jordan Hubbard 更是在此基础上开源了 Darwin 但是 苹果并没有给予 Darwin 太多关注 作为苹果的首个开源项目 它算不
  • OpenCV---膨胀与腐蚀

    膨胀 腐蚀 一 膨胀实现dilate import cv2 as cv import numpy as np def dilate demo image 膨胀 print image shape gray cv cvtColor image
  • 【计算机网络系列】物理层①:物理层的基本概念以及数据通信的基础知识

    本文主要介绍物理层的基本概念以及数据通信的基础知识 同时简单谈谈物理层下面的传输媒体 一 物理层基本概念 首先要强调指出 物理层考虑的是怎样才能在连接各种计算机的传输媒体上传输数据比特流 而不是指具体的传输媒体 大家知道 现有的计算机网络中
  • AI,正在疯狂进化,金融大模型来了

    大家好 现在开源社区 更新速度最快的就是 AI 相关的项目了 几天不看 就又多了一些非常优秀的项目 一 FinGPT 之前我就发过各个领域的大语言模型 比如医学领域的 Huatuo LLaMA 也发过法律领域的大语言模型 LaWGPT 现在
  • 解决Windows丢失msvcp120.dll问题

    其实很多用户玩单机游戏或者安装软件的时候就出现过这种问题 如果是新手第一时间会认为是软件或游戏出错了 其实并不是这样 其主要原因就是你电脑系统的该dll文件丢失了或者损坏了 这时你只需下载这个msvcp120 dll文件进行安装 前提是找到
  • 软件结构化设计-架构真题(二十七)

    2019年 进程P有8个页面 页号分别为0 7 状态位等于1和0分别表示在内存和不在内部才能 假设系统给P分配4个存储块 如果进程P要访问页面6不在内存 那么应该淘汰号是多少 答案 页号2 解析 页号1 2 5 7在内部内存里 而2的被访问
  • Mmdetection训练笔记

    1 imgs per gpu表示一块gpu训练的图片数量 imgs per gpu的值会影响终端输出的显示 比如 如果你有一块GPU 训练集有4000张 imgs per gpu设为2的话 终端的输出可能是Epoch 1 50 2000 另
  • 从Vue2到Vue3【五】——新的组件(Fragment、Teleport、Suspense)

    系列文章目录 内容 链接 从Vue2到Vue3 零 Vue3简介 从Vue2到Vue3 一 Composition API 第一章 从Vue2到Vue3 二 Composition API 第二章 从Vue2到Vue3 三 Composit
  • 前端缓存区别记录 SessionStorage和LocalStorage详解

    LocalStorage和SessionStorage之间的主要区别在于浏览器窗口和选项卡之间的数据共享方式不同 LocalStorage可跨浏览器窗口和选项卡间共享 就是说如果在多个选项卡和窗口中打开了一个应用程序 而一旦在其中一个选项卡
  • spi个人笔记

    spi是全双工通讯 收发同时进行 这句话怎么理解 如上图所示 主机产生一组时钟信号 并通过mosi输出8位数据 这个时候 虽然从机没有返回数据 持续低电平 但是因为是 收发同步 所以此时主机已经采集了此次的miso数据 就是说 无论你的目的
  • 【upload-labs 第四关通关攻略】

    一 类型 无法上传php等多种类型 选择 htaccess配置文件 二 htaccess内容 注意 不能命名 就叫 htaccess
  • Java手写数组和案例拓展

    Java手写数组和案例拓展 1 Mermanid代码绘制的思维导图解释实现思路原理 mermaid svg HoH3kyfEhPDhcUh4 font family trebuchet ms verdana arial sans serif
  • js发布——订阅模式的通用实现及取消订阅

    h1 发布 订阅模式的通用实现 h1 p javaScript作为一门解释执行的语言 给对象添加动态职责是理所当然的 所以我们将发布 订阅的功能提取出来 放在一个单独的对象内 p
  • Javascript创建对象的几种方式及优劣

    1 字面量方式 var obj name tom age 20 career network getName function return this name alert obj getName 这种方式适合创建单个对象 2 创建Obje
  • HttpCanary使用指南——各种神奇的插件

    HttpCanary更多资料 点我 作为目前Android平台最强大的抓包工具 HttpCanary从设计之初就规划了插件功能 2 6 0版本之前称为 模组 基于NetBare框架的虚拟网关 拦截器设计 HttpCanary可以实现非常多的
  • 2020春秋招聘图像处理 人工智能方向 各大厂面试常见题整理一(附答案)(阿里腾讯华为字节)

    因为本人近期也要紧临毕业 面临招聘面试 所以整体别人公开的面经 做一个整理 并且加上自己认为的答案 欢迎各位读者对答案进行指正和交流 深度残差的作用 直观上 深度加深 出现梯度消失和梯度爆炸的问题 在论文中 出现了一个奇怪的现象 就是56层
  • Hive命令的使用

    命令行界面 Command Line Interface CLI 是Hive交互最常见也是最方便的方式 在命令行界面可以执行Hive支持的觉大多数功能 如查询 创建等 hive e 有时 并不需要一直打开命令行界面 也就是说执行完查询后立刻
  • Vue-ref用法

    用ref操作模版中的dom元素 1 在模版中 声明ref名称 div ref引用Dom节点 div 2 用法 change this refs chagneBack style color red 用ref操作组件 1 在组件引用中声明re
  • Redis 汇总

    Redis 0 声明 1 概述 1 1 Redis是什么 1 2 Redis优缺点 1 3 为什么要用 Redis 为什么要用缓存 1 4 Redis为什么这么快 2 常用数据结构 2 1 String 应用场景 2 2 List 应用场景
  • Jetpack Compose多平台用于Android和IOS

    JetBrains和外部开源贡献者已经努力工作了几年时间来开发Compose Multiplatform 并最近发布了适用于iOS的Alpha版本 自然地 我们对其功能进行了测试 并决定通过使用该框架在iOS上运行我们的Dribbble复制