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.mzph.cn/news/893615.shtml

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

相关文章

MongoDB实训:电子商务日志存储任务

一、实验目的 1. 理解如何通过Java API连接MongoDB数据库。 2. 学习在Java中使用MongoDB进行数据库操作&#xff0c;包括插入数据、查询数据以及数据统计等。 3. 掌握电子商务日志数据在MongoDB中的存储和操作方法。 二、实验环境准备 1. JAVA环境准备&#xff1a;确保…

计算机网络 (59)无线个人区域网WPAN

前言 无线个人区域网&#xff08;WPAN&#xff0c;Wireless Personal Area Network&#xff09;是一种以个人为中心&#xff0c;采用无线连接方式的个人局域网。 一、定义与特点 定义&#xff1a;WPAN是以个人为中心&#xff0c;实现活动半径小、业务类型丰富、面向特定群体的无…

从spec到iso的koji使用

了解一下Linux发行版流程&#xff1a;:从spec到iso的koji使用 for Fedora 41。 Fedora 41有24235个包&#xff0c;我们选择 minimal 的几十个源码包&#xff0c;百多个rpm包构建。 配3台服务器 40C64G 44C64G 80C128G&#xff0c;有点大材小用&#xff0c;一台就够了 &#xf…

20250124-注意力机制(5-7)【3/3完结】 ——已复现

Attention Is All You Need&#xff08;注意力就是你所需要的一切&#xff09;&#xff08;5-7&#xff09;【3/3完结】 ——已复现 20250124-注意力机制&#xff08;1-2&#xff09;【1/3】 ——已复现-CSDN博客 20250124-注意力机制&#xff08;3-4&#xff09;【2/3】 ——已…

22_解析XML配置文件_List列表

解析XML文件 需要先 1.【加载XML文件】 而 【加载XML】文件有两种方式 【第一种 —— 使用Unity资源系统加载文件】 TextAsset xml Resources.Load<TextAsset>(filePath); XmlDocument doc new XmlDocument(); doc.LoadXml(xml.text); 【第二种 —— 在C#文件IO…

[JavaScript] ES6及以后版本的新特性

文章目录 箭头函数&#xff08;Arrow Functions&#xff09;为什么需要箭头函数&#xff1f;箭头函数的完整语法箭头函数中的 this实用场景 解构赋值&#xff08;Destructuring Assignment&#xff09;为什么需要解构赋值&#xff1f;数组解构赋值的完整用法对象解构赋值的完整…

C语言进阶——3字符函数和字符串函数(2)

8 strsrt char * strstr ( const char *str1, const char * str2);查找子字符串 返回指向 str1 中第一次出现的 str2 的指针&#xff0c;如果 str2 不是 str1 的一部分&#xff0c;则返回 null 指针。匹配过程不包括终止 null 字符&#xff0c;但会在此处停止。 8.1 库函数s…

ThinkPHP 8请求处理-获取请求对象与请求上下文

【图书介绍】《ThinkPHP 8高效构建Web应用》-CSDN博客 《2025新书 ThinkPHP 8高效构建Web应用 编程与应用开发丛书 夏磊 清华大学出版社教材书籍 9787302678236 ThinkPHP 8高效构建Web应用》【摘要 书评 试读】- 京东图书 使用Composer初始化ThinkPHP 8应用_thinkphp8 compos…

飞行器半实物联合仿真:技术解析与应用实践

1.背景介绍 当前&#xff0c;飞行器已成为大国博弈复杂场景中的重要角色&#xff0c;其技术经过多次实践不断发展&#xff0c;性能持续提升&#xff0c;整体效能显著增强。随着计算机技术和系统仿真技术的发展&#xff0c;利用计算机模拟和仿真构造一个虚拟飞行器的飞行控制系…

c#配置config文件

1&#xff0c;引用命名空间 Configuration 及配置信息

【机器学习】机器学习引领数学难题攻克:迈向未知数学领域的新突破

我的个人主页 我的领域&#xff1a;人工智能篇&#xff0c;希望能帮助到大家&#xff01;&#xff01;&#xff01;&#x1f44d;点赞 收藏❤ 一、引言 在数学的浩瀚领域中&#xff0c;存在着诸多长期未解的难题&#xff0c;这些难题犹如高耸的山峰&#xff0c;吸引着无数数…

OS Copilot功能测评:智能助手的炫彩魔法

简介&#xff1a; OS Copilot 是一款融合了人工智能技术的智能助手&#xff0c;专为Linux系统设计&#xff0c;旨在提升系统管理和运维效率。本文详细介绍了在阿里云ECS实例上安装和体验OS Copilot的过程&#xff0c;重点评测了其三个核心参数&#xff1a;-t&#xff08;模式…

计算机网络 (55)流失存储音频/视频

一、定义与特点 定义&#xff1a;流式存储音频/视频是指经过压缩并存储在服务器上的多媒体文件&#xff0c;客户端可以通过互联网边下载边播放这些文件&#xff0c;也称为音频/视频点播。 特点&#xff1a; 边下载边播放&#xff1a;用户无需等待整个文件下载完成即可开始播放…

Oracle存储过程语法详解

简介 存储过程是一系列SQL语句的集合&#xff0c;可以封装复杂的逻辑&#xff0c;实现特定的功能&#xff0c;可以提高执行速度和代码的复用性&#xff0c;预先编译后存储在数据库中&#xff0c;可以通过指定存储过程的名称对其进行调用。 本文主要讲解Oracle存储过程语法&am…

推箱子游戏

java小游戏2 一游戏介绍 二图像准备 墙、箱子、人、箱子目的地&#xff0c;人左边、人右边、人上边、人下边 三结构准备 地图是什么&#xff0c;我们把地图想象成一个网格&#xff0c;每个格子就是工人每次移动的步长&#xff0c;也是箱子移动的距离&#xff0c;设置一个二维数…

如何分辨ddos攻击和cc攻击?

DDoS&#xff08;分布式拒绝服务&#xff09;攻击和 CC&#xff08;Challenge Collapsar&#xff09;攻击都属于网络攻击手段&#xff0c;主要通过消耗目标服务器资源使其无法正常提供服务&#xff0c;但它们在攻击原理、攻击特征等方面存在区别&#xff1a; 攻击原理 DDoS 攻…

期权帮|如何利用股指期货进行对冲套利?

锦鲤三三每日分享期权知识&#xff0c;帮助期权新手及时有效地掌握即市趋势与新资讯&#xff01; 如何利用股指期货进行对冲套利&#xff1f; 对冲就是通过股指期货来平衡投资组合的风险。它分为正向与反向两种策略&#xff1a; &#xff08;1&#xff09;正向对冲&#xff…

软件质量与测试报告5-压力测试 JMeter 与 Badboy

A&#xff0e;百度搜索引擎压力测试 通过在Badboy下执行如下的测试场景来生成压力测试的脚本&#xff1a; a) 在Badboy的地址栏里面输入www.baidu.com&#xff0c;回车&#xff1b; b) 在右下区域打开的百度的主页上输入搜索关键字JMeter&#xff0c;回车&#xff1b; c) 在…

Mybatis多条件查询:Map传参与对象传参解析

Mybatis 多条件查询常见且关键&#xff0c;本文探讨两种方法——Map 传参和 Java Bean 对象传参&#xff0c;展示用法及区别&#xff0c;总结应用场景和优缺点。 1. Map传参方式 原理&#xff1a;Mybatis允许我们通过一个Map对象来传递动态SQL中的参数。Map的键对应于SQL语句中…

wangEditor富文本编辑器,Laravel上传图片配置和使用

文章目录 前言步骤1. 构造好前端模版2. 搭建后端存储3. 调试 前言 由于最近写项目需要使用富文本编辑器&#xff0c;使用的是VUE3.0版本所以很多不兼容&#xff0c;实际测试以后推荐使用wangEditor 步骤 构造好前端模版搭建后端存储调试 1. 构造好前端模版 安装模版 模版安…