Flutter调用HarmonyOS NEXT原生相机拍摄相册选择照片视频

目录

 

1.项目背景

2.遇到的问题

3.开发准备

4.开发过程

首先创建注册调用鸿蒙原生的渠道

创建并初始化插件

绑定通道完成插件中的功能

5.具体步骤

根据传值判断是相册选取还是打开相机

相册选取照片或视频

相机拍摄照片或视频

调用picker拍摄接口获取拍摄的结果

视频封面缩略图处理

打包缩略图

路径处理

数据返回

6.Flutter调用HarmonyOS原生通过路径上传到服务器

完整代码:


 

1.项目背景

我们的移动端项目是使用Flutter开发,考虑到开发周期和成本,使用了HarmonyOSNEXT(后续简称:鸿蒙)的Flutter兼容库,再将部分三方库更新为鸿蒙的Flutter兼容库,本项目选择相册的图片视频,使用相机拍照拍视频我们使用的是调用Android和iOS的原生方法使用

2.遇到的问题

因为我们使用的是原生方法,所以鸿蒙也得开发一套原生的配合使用,虽然我们也发现鸿蒙的Flutter兼容库中有image_picker这个库,但是在实际线上运行中,部分机型是无法正常工作的,主要是国内厂商深度定制引起的,那根据设备类型判断在纯血鸿蒙手机上用image_picker也是可行的方案,考虑到这样不方便后期维护,所以还是打算使用Flutter通过通道的形式去调用鸿蒙原生方式来实现

3.开发准备

首先得将鸿蒙适配Flutter的SDK下载,具体步骤可以参考:Flutter SDK 仓库,也可以参考我的上一篇文章:Flutter适配HarmonyOS实践_flutter支持鸿蒙系统

4.开发过程

  1. 首先创建注册调用鸿蒙原生的渠道
  2. 创建并初始化插件
  3. 绑定通道完成插件中的功能

首先创建注册调用鸿蒙原生的渠道

使用了兼容库后,ohos项目中在entry/src/main/ets/plugins目录下会自动生成一个GeneratedPluginRegistrant.ets文件,里面会注册所有你使用的兼容鸿蒙的插件,但是我们不能在这里注册,因为每次build,他会根据Flutter项目中的pubspec.yaml文件中最新的插件引用去重新注册。

我们找到GeneratedPluginRegistrant的注册地:EntryAbility.ets,我们在plugins中创建一个FlutterCallNativeRegistrant.ets,将他也注册一下:

import { FlutterAbility, FlutterEngine } from '@ohos/flutter_ohos';
import FlutterCallNativeRegistrant from '../plugins/FlutterCallNativeRegistrant';
import { GeneratedPluginRegistrant } from '../plugins/GeneratedPluginRegistrant';export default class EntryAbility extends FlutterAbility {configureFlutterEngine(flutterEngine: FlutterEngine) {super.configureFlutterEngine(flutterEngine)GeneratedPluginRegistrant.registerWith(flutterEngine)///GeneratedPluginRegistrant是自动根据引入的插件库生成的,所以调用原生的插件必须新起文件进行单独注册FlutterCallNativeRegistrant.registerWith(flutterEngine,this)}
}

创建并初始化插件

创建FlutterCallNativePlugin插件在FlutterCallNativeRegistrant中初始化

export default class FlutterCallNativeRegistrant {private channel: MethodChannel | null = null;private photoPlugin?:PhotoPlugin;static registerWith(flutterEngine: FlutterEngine) {try {flutterEngine.getPlugins()?.add(new FlutterCallNativePlugin());} catch (e) {}}}

绑定通道完成插件中的功能

绑定MethodChannel定义2个执行方法来调用原生的相册选取照片视频,相机拍摄照片视频:selectPhoto和selectVideo

import { FlutterPlugin, FlutterPluginBinding, MethodCall,MethodCallHandler,MethodChannel, MethodResult } from "@ohos/flutter_ohos";
import router from '@ohos.router';
import PhotoPlugin from "./PhotoPlugin";
import { UIAbility } from "@kit.AbilityKit";export default class FlutterCallNativePlugin implements FlutterPlugin,MethodCallHandler{private channel: MethodChannel | null = null;private photoPlugin?:PhotoPlugin;getUniqueClassName(): string {return "FlutterCallNativePlugin"}onMethodCall(call: MethodCall, result: MethodResult): void {switch (call.method) {case "selectPhoto":this.photoPlugin = PhotoPlugin.getInstance();this.photoPlugin.setDataInfo(call, result ,1)this.photoPlugin.openImagePicker();break;case "selectVideo":this.photoPlugin = PhotoPlugin.getInstance();this.photoPlugin.setDataInfo(call, result ,2)this.photoPlugin.openImagePicker();break;default:result.notImplemented();break;}}onAttachedToEngine(binding: FlutterPluginBinding): void {this.channel = new MethodChannel(binding.getBinaryMessenger(), "flutter_callNative");this.channel.setMethodCallHandler(this)}onDetachedFromEngine(binding: FlutterPluginBinding): void {if (this.channel != null) {this.channel.setMethodCallHandler(null)}}}

5.具体步骤

  • 根据传值判断是相册选取还是打开相机
  • 相册选取照片或视频
  • 相机拍摄照片或视频
  • 视频封面处理
  • 路径处理
  • 数据返回

根据传值判断是相册选取还是打开相机

  openImagePicker() {if (this.type === 1) {this.openCameraTakePhoto()} else if (this.type === 2) {this.selectMedia()} else {this.selectMedia()}}

相册选取照片或视频

用户有时需要分享图片、视频等用户文件,开发者可以通过特定接口拉起系统图库,用户自行选择待分享的资源,然后最终完成分享。此接口本身无需申请权限,目前适用于界面UIAbility,使用窗口组件触发。

这个方式的好处显而易见,不像Android或者iOS还需要向用户申请隐私权限,在鸿蒙中,以下操作完全是系统级的,不需要额外申请权限

1.创建图片媒体文件类型文件选择选项实例

const photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();

2.根据类型配置可选的媒体文件类型和媒体文件的最大数目等参数

if (this.mediaType === 1) {photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE // 过滤选择媒体文件类型为IMAGE} else if (this.mediaType === 2) {photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.VIDEO_TYPE // 过滤选择媒体文件类型为VIDEO}photoSelectOptions.maxSelectNumber = this.mediaType === 2?1:this.maxCount; // 选择媒体文件的最大数目photoSelectOptions.isPhotoTakingSupported=false;//是否支持拍照photoSelectOptions.isSearchSupported=false;//是否支持搜索

还有其他可配置项请参考API文档

3创建图库选择器实例,调用PhotoViewPicker.select接口拉起图库界面进行文件选择。文件选择成功后,返回PhotoSelectResult结果集。

let uris: Array<string> = [];
const photoViewPicker = new photoAccessHelper.PhotoViewPicker();
photoViewPicker.select(photoSelectOptions).then((photoSelectResult: photoAccessHelper.PhotoSelectResult) => {uris = photoSelectResult.photoUris;console.info('photoViewPicker.select to file succeed and uris are:' + uris);
}).catch((err: BusinessError) => {console.error(`Invoke photoViewPicker.select failed, code is ${err.code}, message is ${err.message}`);
})

打印相册选择图片和视频的结果:

photoViewPicker.select to file succeed and uris 
are:file://media/Photo/172/IMG_1736574824_157/IMG_20250111_135204.jpg,file://media/Photo/164/IMG_1736514105_152/image_1736514005016.jpg
photoViewPicker.select to file succeed and uris 
are:file://media/Photo/136/VID_1735732161_009/VID_20250101_194749.mp4

相机拍摄照片或视频

1.配置PickerProfile

说明

PickerProfile的saveUri为可选参数,如果未配置该项,拍摄的照片和视频默认存入媒体库中。

如果不想将照片和视频存入媒体库,请自行配置应用沙箱内的文件路径。

应用沙箱内的这个文件必须是一个存在的、可写的文件。这个文件的uri传入picker接口之后,相当于应用给系统相机授权该文件的读写权限。系统相机在拍摄结束之后,会对此文件进行覆盖写入

 let pathDir = getContext().filesDir;let fileName = `${new Date().getTime()}`let filePath = pathDir + `/${fileName}.tmp`let result: picker.PickerResultfileIo.createRandomAccessFileSync(filePath, fileIo.OpenMode.CREATE);let uri = fileUri.getUriFromPath(filePath);let pickerProfile: picker.PickerProfile = {cameraPosition: camera.CameraPosition.CAMERA_POSITION_BACK,saveUri: uri};

调用picker拍摄接口获取拍摄的结果

 if (this.mediaType === 1) {result =await picker.pick(getContext(), [picker.PickerMediaType.PHOTO],pickerProfile);} else if (this.mediaType === 2) {result =await picker.pick(getContext(), [picker.PickerMediaType.VIDEO],pickerProfile);}console.info(`picker resultCode: ${result.resultCode},resultUri: ${result.resultUri},mediaType: ${result.mediaType}`);

打印结果:

picker resultCode: 0,resultUri: 
file://com.example.demo/data/storage/el2/base/haps/entry/files/1737443816605.tmp,mediaType: photo
picker resultCode: 0,resultUri: 
file://com.example.demo/data/storage/el2/base/haps/entry/files/1737443929031.tmp,mediaType: video
因为我们配置了saveUri,所以拍摄的图片视频是存在我们应用沙盒中。

视频封面缩略图处理

视频拿到一般都是直接上传,但是有的场景需要将适配封面也拿到,那么路径在沙盒中,就直接一次性处理好

1.创建AVImageGenerator对象

// 创建AVImageGenerator对象let avImageGenerator: media.AVImageGenerator = await media.createAVImageGenerator()

2.根据传入的视频uri打开视频文件

let file = fs.openSync(filePath, fs.OpenMode.READ_ONLY);

3.将打开后的文件配置给avImageGenerator

let avFileDescriptor: media.AVFileDescriptor = { fd: file.fd };avImageGenerator.fdSrc = avFileDescriptor;

4.初始化参数

  let timeUs = 0let queryOption = media.AVImageQueryOptions.AV_IMAGE_QUERY_NEXT_SYNClet param: media.PixelMapParams = {width : 300,height : 400,}

5.异步获取缩略图

 // 获取缩略图(promise模式)let pixelMap = await avImageGenerator.fetchFrameByTime(timeUs, queryOption, param)

6.缩放资源,并返回缩略图

 avImageGenerator.release()console.info(`release success.`)fs.closeSync(file)return pixelMap

打包缩略图

1.创建imagePicker实例,该类是图片打包器类,用于图片压缩和打包

const imagePackerApi: image.ImagePacker = image.createImagePacker();

2.创建配置image.PackingOption

        let packOpts: image.PackingOption = { format: "image/jpeg", quality: 98 }

3.将缩略图打包保存并返回文件路径

imagePackerApi.packing(pixelMap, packOpts).then(async (buffer: ArrayBuffer) => {let fileName = `${new Date().getTime()}.tmp`// //文件操作let filePath = getContext().cacheDir + fileNamelet file = fileIo.openSync(filePath,fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE)fileIo.writeSync(file.fd,buffer)//获取urilet urlStr = fileUri.getUriFromPath(filePath)resolve(urlStr)})

路径处理

因为以上所有的路径都是在鸿蒙设备上的路径,Flutter的MultipartFile.fromFile(ipath)是无法读取纯血鸿蒙设备的路径

01-16 16:23:46.805   17556-17654   A00000/com.gqs...erOHOS_Native  
flutter settings log message: 错误信息:PathNotFoundException: Cannot retrieve length of file, path = 'file://com.example.demo/data/storage/el2/base/haps/entry/files/1737015822716.tmp' (OS Error: No such file or directory, errno = 2)

所以我们需要把路径转换一下:

/** 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;}}}

通过调用FileUtils的静态方法getPathFromUri,传入上下文和路径,就能获取到真正的SD卡的文件地址:

/data/storage/el2/base/haps/entry/cache/53ee7666-7ba4-4f72-9d37-3c09111a2293/1737446424534.tmp

数据返回

   let videoUrl =  this.retrieveCurrentDirectoryUri(uris[0])let coverImageUrl =  this.retrieveCurrentDirectoryUri(videoThumb)map.set("videoUrl", this.retrieveCurrentDirectoryUri(uris[0]));map.set("coverImageUrl", this.retrieveCurrentDirectoryUri(videoThumb));this.result?.success(map);

6.Flutter调用HarmonyOS原生通过路径上传到服务器

上文中我们提到建立通道Channel
MethodChannel communicateChannel = MethodChannel("flutter_callNative");
final result = await communicateChannel.invokeMethod("selectVideo", vars);
if (result["videoUrl"] != null && result["coverImageUrl"] != null) {String? video = await FileUploader.uploadFile(result["videoUrl"].toString());String? coverImageUrl =await FileUploader.uploadFile(result["coverImageUrl"].toString());
}

完整代码:

import { camera, cameraPicker as picker } from '@kit.CameraKit'
import { fileIo, fileUri } from '@kit.CoreFileKit'
import { MethodCall, MethodResult } from '@ohos/flutter_ohos';
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { BusinessError } from '@kit.BasicServicesKit';
import json from '@ohos.util.json';
import FileUtils from '../utils/FileUtils';
import HashMap from '@ohos.util.HashMap';
import media from '@ohos.multimedia.media';
import { image } from '@kit.ImageKit';
import { fileIo as fs } from '@kit.CoreFileKit';/*** @FileName : PhotoPlugin* @Author : kirk.wang* @Time : 2025/1/16 11:30* @Description :  flutter调用鸿蒙原生组件的选择相片、选择视频、拍照、录制视频*/
export default class  PhotoPlugin {private imgSrcList: Array<string> = [];private call?: MethodCall;private result?: MethodResult;///打开方式:1-拍摄,2-相册private type: number=0;///最大数量private maxCount: number=0;///资源类型:1-图片,2-视频,else 所有文件类型private mediaType: number=0;// 静态属性存储单例实例private static instance: PhotoPlugin;// 静态方法获取单例实例public static getInstance(): PhotoPlugin {if (!PhotoPlugin.instance) {PhotoPlugin.instance = new PhotoPlugin();}return PhotoPlugin.instance;}// 提供设置和获取数据的方法public setDataInfo(call: MethodCall, result: MethodResult, mediaType: number) {this.call = call;this.result = result;this.mediaType = mediaType;this.type = this.call.argument("type") as number;this.maxCount = call.argument("maxCount") as number;}openImagePicker() {if (this.type === 1) {this.openCameraTakePhoto()} else if (this.type === 2) {this.selectMedia()} else {this.selectMedia()}}selectMedia() {const photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();if (this.mediaType === 1) {photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE // 过滤选择媒体文件类型为IMAGE} else if (this.mediaType === 2) {photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.VIDEO_TYPE // 过滤选择媒体文件类型为VIDEO}photoSelectOptions.maxSelectNumber = this.mediaType === 2?1:this.maxCount; // 选择媒体文件的最大数目photoSelectOptions.isPhotoTakingSupported=false;//是否支持拍照photoSelectOptions.isSearchSupported=false;//是否支持搜索let uris: Array<string> = [];const photoViewPicker = new photoAccessHelper.PhotoViewPicker();photoViewPicker.select(photoSelectOptions).then(async (photoSelectResult: photoAccessHelper.PhotoSelectResult) => {uris = photoSelectResult.photoUris;console.info('photoViewPicker.select to file succeed and uris are:' + uris);let jsonResult = "";if (this.mediaType === 1) {uris.forEach((uri => {this.imgSrcList.push(this.retrieveCurrentDirectoryUri(uri))}))jsonResult = json.stringify(this.imgSrcList)this.result?.success(jsonResult);} else if (this.mediaType === 2) {let map = new HashMap<string, string>;await this.getVideoThumbPath(uris[0]).then((videoThumb)=>{let videoUrl =  this.retrieveCurrentDirectoryUri(uris[0])let coverImageUrl =  this.retrieveCurrentDirectoryUri(videoThumb)map.set("videoUrl", videoUrl);map.set("coverImageUrl", coverImageUrl);this.result?.success(map);});}console.assert('result  success:'+jsonResult);}).catch((err: BusinessError) => {console.error(`Invoke photoViewPicker.select failed, code is ${err.code}, message is ${err.message}`);})}async openCameraTakePhoto() {let pathDir = getContext().filesDir;let fileName = `${new Date().getTime()}`let filePath = pathDir + `/${fileName}.tmp`let result: picker.PickerResultfileIo.createRandomAccessFileSync(filePath, fileIo.OpenMode.CREATE);let uri = fileUri.getUriFromPath(filePath);let pickerProfile: picker.PickerProfile = {cameraPosition: camera.CameraPosition.CAMERA_POSITION_BACK,saveUri: uri};if (this.mediaType === 1) {result =await picker.pick(getContext(), [picker.PickerMediaType.PHOTO],pickerProfile);} else if (this.mediaType === 2) {result =await picker.pick(getContext(), [picker.PickerMediaType.VIDEO],pickerProfile);} else if (this.mediaType === 3) {result =await picker.pick(getContext(), [picker.PickerMediaType.PHOTO, picker.PickerMediaType.VIDEO],pickerProfile);} else {result =await picker.pick(getContext(), [picker.PickerMediaType.PHOTO, picker.PickerMediaType.VIDEO],pickerProfile);}console.info(`picker resultCode: ${result.resultCode},resultUri: ${result.resultUri},mediaType: ${result.mediaType}`);if (result.resultCode == 0) {if (result.mediaType === picker.PickerMediaType.PHOTO) {let imgSrc = this.retrieveCurrentDirectoryUri(result.resultUri);this.imgSrcList.push(imgSrc);this.result?.success(json.stringify(this.imgSrcList));} else {let map = new HashMap<string, string>;await this.getVideoThumbPath(result.resultUri).then((videoThumb)=>{if(videoThumb!==''){let videoUrl =  this.retrieveCurrentDirectoryUri(result.resultUri)let coverImageUrl =  this.retrieveCurrentDirectoryUri(videoThumb)map.set("videoUrl",videoUrl);map.set("coverImageUrl", coverImageUrl);this.result?.success(map);}});}}}retrieveCurrentDirectoryUri(uri: string): string {let realPath = FileUtils.getPathFromUri(getContext(), uri);return realPath ?? '';}async getVideoThumbPath(filePath:string) {return new Promise<string>((resolve, reject) => {setTimeout(() => {let packOpts: image.PackingOption = { format: "image/jpeg", quality: 98 }const imagePackerApi = image.createImagePacker();this.getVideoThumb(filePath).then((pixelMap)=>{imagePackerApi.packing(pixelMap, packOpts).then(async (buffer: ArrayBuffer) => {let fileName = `${new Date().getTime()}.tmp`// //文件操作let filePath = getContext().cacheDir + fileNamelet file = fileIo.openSync(filePath,fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE)fileIo.writeSync(file.fd,buffer)//获取urilet urlStr = fileUri.getUriFromPath(filePath)resolve(urlStr)})})}, 0);});}///获取视频缩略图getVideoThumb = async (filePath: string) => {// 创建AVImageGenerator对象let avImageGenerator: media.AVImageGenerator = await media.createAVImageGenerator()let file = fs.openSync(filePath, fs.OpenMode.READ_ONLY);let avFileDescriptor: media.AVFileDescriptor = { fd: file.fd };avImageGenerator.fdSrc = avFileDescriptor;// 初始化入参let timeUs = 0let queryOption = media.AVImageQueryOptions.AV_IMAGE_QUERY_NEXT_SYNClet param: media.PixelMapParams = {width : 300,height : 400,}// 获取缩略图(promise模式)let pixelMap = await avImageGenerator.fetchFrameByTime(timeUs, queryOption, param)// 释放资源(promise模式)avImageGenerator.release()console.info(`release success.`)fs.closeSync(file)return pixelMap};}

创作不易,如果我的内容帮助到了你,烦请小伙伴点个关注,留个言,分享给需要的人,不胜感激。

 

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

JavaScript笔记基础篇03——函数

黑马程序员视频地址&#xff1a;黑马程序员前端JavaScript入门到精通全套视频教程https://www.bilibili.com/video/BV1Y84y1L7Nn?vd_source0a2d366696f87e241adc64419bf12cab&spm_id_from333.788.videopod.episodes 目录 函数 函数的使用 1.函数的声明语法 2.函数的…

manim(manimgl)安装教学-win11(2024-08)

manim 目前的两种版本&#xff1a;★★ 稍微捋一捋【项目中的 readme.md 十分重要】 manimgl 是 Grant Sanderson&#xff08;YouTube频道 3Blue1Brown的作者&#xff09;等人开发。 现在为 manimgl&#xff0c;在维护中。 manimCE 是2020年后的 manim 分支 manim community e…

常见Arthas命令与实践

Arthas 官网&#xff1a;https://arthas.aliyun.com/doc/&#xff0c;官方文档对 Arthas 的每个命令都做出了介绍和解释&#xff0c;并且还有在线教程&#xff0c;方便学习和熟悉命令。 Arthas Idea 的 IDEA 插件。 这是一款能快速生成 Arthas命令的插件&#xff0c;可快速生成…

DS18B20温度传感器详解(STM32)

目录 一、介绍 二、传感器原理 1.原理图 2.工作时序 3.工作原理&#xff1a;复位脉冲与应答脉冲 4.工作原理&#xff1a;写时序 5.工作原理&#xff1a;读时序 6.工作原理&#xff1a;DS18B20读取的数据格式 7.工作原理&#xff1a;DS18B20配置步骤 三、程序设计 ma…

Chrome远程桌面无法连接怎么解决?

Chrome远程桌面连接已停止工作 Chrome远程桌面是一款极为便捷的浏览器插件&#xff0c;能够帮助用户将自己的计算机连接到其他设备&#xff0c;无论是手机、平板电脑还是其他电脑。然而&#xff0c;在实际使用中&#xff0c;许多用户可能会面临各种各样的问题&#xff0c;比如…

靠右行驶数学建模分析(2014MCM美赛A题)

笔记 题目 要求分析&#xff1a; 比较规则的性能&#xff0c;分为light和heavy两种情况&#xff0c;性能指的是 a.流量与安全 b. 速度限制等分析左侧驾驶分析智能系统 论文 参考论文 两类规则分析 靠右行驶&#xff08;第一条&#xff09;2. 无限制&#xff08;去掉了第一条…

如何实现亿级用户在线状态统计?

亿级用户在线场景分析与解决方案 目录 亿级用户在线场景分析解决方案 2.1 基于总数的统计方案2.2 基于具体用户详情的统计方案 具体实现 3.1 基于总数的统计方案3.2 基于用户标识的统计实现3.3 Spring Boot 中的实现 总结 1. 亿级用户在线场景分析 以 QQ 在线状态统计为例&am…

多线程杂谈:惊群现象、CAS、安全的单例

引言 本文是一篇杂谈&#xff0c;帮助大家了解多线程可能会出现的面试题。 目录 引言 惊群现象 结合条件变量 CAS原子操作&#xff08;cmp & swap&#xff09; 线程控制&#xff1a;两个线程交替打印奇偶数 智能指针线程安全 单例模式线程安全 最简单的单例&…

sql实战解析-sum()over(partition by xx order by xx)

该窗口函数功能 sum( c )over( partition by a order by b) 按照一定规则汇总c的值&#xff0c;具体规则为以a分组&#xff0c;每组内按照b进行排序&#xff0c;汇总第一行至当前行的c的加和值。 从简单开始一步一步讲&#xff0c; 1、sum( )over( ) 对所有行进行求和 2、sum(…

你还在用idea吗

从VIM、Emacs&#xff0c;到eclipse、Jetbrains, 再到VSCode&#xff0c;过去的三十年时间&#xff0c;出现了这三代IDE产品。现在属于AI的时代来了&#xff0c;最新一代的产品像Cursor、Windsurf&#xff0c;就在昨天&#xff0c;字节跳动发布了最新的IDE&#xff0c;就叫Trae…

Unity新版InputSystem短按与长按,改键的实现

目录 前言&#xff1a; 一、InputSystem简介 1.安装InputSystem包 2.创建配置文件 3.创建自定义的Actions 二、自定义输入类 三、改键 四、全代码 前言&#xff1a; 新版inputsystem是Unity推出的一种新的输入方式&#xff0c;它将设备与行为进行分离&#xff0c;通过…

Android AutoMotive --CarService

1、AAOS概述 Android AutoMotive OS是谷歌针对车机使用场景打造的操作系统&#xff0c;它是基于现有Android系统的基础上增加了新特性&#xff0c;最主要的就是增加了CarService&#xff08;汽车服务&#xff09;模块。我们很容易把Android AutoMotive和Android Auto搞混&…

AWTK-WEB 快速入门(3) - C 语言 Http 应用程序

AWTK-WEB 快速入门 - C 语言 Http 应用程序 XMLHttpRequest 改变了 Web 应用程序与服务器交换数据的方式&#xff0c;fetch 是 XMLHttpRequest 继任者&#xff0c;具有更简洁的语法和更好的 Promise 集成。本文介绍一下如何使用 C 语言开发 AWTK-WEB 应用程序&#xff0c;并用 …

WPF1-从最简单的xaml开始

1. 最简单的WPF应用 1.1. App.config1.2. App.xaml 和 App.xaml.cs1.3. MainWindow.xaml 和 MainWindow.xaml.cs 2. 正式开始分析 2.1. 声明即定义2.2. 命名空间 2.2.1. xaml的Property和Attribute2.2.2. xaml中命名空间2.2.3. partial关键字 学习WPF&#xff0c;肯定要先学…

C#与AI的共同发展

C#与人工智能(AI)的共同发展反映了编程语言随着技术进步而演变&#xff0c;以适应新的挑战和需要。自2000年微软推出C#以来&#xff0c;这门语言经历了多次迭代&#xff0c;不仅成为了.NET平台的主要编程语言之一&#xff0c;还逐渐成为构建各种类型应用程序的强大工具。随着时…

图解Git——分布式Git《Pro Git》

分布式工作流程 Centralized Workflow&#xff08;集中式工作流&#xff09; 所有开发者都与同一个中央仓库同步代码&#xff0c;每个人通过拉取、提交来合作。如果两个开发者同时修改了相同的文件&#xff0c;后一个开发者必须在推送之前合并其他人的更改。 Integration-Mana…

2025年最新汽车零部件企业销售项目管理解决方案

在汽车零部件企业&#xff0c;销售项目管理的不规范和销售预测的不准确性常导致生产计划无法及时调整&#xff0c;因此客户关系常常中断&#xff0c;导致企业业务机会的丧失。为解决该问题&#xff0c;企业需要投入更多资源以优化销售流程与销售预测。 1、360多维立体客户视图…

vscode导入模块不显示类型注解

目录结构&#xff1a; utils.py&#xff1a; import random def select_Jrandom(i:int, m:int) -> int:"""随机选择一个不等于 i 的整数"""j iwhile j i:j int(random.uniform(0, m))return jdef clip_alpha(alpha_j:float, H:float, L:f…

【Elasticsearch】 Ingest Pipeline `processors`属性详解

在Elasticsearch中&#xff0c;Ingest Pipeline 的 processors 属性是一个数组&#xff0c;包含一个或多个处理器&#xff08;processors&#xff09;。每个处理器定义了一个数据处理步骤&#xff0c;可以在数据索引之前对数据进行预处理或富化。以下是对 processors 属性中常见…

python转转商超书籍信息爬虫

1基本理论 1.1概念体系 网络爬虫又称网络蜘蛛、网络蚂蚁、网络机器人等&#xff0c;可以按照我们设置的规则自动化爬取网络上的信息&#xff0c;这些规则被称为爬虫算法。是一种自动化程序&#xff0c;用于从互联网上抓取数据。爬虫通过模拟浏览器的行为&#xff0c;访问网页并…