ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

鸿蒙Share Kit实战:从相册选图到系统分享面板的完整流程

鸿蒙Share Kit实战:从相册选图到系统分享面板的完整流程 开发鸿蒙版内容社区App的时候产品提了一个让我当时很头疼的需求用户生成的海报要能一键分享出去不光分享到另一个App还得把图片本身带上而不是只发一个链接。这个需求放在Android上很好办接个微信SDK或者用系统Intent就能搞定但到了HarmonyOS NEXT这套全新框架里一切都要重新摸。我折腾了一周多最终确定用系统级的Share Kit来做图片分享这也是今天要拆解的“鸿蒙学习实战之路-Share Kit系列(4/17)-分享图片内容实战”这篇内容的核心。这篇文章适合三类人看正在给鸿蒙应用做分享功能的开发者从Android/iOS转过来想快速理解Share Kit的新人以及做社交、内容社区、工具类产品时被“分享图片”需求卡住的朋友。我会把从相册选图、解码成PixelMap、组装ShareRecord、拉起系统分享面板到真机实测踩坑的完整过程都写清楚最后再多图分享和运行时生成海报这两个进阶场景给出我的选型建议。1. 分享面板和分享内容的分层Share Kit到底替你干了什么1.1 先把“分享内容”和“分享面板”拆开看刚开始接触Share Kit时最容易犯的一个错误是把它当成微信SDK那种三方分享SDK来理解。实际上Share Kit做的事情非常纯粹它负责把你的“分享内容”交给系统分享面板再由这个面板让用户选择要发给哪个应用选定后系统负责投递。它不维护任何目标应用的私密通道也不负责微信、钉钉这些App内部的对接逻辑应用能出现在分享面板里是它们在系统侧注册了对应的分享接收能力。想通这一点特别重要。我见过不少同行图片分享不出去就怀疑是Share Kit的问题查了几天最后发现是自己把图片数据形态给错了系统根本不知道这是一张图。所以做分享功能前脑子里要先有一个分层模型你的应用负责生成分享内容比如一张图片、一段文字、一个文件Share Kit负责把内容包装成系统能识别的结构系统分享面板负责展示和让用户选择目标目标应用负责接收并处理内容。Share Kit夹在中间既不是源头也不是终点。它的价值在于你不需要关心目标应用长什么样、怎么传数据只要把内容按规范准备好剩下的事情交给系统。这个东西在HarmonyOS NEXT上比Android的Intent分享更规范所有分享内容都要统一走ShareRecord这个数据结构。1.2 图片在分享链路里的三种数据形态实战中分享图片Share Kit支持三种内容形态这也是理解Share Kit最关键的一个环节。我把它们整理成一张表后面选型基本都靠它数据形态ShareRecord字段典型场景优点缺点LINK网络链接link分享网图、远端图片资源传参轻、无需处理本地文件依赖网络目标应用可能不支持FILE本地文件files分享相册图、沙箱里的图片文件支持多图、保留原图质量目标应用可能需要URI授权才能读取PixelMap内存图pixelMap分享运行时生成的海报、截图最直接、离线可用、不落盘占用内存单个ShareRecord一般只带一张这三种形态可以混着用但实践下来我建议一个ShareRecord尽量只选一种主形态混着传会让系统面板在预览和投递时出现不可预期的表现比如明明带了pixelMapfiles里又塞了一大堆文件有的版本按这个解析有的版本按那个解析你很难控制。1.3 实际选型什么场景走哪条路我的选型逻辑其实很朴素按下面的优先级来判断图片已经在网络上比如商品缩略图、用户头像直接用LINK顺手还省内存图片在本地的相册或者沙箱里而且一次要传多张走FILE图片是运行时生成的海报、二维码、Canvas画布截图走PixelMap因为数据在内存里没必要先落盘再分享图片内容比较敏感不想在文件系统留下痕迹也走PixelMap全程内存传递。这套逻辑在后面几节的代码里会反复用到。先把形态选对后面代码写起来才不会被各种诡异问题纠缠。2. 开工前容易被带偏的三个认知环境、导入和权限边界2.1 环境与工程基线API 12、真机调试先说工程环境。我们项目用的是DevEco Studio 5.0HarmonyOS NEXT的API 12。Share Kit在这个版本上已经比较稳定了如果你用的版本更低建议先升级否则有些接口和枚举名对不上。另外一个非常重要的经验分享面板一定要真机调试。模拟器上我遇到的情况是分享面板有时能弹出来但预览区空白有时干脆不响应这个现象在真机上基本不会出现。原因也不难理解分享面板依赖系统侧的分享服务模拟器对这部分能力的支持并不完整。所以用完IDE的预览功能快速验证UI后分享功能还是尽早挂真机。工程侧也不需要额外配置什么特殊能力。我翻过项目里的module.json5分享功能本身没有要求申请任何requestPermissions。这一点很多从Android转过来的同事会下意识去找权限配置其实不用。2.2 模块导入的正确写法Share Kit的导入方式跟其他Kit不太一样它拆成了几个命名空间最常用的三个import { systemShare, share, uniformTypeDescriptor as utd } from kit.ShareKit;刚开始我犯过一个错误只导入systemShare然后发现ShareRecord这个类型找不到。这里要搞清楚systemShare负责的是“系统分享面板”share负责的是“分享内容的构造和数据结构”uniformTypeDescriptor通常简写成utd负责声明内容类型。三者各管一段写分享代码时基本都要用到。配套的还有几个模块import { image } from kit.ImageKit; import { photoAccessHelper } from kit.MediaLibraryKit; import { fileIo as fs } from kit.CoreFileKit; import { BusinessError } from kit.BasicServicesKit;image用来把图片文件解码成PixelMapphotoAccessHelper用来调相册选择器fileIo用来打开文件拿文件描述符BusinessError用来统一处理Promise链上的异常。后面代码里都会用到。2.3 权限边界选图权限和分享权限不是一回事这一节我认为是最容易被带偏的地方值得单独拿出来说。很多人的直觉是要分享相册里的图片肯定得申请ohos.permission.READ_IMAGEVIDEO。这个直觉是错的。我们通过系统相册选择器PhotoViewPicker选图不需要申请任何相册权限。原因是选择器运行在系统进程里由它完成用户的授权动作你的应用拿到的只是它授权过的那张图片的URI访问权。什么时候才需要权限呢如果你不走选择器而是用mediaLibrary直接遍历相册、读图片列表那才需要申请READ_IMAGEVIDEO而且还得弹窗让用户手动授权。我在实战里强烈建议大家走PhotoViewPicker一方面省去权限申请和用户授权弹窗另一方面也符合系统规范隐私合规上省了很多事。再往下说分享这个动作本身也不需要额外权限。你的应用把ShareRecord交给系统分享面板后后续的URI授权和投递由系统服务处理。所以整个分享图片链路里你甚至不需要在module.json5里加任何权限项代码不要凭感觉加权限加了反而可能因为权限申请理由不充分被应用市场上架审核拦下来。3. 选图到弹面板的核心链路图片数据怎么变成分享内容3.1 相册选图PhotoViewPicker的隐藏福利分享图片的第一步是让用户从相册里选一张图。用photoAccessHelper.PhotoViewPicker代码如下async function pickOneImage(): Promisestring { const picker new photoAccessHelper.PhotoViewPicker(); const result await picker.select({ MIMEType: photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE, maxSelectNumber: 1 }); if (result.photoUris.length 0) { throw new Error(没有选择图片); } return result.photoUris[0]; }这里有个细节select()返回的photoUris是类似file://media/Photo/xxx的字符串。之前有同事问要不要自己拼路径完全不用。这个URI可以直接交给后续的fileIo.openSync来读取。maxSelectNumber设为1是为了单图分享场景后面做多图分享时把这个值调大就行。MIMEType限定了图片类型避免用户在相册里翻视频翻得一头雾水。这套写法跑起来后系统会自动把选中的图片临时授权给你的应用你不需要关心授权细节。3.2 把URI变成PixelMap一步同时完成压缩和格式统一拿到URI之后下一步是把图片文件解码成内存里的PixelMap。为什么要转成PixelMap两个原因。一是分享面板在RICH预览模式下需要展示图片缩略图PixelMap是系统最通用的图片数据形态二是分享到目标应用时PixelMap可以直接序列化传递不依赖文件路径不依赖URI授权兼容性最好。解码的核心代码如下async function uriToPixelMap(uri: string, maxSize: number 1024): Promiseimage.PixelMap { const file fs.openSync(uri, fs.OpenMode.READ_ONLY); try { const imageSource image.createImageSource(file.fd); const pixelMap await imageSource.createPixelMap({ desiredSize: { width: maxSize, height: maxSize }, desiredPixelFormat: image.PixelMapFormat.RGBA_8888 }); imageSource.release(); return pixelMap; } finally { fs.closeSync(file); } }这里有两个操作意图要解释清楚。第一image.createImageSource(file.fd)接收的是文件描述符不是文件路径字符串。用fd的好处是不管URI是file://media还是沙箱路径只要你的应用有读取权限就能稳定打开。你在真机上用file://media/Photo/xxx直接传给createImageSource可能会遇到解析差异而fd方式绕开了这一点。第二desiredSize设成了1024×1024这不是要拉伸图片而是“等比缩放”的约束。系统会保持原图宽高比把长边限制在1024以内。为什么这么做拿一张4032×3024的相册原图来说解码成RGBA_8888的PixelMap内存占用大约是4032×3024×4字节算下来接近47MB。一个分享预览而已让系统面板去处理47MB的PixelMap必然卡。限制到1024之后内存占用降到4MB左右分享面板秒开投递也稳定。desiredPixelFormat我统一用RGBA_8888。这个格式通用性最好分享面板和绝大多数目标应用都能正确处理。之前我试过RGB_565想省内存结果在某些版本的系统分享面板预览里显示成了黑块排查了很久最后换回RGBA_8888就好了。3.3 组装ShareRecordutd字段是关键PixelMap到手后就要把它包装成share.ShareRecord。这个对象是整个分享链路的核心数据结构代码很简单function buildImageShareRecord(pixelMap: image.PixelMap): share.ShareRecord { return { utd: utd.UniformDataType.IMAGE, title: 分享图片, description: 来自鸿蒙图片分享实战的一张图片, pixelMap: pixelMap }; }我这里想重点强调utd字段。它表示“统一数据类型”可以理解成MIME的升级版。系统分享面板拿到ShareRecord后第一件事就是看utd判断这是什么类型的内容能不能预览哪些应用可以接收。如果这个字段不填或者填错后面就全乱套了。分享图片填utd.UniformDataType.IMAGE分享视频填VIDEO分享文件填FILE就这么简单。title和description是展示给用户的在RICH预览模式下会显示在面板上方。实测下来这两个字段不要留空空标题的预览面板看起来非常像系统出了Bug。pixelMap字段就是刚才解码出来的内存图片。3.4 拉起系统分享面板ShowOptions的参数含义有了ShareRecord接下来要创建分享控制器并拉起系统面板。这里的核心配置项是ShareControllerShowOptionsfunction showSharePanel(record: share.ShareRecord) { const options: systemShare.ShareControllerShowOptions { previewRect: { x: 0, y: 0, width: 0, height: 0 }, previewMode: systemShare.SharePreviewMode.RICH, selectionMode: systemShare.SelectionMode.SINGLE, shareContent: record }; systemShare.createShareController(options) .then((controller: systemShare.ShareController) { return controller.show(); }) .then((shareResult: systemShare.ShareResult) { if (shareResult.result systemShare.ShareResult.SUCCESS) { console.info(本次分享已提交给目标应用); } else if (shareResult.result systemShare.ShareResult.CANCEL) { console.info(用户取消了分享); } else { console.warn(分享流程失败); } }) .catch((err: BusinessError) { console.error(分享面板调用失败code${err.code}, message${err.message}); }); }previewRect是分享面板弹出位置的计算参照系。实战里传全零矩形让系统按当前页面自行布局是最省事也最不容易出错的写法。previewMode有两个值RICH和BASIC。分享图片一定要用RICH这样面板上方会显示图片预览、标题和描述BASIC模式下只有应用列表图片预览被砍掉分享体验差很多。selectionMode用来配置目标应用是单选还是多选图片分享场景SINGLE足够。还有一个坑要提醒controller.show()返回的是一个Promise里面是分享结果。很多新手只调用.then(controller controller.show())然后就不管后面了这样用户取消分享、分享失败你都感知不到。正确的做法是像上面这样把show()接住根据ShareResult分别处理成功、取消、失败三种状态。3.5 完整的分享函数照抄就能跑把前面几段串起来就是一个完整的“选图并分享”函数async function shareImage() { try { const uri await pickOneImage(); const pixelMap await uriToPixelMap(uri); const record buildImageShareRecord(pixelMap); showSharePanel(record); } catch (err) { console.error(分享流程异常${JSON.stringify(err)}); } }在按钮的点击回调里调用shareImage()跑起来的效果是先弹出系统相册选择器选完图后系统分享面板马上弹出来面板上方有图片预览下面是可接收图片的应用列表。用户点任意一个图片数据就会投递过去。这里特别提醒一下PixelMap的释放时机。分享是异步的controller.show()返回后图片数据才正式提交给目标应用。所以不要一调用完shareImage()就去pixelMap.release()否则分享面板正预览着内存图片没了轻则黑屏重则崩溃。我建议在收到ShareResult.SUCCESS或CANCEL回调之后确认页面不再使用这个PixelMap了再释放。如果分享完还要把它展示在界面上那就一直保留到界面销毁。4. 真机实测排雷黑屏、面板闪退、目标应用不收图4.1 分享面板一闪而过先从ShareRecord找问题第一次真机跑这个功能时我的现象是分享面板刚弹出来就消失日志里抛了一个异常。ShareControllerShowOptions里明明都填了为什么会有这种问题我当时的排查思路是这样的先看异常堆栈是不是系统面板的问题但报错信息很笼统没法直接定位。于是我把ShareRecord整个对象序列化打印出来看发现utd字段居然是undefined。再往前查发现项目中导入的uniformTypeDescriptor别名写法在某个文件里写成了utd但在另外一处引用时又直接用了原名导致类型没对上。这个案例提醒我出现面板闪退时第一排查对象永远是ShareRecord本身而不是系统的分享服务。检查顺序是utd有没有值、pixelMap是不是有效对象、标题描述有没有填空、有没有在show()之前提前释放了PixelMap。按这个顺序查绝大多数闪退都能定位。4.2 RICH预览黑屏多半是PixelMap提前释放另一个高频问题是预览区黑屏面板弹出来了标题也有就是图片区域一片黑。我遇到的情况和排查路径是这样的第一步检查pixelMap是否在createShareController调用前就被release()了。我们的代码在页面onPageHide时统一释放了一次图片资源导致用户从相册回来、分享面板正要创建时PixelMap已经失效。这是最典型的原因。解决办法是把release时机放到分享回调之后而不是页面生命周期里一刀切。第二步检查PixelMap格式。刚才提过RGB_565格式在分享面板预览里可能显示成黑块。如果你没有强制指定desiredPixelFormat某些图片来源会给你返回奇怪的格式。统一用RGBA_8888这个坑就绕开了。第三步检查图片的解码尺寸。如果原始URI指向的是一个超大图而且你没有设desiredSize解码出来的PixelMap可能大到系统预览组件直接放弃渲染。控制长边在1024到2048之间问题不大。4.3 目标应用收到空白文件URI授权链路的坑分享流程走通之后又遇到一个更隐蔽的问题分享面板显示一切正常用户选了一个目标应用对方也确实收到了内容但打开一看是空白或者是无法解析的文件。这个问题的最常见原因是你分享的是一个应用沙箱里的file://路径比如file://cache/share_temp.png。系统分享面板在投递时确实会尝试给目标应用临时授权但不是所有目标应用都对这种授权处理得很好尤其是那些从传统Android移植过来的应用对URI权限的适配参差不齐。我的解决方案很简单粗暴能转PixelMap就转PixelMap分享。PixelMap是内存数据投递时不依赖文件路径不存在URI授权的问题。如果你一定要分享原图文件先把图片复制到应用自身的临时目录再通过files字段分享并用file://完整拼接沙箱路径。这样相比直接甩相册URI目标应用拿到文件的确定性要高得多。4.4 内存涨得离谱分享前加一道压缩最后说一个性能问题。刚开始没压缩时我用一台老测试机跑分享相册原图4000万像素选完图那一瞬间内存直接涨了80MB面板打开耗时超过3秒然后系统开始杀后台进程。这种问题的解法就是文章里反复提到的desiredSize压缩。分享场景真的不需要原图把长边限制在2048以内视觉上几乎看不出差别内存占用却降到了原来的四分之一以下。如果你做的是社交分享甚至1024就够。另外还有一个经验ImageSource可以多次调用createPixelMap。比如你想同时拿一个缩略图用于预览、一个稍微大点的图用于分享可以用同一个ImageSource解码两次分别指定不同的desiredSize避免重复IO。注意两次创建的PixelMap各自管理自己的释放时机。常见问题排查优先级解决办法面板闪退1. utd 2. ShareRecord完整性打印ShareRecord检查字段RICH预览黑屏1. PixelMap释放 2. 格式 3. 尺寸延迟release统一RGBA_8888目标应用空白1. URI授权 2. 文件路径转PixelMap或复制沙箱内存高涨1. desiredSize 2. 格式限制长边用RGBA_88885. 再往前走一步多图、本地文件与离线场景的取舍5.1 多图批量分享用files而不是PixelMap产品总会在单图分享上线后提多图需求这个很常见。多图分享的核心区别在于一个ShareRecord里只应该有一个pixelMap真正承载多图数据的是files字段。function buildMultiImageShareRecord(uris: string[]): share.ShareRecord { return { utd: utd.UniformDataType.IMAGE, title: 分享${uris.length}张图片, description: 来自相册的多张图片, files: uris.map(uri ({ uri: uri, type: utd.UniformDataType.IMAGE })) }; }配套的选图函数把maxSelectNumber改成9其他逻辑不变。我这里建议最多让用户选9张一是相册选择器本身有上限二是分享面板对多图的预览表现是“数量缩略图堆叠”选太多用户也没有耐心看。多图场景下有一个细节要小心files里的URI如果是相册URI依然会遇到上面说的URI授权问题。我实测下来部分目标应用对单图pixelMap支持良好但多图只能用files时就会挑食。稳妥做法是选图后先把所有图片复制到应用沙箱临时目录再分享file://路径。代价是多一次IO拷贝换来的是兼容性大幅提升值。5.2 分享运行时生成的图片Canvas海报场景内容社区App里最常遇到的需求是把用户填的信息渲染成一张海报再分享出去。这张海报不存在相册里也不在网络URL上就是运行时的内存数据。这种场景下直接生成PixelMap并塞进shareRecord.pixelMap是最优雅的方案。我之前用离线Canvas绘制海报最终得到的是一个PixelMap对象。关键点在于这个PixelMap的宽高比例往往不标准比如长海报是750×1334。解码照片时设置desiredSize等比缩放的做法用在这里就不合适了因为你自己生成的PixelMap不需要再解码它本身就是目标尺寸。分享这种长图时建议直接把原始PixelMap塞进ShareRecord长边控制在2000以内。如果海报特别长超过了系统分享面板的预览渲染上限预览区可能显示不全但分享出去的内容还是完整的不影响目标应用查看。实测下来RICH预览模式对超高图片会做等比缩放显示不会因为宽高比奇怪就崩溃。5.3 离线分享的边界与取舍最后一个场景是离线分享。有人问分享图片是不是一定要联网我的实测结论是分享动作本身不依赖网络。系统分享面板是系统进程渲染的你的App只需要把PixelMap或文件交给它不需要上传到任何服务器。目标应用拿到数据后要做什么、要不要联网那是目标应用自己的事。真正需要权衡的是分享内容形态对离线的敏感度。用PixelMap和FILE分享本地图片离线完全没问题如果用LINK分享网图目标应用需要联网去拉这张图对方网络差就拉不出来。所以我个人在离线场景下的铁律是能传PixelMap就传PixelMap不能传PixelMap就传FILE能不用LINK就不用LINK。这条规则在分享图片这个赛道上基本没有例外。最后分享一个这几次实战里养成的习惯分享按钮的反馈一定要区分成功、取消、失败三态。很多用户分享到一半会习惯性点返回如果产品把取消当成失败来统计会得出完全错误的上报数据。我后来在代码里用ShareResult.CANCEL单独打点上线后看到取消率超过30%才知道用户不是嫌功能不好用只是单纯不想发到那个App。这个小细节对产品判断影响很大。
返回列表