《HarmonyOS技术精讲-Core File Kit》第8篇:跨应用文件分享实战 《HarmonyOS技术精讲-Core File Kit》第8篇跨应用文件分享实战1. 开篇文件在HAP之间如何流转HarmonyOS NEXT中不同应用之间的文件传递并不是简单地传一个“路径字符串”就能解决的。你可能会想直接把/data/storage/el2/base/haps/entry/files/photo.jpg这个路径发给另一个应用不就行了这在Android上是行不通的在HarmonyOS上同样不行——因为沙箱隔离机制每个应用只能访问自己的数据目录直接传路径对方根本读不到。官方提供的解决方案是通过Core File Kit中的URI统一资源标识符机制来实现安全的跨应用文件分享。它的核心逻辑是分享方将文件通过FileUri生成一个URI然后通过startAbility将这个URI传递给接收方应用接收方拿到URI后再通过fs.openSync去读取文件内容。这个功能本身不复杂但很多人在第一次实现时容易忽略权限声明、Ability的启动参数类型、以及接收方对URI的读取方式。这篇文章就通过一个完整的实战项目把这个流程走一遍。2. 它解决什么问题为什么要用URI而非路径跨应用文件分享的核心痛点是安全隔离与访问权限。方案优点缺点适用场景直接传文件路径简单粗暴沙箱隔离导致接收方无法访问不推荐无法跨应用通过FileKit生成URI安全可控接收方通过URI可读文件需要处理URI持久化文件移动后URI会失效推荐适合所有跨应用文件传递场景通过Want携带文件描述符FD更高效适合大数据量流式传输实现复杂需要处理FD的生命周期流媒体/大文件传输场景对于图片分享这种常见场景URI方案是最直接且稳定的选择。不适合场景如果你要分享的是实时流媒体或超大文件几百MB以上建议使用FD方式避免URI读取时的完整加载开销。3. 环境说明DevEco Studio 版本DevEco Studio 6.1.0 及以上 HarmonyOS SDK 版本HarmonyOS 6.1.0(23) 及以上 目标设备手机真机测试4. 核心实现A应用分享图片给B应用我们需要两个HAP模块一个作为分享方AppA一个作为接收方AppB。4.1 AppA选择图片并生成URI启动AppB页面UI一个按钮点击后从图库选择图片拿到图片的URI然后启动AppB并把这个URI传过去。代码pages/index.etsAppAimport{photoAccessHelper}fromkit.MediaLibraryKit;import{fileUri}fromkit.CoreFileKit;import{Want}fromkit.AbilityKit;import{BusinessError}fromkit.BasicServicesKit;EntryComponentstruct Index{Statemessage:string选择并分享图片;privatecontext:ContextgetContext(this);build(){Row(){Column(){Button(this.message).onClick((){this.selectAndSharePhoto();})}.width(100%)}.height(100%)}// 从图库选择图片privateasyncselectAndSharePhoto(){try{// 1. 获取图库访问权限consthelperphotoAccessHelper.getPhotoAccessHelper(this.context);// 选择单张图片consturis:Arraystringawaithelper.select({type:photoAccessHelper.PhotoType.IMAGE,maxSelectCount:1});if(uris.length0){return;}// 2. 拿到图片的URI注意这个URI是图库API返回的constphotoUri:stringuris[0];console.info(Selected photo URI:${photoUri});// 3. 构建Want启动AppB并传递URIconstwant:Want{bundleName:com.example.appb,// 替换为接收方应用的bundleNameabilityName:EntryAbility,// 替换为接收方Ability名称parameters:{sharePhotoUri:photoUri// 将URI放入parameters中传递}};// 4. 启动Abilityawaitthis.context.startAbility(want);console.info(Started AppB successfully);}catch(error){consterr:BusinessErrorerrorasBusinessError;console.error(selectAndSharePhoto failed:${JSON.stringify(err)});}}}注意事项photoAccessHelper.select返回的URI是图库系统提供的格式类似photospicker://……可以直接用于跨应用分享因为它是系统级的合法URI。startAbility的parameters可以携带任意键值对但值必须是字符串、数字或JSON可序列化的对象。这里我们传字符串URI。需要确保AppB已经在手机上安装否则startAbility会报错抛出error.code: 16000001表示找不到目标Ability。4.2 AppB接收URI并显示图片AppB的EntryAbility需要在onCreate或onNewWant中获取到传入的parameters然后读取图片文件并渲染到UI上。代码entryability/EntryAbility.etsAppB仅展示处理逻辑import{UIAbility,Want}fromkit.AbilityKit;import{window}fromkit.ArkUI;import{fileIo}fromkit.CoreFileKit;exportdefaultclassEntryAbilityextendsUIAbility{privatereceivedPhotoUri:string|nullnull;onCreate(want:Want,launchParam:object):void{// 从传过来的参数中提取URIif(want?.parameters?.[sharePhotoUri]){this.receivedPhotoUriwant.parameters[sharePhotoUri]asstring;console.info(Received photo URI:${this.receivedPhotoUri});}}onWindowStageCreate(windowStage:window.WindowStage):void{// 获取URI后传递给UI页面windowStage.loadContent(pages/Index,(err,data){if(err.code){return;}// 通过AppStorage或全局变量传递if(this.receivedPhotoUri){AppStorage.setOrCreate(sharedPhotoUri,this.receivedPhotoUri);}});}}代码pages/index.etsAppBimport{image}fromkit.ImageKit;import{fileIo}fromkit.CoreFileKit;import{common}fromkit.AbilityKit;EntryComponentstruct Index{StorageLink(sharedPhotoUri)sharedPhotoUri:string;StateimagePixelMap:image.PixelMap|nullnull;build(){Column(){if(this.imagePixelMap){Image(this.imagePixelMap).width(80%).aspectRatio(1).objectFit(ImageFit.Contain).margin({top:20});}else{Text(等待接收图片...).fontSize(20);}}.width(100%).height(100%).onAppear((){if(this.sharedPhotoUri){this.loadImageFromUri(this.sharedPhotoUri);}})}privateasyncloadImageFromUri(uri:string){try{// 1. 通过URI打开文件只读模式constfile:fileIo.FilefileIo.openSync(uri,fileIo.OpenMode.READ_ONLY);// 2. 将文件读取为ArrayBufferconstarrayBuffer:ArrayBufferfileIo.readSync(file.fd,fileIo.statSync(file.fd).size);// 3. 将ArrayBuffer解码为PixelMapconstimageSource:image.ImageSourceimage.createImageSource(arrayBuffer);constpixelMap:image.PixelMapawaitimageSource.createPixelMap();// 4. 更新UI显示this.imagePixelMappixelMap;// 5. 关闭文件重要fileIo.closeSync(file);}catch(error){console.error(loadImageFromUri failed:${JSON.stringify(error)});}}}注意事项fileIo.openSync中的uri可以是file://协议或photospicker://等系统URIFileKit会自动解析。必须关闭文件描述符fileIo.closeSync如果不调用会一直占用系统FD资源当分享频繁时容易导致FD泄漏。如果使用AppStorage传递数据要确保页面onAppear之前已经获取到了值否则可能出现sharedPhotoUri为空的情况。5. 踩坑记录核心坑1权限设置遗漏导致openSync失败现象AppB收到URI后调用fileIo.openSync时抛出error.code: 201提示“Permission denied”。原因AppB需要读取外部存储或特定URI的权限。虽然URI是由系统图库API生成的但接收方应用仍然需要声明ohos.permission.READ_IMAGEVIDEO权限如果处理的是图片/视频或ohos.permission.READ_MEDIA。解法在AppB的module.json5中声明权限{module:{requestPermissions:[{name:ohos.permission.READ_IMAGEVIDEO,reason:用于接收并显示分享的图片}]}}注意READ_IMAGEVIDEO是敏感权限需要在应用启动时动态请求通过AtManager.requestPermissionsFromUser但如果是通过startAbility由系统发起的启动系统会自动授予临时访问权限给URI对应的源应用。不过为了稳定还是显式声明并请求更安全。坑2URI传递后页面显示为空现象AppB的UI正常显示“等待接收图片…”但sharedPhotoUri始终为空字符串。原因onCreate中的AppStorage.setOrCreate调用时UI页面还未加载完毕AppStorage的数据可能未被页面正确绑定。更关键的是onWindowStageCreate中loadContent是异步的直接调用AppStorage.setOrCreate后页面StorageLink依赖的响应式绑定可能未建立成功。解法在onWindowStageCreate回调中将URI通过windowStage.loadContent的第二个参数回调函数传入或者使用更稳定的全局变量// EntryAbility.etsprivateglobalUri:string;onCreate(want:Want,launchParam:object):void{if(want?.parameters?.[sharePhotoUri]){this.globalUriwant.parameters[sharePhotoUri]asstring;}}onWindowStageCreate(windowStage:window.WindowStage):void{windowStage.loadContent(pages/Index,(err,data){if(err.code)return;// 将URI存入AppStorage确保页面已经加载完毕AppStorage.setOrCreate(sharedPhotoUri,this.globalUri);});}根本原因ArkUI的StorageLink绑定在组件创建时建立如果在组件创建前就setOrCreate会导致绑定失效。所以必须在loadContent回调中设置。6. 最佳实践优先使用photospicker://类型的URI图库API返回的URI是系统级的临时URI由系统管理访问权限接收方应用无需额外申请权限即可读取是最安全的方式。避免自己手动构建file://URI因为沙箱路径对方无法访问。UI渲染尽量用PixelMap而不是直接从文件流如果传输的是图片推荐先通过image.createImageSource解码为PixelMap再显示而不是直接在Image组件中传递文件路径。因为Image组件可能会缓存文件导致旧文件不更新。确保接收方Ability的launchType为singleton如果用户多次分享图片给AppB每次都会启动新的Ability实例导致旧Ability中的parameters丢失。在module.json5中设置launchType: singleton这样新请求会通过onNewWant回调通知现有实例而不是创建新实例。接收方需要重写onNewWant来更新URI。7. FAQ真实开发视角Q为什么真机测试可以模拟器上openSync失败A模拟器对图库支持有限可能无法生成合法的photospicker://URI。建议优先使用真机测试。如果必须在模拟器上验证可以手动构造一个file://URI指向模拟器预先存在的文件但要注意目录权限。Q分享后AppB退出再重新打开还能看到之前的图片吗A不能。URI是临时有效的只有短时间通常在AppB关闭前有效。如果AppB退出URI会失效。要持久化分享的内容接收方需要在收到URI后立即将文件复制到自己的沙箱目录中然后用新的URI保存。Q为什么第一次授权成功第二次再分享时权限直接拒绝A如果用户第一次拒绝权限申请系统会记录该选择。当同一个应用再次请求相同权限时系统可能直接拒绝而不弹出弹窗。解决方法是引导用户进入“设置-应用-权限管理”手动开启权限。代码层面可以通过AtManager.canRequestPermission检查权限状态并给出提示。如果你也在尝试跨应用文件分享可以重点检查权限声明和URI传递时机。如果遇到其他奇怪的问题欢迎在评论区交流。