Flutter鸿蒙化中的Plugin

Flutter鸿蒙化中的Plugin

  • 前言
  • 鸿蒙项目内Plugin
    • Flutter端实现
    • 鸿蒙端实现
      • 创建Plugin的插件类
      • 注册Plugin
  • 开发纯Dart的package
  • 为现有插件项目添加ohos平台支持
    • 创建插件
    • 配置插件
    • 编写插件内容
  • 参考资料

前言

大家知道Flutter和鸿蒙通信方式和Flutter和其他平台通信方式都是一样的, 都是使用Platform Channel API来通信。

那么鸿蒙中这些通信的代码是写在哪里? 如何编写的了?
下面我们简单的学习下。

鸿蒙项目内Plugin

在我们开发App的过程中,可能有这样的需求:
在鸿蒙平台上特有的,并且需要调用鸿蒙原生的API来完成的。那么我们可以在在ohos平台上创建一个Plugin的方式来支持这个功能。

示例的通信方式使用:MethodChannel的方式。

Flutter端实现

// flutter端创建一个MethodChannel的通道,通道名称必须和鸿蒙指定, 如果创建的名称不一致,会导致无法通信
final channel = const MethodChannel("com.test.channel");
// flutter给鸿蒙端发送消息
channel.invokeMapMethod("testData");

鸿蒙端实现

创建Plugin的插件类

首先我们需要创建一个插件类, 继承自FlutterPlugin类, 并实现其中的方法

export default class TestPlugin implements FlutterPlugin {
// 通道
  private channel?: MethodChannel;
//获取唯一的类名 类似安卓的Class<? extends FlutterPlugin ts无法实现只能用户自定义
  getUniqueClassName(): string {
    return 'TestPlugin'
  }

// 当插件从engine上分离的时候调用
  onDetachedFromEngine(binding: FlutterPluginBinding): void {
    this.channel?.setMethodCallHandler(null);
  }

// 当插件挂载到engine上的时候调用
  onAttachedToEngine(binding: FlutterPluginBinding): void {
    this.channel = new MethodChannel(binding.getBinaryMessenger(), "com.test.channel");
    //  给通道设置回调监听 
    this.channel.setMethodCallHandler({
      onMethodCall(call: MethodCall, result: MethodResult) {
        switch (call.method) {
          case "testData":
            console.log(`接收到flutter传递过来的参shu ===================`)
            break;
          default:
            result.notImplemented();
            break;
        }
      }
    })
  }
}

注册Plugin

我们创建完Plugin了, 我们还需要再EntryAbility中去注册我们的插件

export default class EntryAbility extends FlutterAbility {
  configureFlutterEngine(flutterEngine: FlutterEngine) {
    super.configureFlutterEngine(flutterEngine)
    this.addPlugin(new TestPlugin());
  }
}

完成上述两步之后,我们就可以使用这个专属鸿蒙的插件来完成鸿蒙上特有的功能了。

开发纯Dart的package

我们知道,flutter_flutter的仓库对于纯Dart开发的package是完全支持的, 对于纯Dart的package我们主要关注Dart的版本支持。

开发纯Dart的命令:flutter create --template=package hello

对于具体如何开发纯Dart的package,Flutter官方已经讲的非常详细, 开发,集成详细 可以参考官方文档。Flutter中的Package开发

为现有插件项目添加ohos平台支持

在我们开发Flutter项目适配鸿蒙平台的时候,会有些插件还没有适配ohos平台, 这个时候我们等华为适配, 或者我们自己下载插件的源码, 然后我们自己在源码中编写适配ohos平台的代码

下面以image_picker插件为示例,来学习下如何为已有插件项目添加ohos的平台支持

创建插件

首先我们需要下载image_picker源码, 然后使用Android studio打开flutter项目。 可以查看到项目的结构

然后通过命令行进入到项目根目录, 执行命令:flutter create . --template=plugin --platforms=ohos

执行完后的目录结构如下:
在这里插入图片描述

配置插件

我们创建完ohos平台的插件之后, 我们需要再Plugin工程的pubspec.yaml配置文件中配置ohos平台的插件。
在这里插入图片描述
当我们配置完成之后, 我们接下来就可以开始编写ohos平台插件相关内容了。

编写插件内容

在我们编写ohos平台的插件内容时, 我们首先需要知道这个插件是通过什么通道, 调用什么方法来和个个平台通信的。 ohos平台的通道名称、调用方法尽量和原来保持一致,有助于理解。

// 执行flutter指令创建plugin插件时, 会自动创建这个类
export default class ImagesPickerPlugin implements FlutterPlugin, MethodCallHandler, AbilityAware {
  private channel: MethodChannel | null = null;
  private pluginBinding: FlutterPluginBinding | null = null;
  // 当前处理代理对象
  private delegate: ImagePickerDelegate | null = null

  constructor() {
  }

  getUniqueClassName(): string {
    return "ImagesPickerPlugin"
  }

  onAttachedToEngine(binding: FlutterPluginBinding): void {
  // 后续用到的页面context,都是需要重binding对象中获取, 如果你直接this.getcontext 等方法获取, 可能不是页面的context
    this.pluginBinding = binding;
    this.channel = new MethodChannel(binding.getBinaryMessenger(), "chavesgu/images_picker");
    this.channel.setMethodCallHandler(this)
  }

  onDetachedFromEngine(binding: FlutterPluginBinding): void {
    this.pluginBinding = null;
    if (this.channel != null) {
      this.channel.setMethodCallHandler(null)
    }
  }

//  插件挂载到ablitity上的时候
  onAttachedToAbility(binding: AbilityPluginBinding): void {
    if (!this.pluginBinding) {
      return
    }
    this.delegate = new ImagePickerDelegate(binding.getAbility().context, this.pluginBinding.getApplicationContext());
  }

  onDetachedFromAbility() {

  }

  onMethodCall(call: MethodCall, result: MethodResult): void {
    if (call.method === "pick") {
      // 解析参数
      let count = call.argument("count") as number;
      // let language = call.argument("language") as string;
      let pickType = call.argument("pickType") as string;
      // let supportGif = call.argument("gif") as boolean;
      // let maxTime = call.argument("maxTime") as number;
      let maxSize = call.argument("maxSize") as number;
      if (this.delegate !== null) {
        this.delegate.pick(count, pickType, maxSize, result)
      }
    } else if (call.method === "saveImageToAlbum" || call.method === "saveVideoToAlbum") {
      // 保存图片
      let filePath = call.argument("path") as string; // 图片路径
      if (this.delegate !== null) {
        this.delegate.saveImageOrVideo(filePath, call.method === "saveImageToAlbum", result)
      }
    } else if (call.method === "openCamera") {
      let pickType = call.argument("pickType") as string;
      let maxSize = call.argument("maxSize") as number;
      if (this.delegate !== null) {
        this.delegate.openCamear(pickType, maxSize, result)
      }
    } else {
      result.notImplemented()
    }
  }
}

注意:这个插件内开发代码是没有代码提示的, 也不会自动检车报错, 只有你运行测试demo时, 编译时才会报错,所以建议大家把插件的功能在一个demo中完成,在把代码拷贝过来。

逻辑实现代码:

import ArrayList from '@ohos.util.ArrayList';
import common from '@ohos.app.ability.common';
import photoAccessHelper from '@ohos.file.photoAccessHelper';
import { BusinessError } from '@kit.BasicServicesKit';
import picker from '@ohos.multimedia.cameraPicker';
import camera from '@ohos.multimedia.camera';
import dataSharePredicates from '@ohos.data.dataSharePredicates';
import { fileUri } from '@kit.CoreFileKit';
import FileUtils from './FileUtils'
import fs from '@ohos.file.fs';
import {
  MethodResult,
} from '@ohos/flutter_ohos';
import abilityAccessCtrl, { PermissionRequestResult } from '@ohos.abilityAccessCtrl';


export default class ImagePickerDelegate {
  // 当前UIAblitity的context
  private context: common.Context
  // 插件绑定的context
  private bindContext: common.Context
  // 构造方法
  constructor(context: common.Context, bindContext: common.Context) {
    this.context = context
    this.bindContext = bindContext
  }

  // 选择相册图片和视频
  pick(count: number, pickType: string, maxSize: number, callback: MethodResult) {
    // 创建一个选择配置
    const photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();

    // 媒体选择类型
    let mineType: photoAccessHelper.PhotoViewMIMETypes = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
    if (pickType === "PickType.all") {
      mineType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_VIDEO_TYPE;
    } else if (pickType === "PickType.video") {
      mineType = photoAccessHelper.PhotoViewMIMETypes.VIDEO_TYPE;
    }
    photoSelectOptions.MIMEType = mineType
    photoSelectOptions.maxSelectNumber = count; // 选择媒体文件的最大数目

    let uris: Array<string> = [];
    const photoViewPicker = new photoAccessHelper.PhotoViewPicker();
    // 通过photoViewPicker对象来打开相册图片
    photoViewPicker.select(photoSelectOptions).then((photoSelectResult: photoAccessHelper.PhotoSelectResult) => {
      uris = photoSelectResult.photoUris;
      console.info('photoViewPicker.select to file succeed and uris are:' + uris);
      this.hanlderSelectResult(uris, callback)
    }).catch((err: BusinessError) => {
      console.error(`Invoke photoViewPicker.select failed, code is ${err.code}, message is ${err.message}`);
    })
  }

  // 处理打开相机照相/录制
  async openCamear(type: string, maxSize: number, callback: MethodResult) {
    // 定义一个媒体类型数组
    let mediaTypes: Array<picker.PickerMediaType> = [picker.PickerMediaType.PHOTO];
    if (type === "PickType.all") {
      mediaTypes = [picker.PickerMediaType.PHOTO, picker.PickerMediaType.VIDEO]
    } else if (type === "PickType.video") {
      mediaTypes = [picker.PickerMediaType.VIDEO]
    }
    try {
      let pickerProfile: picker.PickerProfile = {
        cameraPosition: camera.CameraPosition.CAMERA_POSITION_BACK
      };
      let pickerResult: picker.PickerResult = await picker.pick(this.context,
        mediaTypes, pickerProfile);
      console.log("the pick pickerResult is:" + JSON.stringify(pickerResult));

      // 获取uri的路径和媒体类型
      let resultUri = pickerResult["resultUri"] as string
      let mediaTypeTemp = pickerResult["mediaType"] as string
      // 需要把uri转换成沙河路径
      let realPath = FileUtils.getPathFromUri(this.bindContext, resultUri);
      if (mediaTypeTemp === "video") {
        // 需要获取缩略图
        callback.success([{thumbPath: realPath, path: realPath, size: maxSize}])
      } else {
        // 图片无需设置缩略图
        callback.success([{thumbPath: realPath, path: realPath, size: maxSize}])
      }
    } catch (error) {
      let err = error as BusinessError;
      console.error(`the pick call failed. error code: ${err.code}`);
    }
  }

  // 处理保存图片
  async saveImageOrVideo(path: string, isImage: boolean, callback: MethodResult) {
    let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
    atManager.requestPermissionsFromUser(this.context,
      ['ohos.permission.WRITE_IMAGEVIDEO', 'ohos.permission.READ_IMAGEVIDEO'],
      async (err: BusinessError, data: PermissionRequestResult) => {
        if (err) {
          console.log(`requestPermissionsFromUser fail, err->${JSON.stringify(err)}`);
        } else {
          console.info('data:' + JSON.stringify(data));
          console.info('data permissions:' + data.permissions);
          console.info('data authResults:' + data.authResults);
          //转换成uri
          let uriTemp = fileUri.getUriFromPath(path);
          //打开文件
          let fileTemp = fs.openSync(uriTemp, fs.OpenMode.READ_ONLY);
          //读取文件大小
          let info = fs.statSync(fileTemp.fd);
          //缓存照片数据
          let bufferImg: ArrayBuffer = new ArrayBuffer(info.size);
          //写入缓存
          fs.readSync(fileTemp.fd, bufferImg);
          //关闭文件流
          fs.closeSync(fileTemp);
          let phHelper = photoAccessHelper.getPhotoAccessHelper(this.context);
          try {
            const uritemp = await phHelper.createAsset(isImage ? photoAccessHelper.PhotoType.IMAGE :
            photoAccessHelper.PhotoType.VIDEO, isImage ? 'jpg' : "mp4"); // 指定待创建的文件类型、后缀和创建选项,创建图片或视频资源
            const file = await fs.open(uritemp, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
            await fs.write(file.fd, bufferImg);
            await fs.close(file.fd);

            callback.success(true);
          } catch (error) {
            console.error(`error=========${JSON.stringify(error)}`)
            callback.success(false);
          }
        }
      });

  }


  // 处理选中结果
  hanlderSelectResult(uris: Array<string>, callback: MethodResult) {
    // 定义一个path数组
    let pathList: ArrayList<string> = new ArrayList();
    for (let path of uris) {
      // if (path.search("video") < 0) {
      //   path = await this.getResizedImagePath(path, this.pendingCallState.imageOptions);
      // }
      this.getVideoThumbnail(path)
      let realPath = FileUtils.getPathFromUri(this.bindContext, path);
      pathList.add(realPath);
    }

    let uriModels: UriModel[] = [];
    pathList.forEach(element => {
      uriModels.push({
        thumbPath: element,
        path: element,
        size: 500
      })
    });

    callback.success(uriModels)
  }

  // 获取视频的缩略图
  async getVideoThumbnail(uri: string) {
    //建立视频检索条件,用于获取视频
    console.log("开始获取缩略图==========")
    let predicates: dataSharePredicates.DataSharePredicates = new dataSharePredicates.DataSharePredicates();
    predicates.equalTo(photoAccessHelper.PhotoKeys.URI, uri);
    let fetchOption: photoAccessHelper.FetchOptions = {
      fetchColumns: [],
      predicates: predicates
    };

    // let size: image.Size = { width: 720, height: 720 };
    let phelper = photoAccessHelper.getPhotoAccessHelper(this.context)
    let fetchResult: photoAccessHelper.FetchResult<photoAccessHelper.PhotoAsset> = await phelper.getAssets(fetchOption);
    console.log(`fetchResult=========${JSON.stringify(fetchResult)}`)
    let asset = await fetchResult.getFirstObject();
    console.info('asset displayName = ', asset.displayName);
    asset.getThumbnail().then((pixelMap) => {
      console.info('getThumbnail successful ' + pixelMap);
    }).catch((err: BusinessError) => {
      console.error(`getThumbnail fail with error: ${err.code}, ${err.message}`);
    });
  }
}

// 定义一个返回的对象
interface UriModel {
  thumbPath: string;
  path: string;
  size: number;
}

工具类代码 :

/*
 * Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import common from '@ohos.app.ability.common';
import fs from '@ohos.file.fs';
import util from '@ohos.util';
import Log from '@ohos/flutter_ohos/src/main/ets/util/Log';

const TAG = "FileUtils";

export default class FileUtils {
  static getPathFromUri(context: common.Context | null, uri: string, defExtension?: string) {
    Log.i(TAG, "getPathFromUri : " + uri);
    let inputFile: fs.File;
    try {
      inputFile = fs.openSync(uri);
    } catch (err) {
      Log.e(TAG, "open uri file failed err:" + err)
      return null;
    }
    if (inputFile == null) {
      return null;
    }
    const uuid = util.generateRandomUUID();
    if (!context) {
      return
    }
    {
      const targetDirectoryPath = context.cacheDir + "/" + uuid;
      try {
        fs.mkdirSync(targetDirectoryPath);
        let targetDir = fs.openSync(targetDirectoryPath);
        Log.i(TAG, "mkdirSync success targetDirectoryPath:" + targetDirectoryPath + " fd: " + targetDir.fd);
        fs.closeSync(targetDir);
      } catch (err) {
        Log.e(TAG, "mkdirSync failed err:" + err);
        return null;
      }

      const inputFilePath = uri.substring(uri.lastIndexOf("/") + 1);
      const inputFilePathSplits = inputFilePath.split(".");
      Log.i(TAG, "getPathFromUri inputFilePath: " + inputFilePath);
      const outputFileName = inputFilePathSplits[0];
      let extension: string;
      if (inputFilePathSplits.length == 2) {
        extension = "." + inputFilePathSplits[1];
      } else {
        if (defExtension) {
          extension = defExtension;
        } else {
          extension = ".jpg";
        }
      }
      const outputFilePath = targetDirectoryPath + "/" + outputFileName + extension;
      const outputFile = fs.openSync(outputFilePath, fs.OpenMode.CREATE);
      try {
        Log.i(TAG, "copyFileSync inputFile fd:" + inputFile.fd + " outputFile fd:" + outputFile.fd);
        fs.copyFileSync(inputFile.fd, outputFilePath);
      } catch (err) {
        Log.e(TAG, "copyFileSync failed err:" + err);
        return null;
      } finally {
        fs.closeSync(inputFile);
        fs.closeSync(outputFile);
      }
      return outputFilePath;
    }
  }
}

编写完上述代码就可以运行example工程去测试相关功能了。 当测试完成之后 , 我们可以把整个源码工程拷贝到flutter工程中, 通过集成本地package的方式来集成这个package。或者你可以在发一个新的pacage到pub.dev上, 然后在按照原有方式集成即可。

参考资料

Flutter官方的package开发和使用
开发Plugin

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

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

相关文章

【Docker】搭建一个功能强大的自托管虚拟浏览器 - n.eko

前言 本教程基于群晖的NAS设备DS423的docker功能进行搭建&#xff0c;DSM版本为 DSM 7.2.2-72806 Update 2。 n.eko 支持多种类型浏览器在其虚拟环境中运行&#xff0c;本次教程使用 Chromium​ 浏览器镜像进行演示&#xff0c;支持访问内网设备和公网地址。 简介 n.eko 是…

AIGC视频生成国产之光:ByteDance的PixelDance模型

大家好&#xff0c;这里是好评笔记&#xff0c;公主号&#xff1a;Goodnote&#xff0c;专栏文章私信限时Free。本文详细介绍ByteDance的视频生成模型PixelDance&#xff0c;论文于2023年11月发布&#xff0c;模型上线于2024年9月&#xff0c;同时期上线的模型还有Seaweed&…

《keras 3 内卷神经网络》

keras 3 内卷神经网络 作者&#xff1a;Aritra Roy Gosthipaty 创建日期&#xff1a;2021/07/25 最后修改时间&#xff1a;2021/07/25 描述&#xff1a;深入研究特定于位置和通道无关的“内卷”内核。 &#xff08;i&#xff09; 此示例使用 Keras 3 在 Colab 中查看 GitHub …

【json_object】mysql中json_object函数过长,显示不全

问题&#xff1a;json只显示部分 解决&#xff1a; SET GLOBAL group_concat_max_len 1000000; -- 设置为1MB&#xff0c;根据需要调整如果当前在navicat上修改&#xff0c;只有效本次连接和后续会话&#xff0c;重新连接还是会恢复默认值1024 在my.ini配置文件中新增或者修…

云消息队列 Kafka 版 V3 系列荣获信通院“云原生技术创新标杆案例”

2024 年 12 月 24 日&#xff0c;由中国信息通信研究院&#xff08;以下简称“中国信通院”&#xff09;主办的“2025 中国信通院深度观察报告会&#xff1a;算力互联网分论坛”&#xff0c;在北京隆重召开。本次论坛以“算力互联网 新质生产力”为主题&#xff0c;全面展示中国…

2024 年度学习总结

目录 1. 前言 2. csdn 对于我的意义 3. 写博客的初衷 3.1 现在的想法 4. 写博客的意义 5. 关于生活和博客创作 5.1 写博客较于纸质笔记的优势 6. 致 2025 1. 前言 不知不觉, 来到 csdn 已经快一年了, 在这一年中, 我通过 csdn 学习到了很多知识, 结识了很多的良师益友…

Spring Boot自动配置原理:如何实现零配置启动

引言 在现代软件开发中&#xff0c;Spring 框架已经成为 Java 开发领域不可或缺的一部分。而 Spring Boot 的出现&#xff0c;更是为 Spring 应用的开发带来了革命性的变化。Spring Boot 的核心优势之一就是它的“自动配置”能力&#xff0c;它极大地简化了 Spring 应用的配置…

1.2.神经网络基础

目录 1.2.神经网络基础 1.2.1.Logistic回归 1.2.2 梯度下降算法 1.2.3 导数 1.2.4 向量化编程 1.2.5 正向传播与反向传播 1.2.6.练习 1.2.神经网络基础 1.2.1.Logistic回归 1.2.1.1.Logistic回归 逻辑回归是一个主要用于二分分类类的算法。那么逻辑回归是给定一个x ,…

Matlab总提示内存不够用,明明小于电脑内存

目录 前言情况1&#xff08;改matlab最大内存限制&#xff09;情况2&#xff08;重启电脑&#xff09;情况3 前言 在使用matlab中&#xff0c;有时候需要占用的内存并没有超过电脑内存依旧会报错&#xff0c;提示内存不够用&#xff0c;可以尝试下面几种方法&#xff0c;总有一…

使用AI生成金融时间序列数据:解决股市场的数据稀缺问题并提升信噪比

“GENERATIVE MODELS FOR FINANCIAL TIME SERIES DATA: ENHANCING SIGNAL-TO-NOISE RATIO AND ADDRESSING DATA SCARCITY IN A-SHARE MARKET” 论文地址&#xff1a;https://arxiv.org/pdf/2501.00063 摘要 金融领域面临的数据稀缺与低信噪比问题&#xff0c;限制了深度学习在…

深入解析人工智能中的协同过滤算法及其在推荐系统中的应用与优化

目录 什么是协同过滤算法核心原理基本步骤相似度计算代码实现详解1.流程图2.创建基础的数据结构存储用户评分数据3.计算用户相似度4.获取相似用户5.推荐方法 算法优化建议1. 数据预处理优化去除异常值和噪声数据进行数据标准化使用稀疏矩阵优化存储 2. 相似度计算优化使用局部敏…

react install

react 安装 React 是一个用于构建用户界面的 JavaScript 库。以下是安装 React 的步骤&#xff1a; 使用 Create React App Create React App 是一个官方支持的命令行工具&#xff0c;用于快速搭建 React 应用。 安装 Node.js 和 npm 确保你的计算机上安装了 Node.js 和 npm…

程序员不可能不知道的常见锁策略

前面我们学习过线程不安全问题&#xff0c;我们通过给代码加锁来解决线程不安全问题&#xff0c;在生活中我们也知道有很多种类型的锁&#xff0c;同时在代码的世界当中&#xff0c;也对应着很多类型的锁&#xff0c;今天我们对锁一探究竟&#xff01; 1. 常见的锁策略 注意: …

智启未来,AI筑梦科技新星”------华清远见成都中心2025冬令营圆满结束

2025年1月11日-16日&#xff0c;华清远见成都中心为期6天的“智启未来&#xff0c;AI筑梦科技新星”2025冬令营活动圆满结束。此次活动吸引了众多对人工智能和无人驾驶技术充满热情的学生参与&#xff0c;共同开启了一段点燃科技梦想的精彩旅程。 报道接待 以AI无人驾驶小车为核…

Debezium日常分享系列之:对于从Oracle数据库进行快照的性能优化

Debezium日常分享系列之&#xff1a;对于从Oracle数据库进行快照的性能优化 源数据库Kafka Connect监控测试结果 源数据库 Oracle 19c&#xff0c;本地&#xff0c;CDB数据库主机的I/O带宽为6 GB/s&#xff0c;由此主机上运行的所有数据库共享临时表空间由42个文件组成&#x…

RabbitMQ--延迟队列

&#xff08;一&#xff09;延迟队列 1.概念 延迟队列是一种特殊的队列&#xff0c;消息被发送后&#xff0c;消费者并不会立刻拿到消息&#xff0c;而是等待一段时间后&#xff0c;消费者才可以从这个队列中拿到消息进行消费 2.应用场景 延迟队列的应用场景很多&#xff0c;…

3DsMax设置中文界面

按键盘上的“Win”键&#xff0c;直接输入3dsmax&#xff0c;选择Simplified Chinese打开&#xff0c;之后就都是中文了

opencv在图片上添加中文汉字(c++以及python)

opencv在图片上添加中文汉字&#xff08;c以及python&#xff09;_c opencv绘制中文 知乎-CSDN博客 环境&#xff1a; ubuntu18.04 desktopopencv 3.4.15 opencv是不支持中文的。 这里C代码是采用替换原图的像素点来实现的&#xff0c;实现之前我们先了解一下汉字点阵字库。…

线程同步与Mutex

梦想是逃离世界… 文章目录 一、什么是线程同步&#xff1f;二、线程同步机制三、互斥锁&#xff08;Mutex&#xff09;四、loock 和 unlock五、Mutex的四种类型 一、什么是线程同步&#xff1f; 线程同步(Thread Synchronization)是多线程编程中的一个重要概念&#xff0c;它…

基于SpringBoot和PostGIS的全球首都信息管理设计与实现

目录 前言 一、首都空间表的设计 1、三张空间表的结构 二、SpringBoot后台管理的设计与实现 1、模型层的实现 2、业务层及控制层实现 三、前端的实现与成果可视化 1、新增数据的保存 2、首都的实际管理成果 3、全球首都信息 四、总结 前言 首都&#xff0c;一个国家的…