HarmonyOS鸿蒙Next中flutter鸿蒙化,如何在flutter页面点击保存图片然后调用原生的权限把图片保存到相册
HarmonyOS鸿蒙Next中flutter鸿蒙化,如何在flutter页面点击保存图片然后调用原生的权限把图片保存到相册?
【背景知识】
- image_gallery_saver是一个Flutter插件,用于将图片或者视频保存到设备相册,其中提供文档说明和Demo。由于Flutter的image_picker插件只能选择图片而无法保存图片到相册,此插件提供了该功能。
【解决方案】
-
以下提供4种方式保存图片或者视频到设备相册。
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Save image to gallery"), ), body: Center( child: Column( children: <Widget>[ SizedBox(height: 15), // `RepaintBoundary` 的作用是将这个蓝色矩形的绘制区域隔离出来,以便在需要时可以独立处理它的绘制和重绘。`_globalKey` 则用于在代码的其他部分访问这个 `RepaintBoundary` 小部件。 RepaintBoundary( key: _globalKey, child: Container( alignment: Alignment.center, width: 300, height: 300, color: Colors.blue, ), ), Container( padding: EdgeInsets.only(top: 15), child: ElevatedButton( onPressed: _saveLocalImage, child: Text("Save Local Image"), ), width: 300, height: 44, ), Container( padding: EdgeInsets.only(top: 15), child: ElevatedButton( onPressed: _saveNetworkImage, child: Text("Save Network Image"), ), width: 300, height: 44, ), Container( padding: EdgeInsets.only(top: 15), child: ElevatedButton( onPressed: _saveNetworkGifFile, child: Text("Save Network Gif Image"), ), width: 300, height: 44, ), Container( padding: EdgeInsets.only(top: 15), child: ElevatedButton( onPressed: _saveNetworkVideoFile, child: Text("Save Network Video"), ), width: 300, height: 44, ), ], ), )); }
-
保存本地图片到相册,参考代码如下:
GlobalKey _globalKey = GlobalKey(); // 创建一个`GlobalKey`类型的全局键对象,并将其赋值给变量 `_globalKey` // 从指定的Widget中获取渲染对象(Render Object),然后将该Widget的内容转换成一个图像(Image) _saveLocalImage() async { RenderRepaintBoundary boundary = _globalKey.currentContext!.findRenderObject() as RenderRepaintBoundary; ui.Image image = await boundary.toImage(); ByteData? byteData = await (image.toByteData(format: ui.ImageByteFormat.png)); if (byteData != null) { final result = await ImageGallerySaver.saveImage(byteData.buffer.asUint8List()); print(result); Utils.toast(result.toString()); } }
-
保存网络图片到相册,参考代码如下:
_saveNetworkImage() async { var response = await Dio().get( "https://XXXXX.jpg", options: Options(responseType: ResponseType.bytes)); final result = await ImageGallerySaver.saveImage( Uint8List.fromList(response.data), quality: 60, name: "hello"); print(result); Utils.toast("$result"); }
-
保存网络gif图到相册,参考代码如下:
_saveNetworkGifFile() async { var appDocDir = await getTemporaryDirectory(); String savePath = appDocDir.path + "/temp.gif"; String fileUrl = "https://XXXX.gif"; await Dio().download(fileUrl, savePath); final result = await ImageGallerySaver.saveFile(savePath, isReturnPathOfIOS: true); print(result); Utils.toast("$result"); }
-
保存网络video到相册,参考代码如下:
_saveNetworkVideoFile() async { var appDocDir = await getTemporaryDirectory(); String savePath = appDocDir.path + "/temp.mp4"; String fileUrl = "https://XXXX.mp4"; await Dio().download(fileUrl, savePath, onReceiveProgress: (count, total) { print((count / total * 100).toStringAsFixed(0) + "%"); }); final result = await ImageGallerySaver.saveFile(savePath); print(result); Utils.toast("$result"); }
-
更多关于HarmonyOS鸿蒙Next中flutter鸿蒙化,如何在flutter页面点击保存图片然后调用原生的权限把图片保存到相册的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
下面是我的代码吗,我采用了你的方案一:我压根没有执行到保存到图库的地方,一直提示我没有存储权限。
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:experimental_stu/components/toast/finish_toast.dart';
import 'package:experimental_stu/event/app_events.dart';
import 'package:experimental_stu/event/event_manager.dart';
import 'package:experimental_stu/services/app_permission_status.dart';
import 'package:experimental_stu/utils/assets_util.dart';
import 'package:experimental_stu/utils/toast_util.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:image_gallery_saver/image_gallery_saver.dart';
class Ceshi extends StatefulWidget {
double scale = 1;
String qrUrl = '';
/// 默认及加载失败图
String defImgPath;
final String title;
double? titleSize;
double? btnFontSize;
Size? btnSize;
Ceshi({
Key? key,
this.scale = 1,
required this.title,
this.titleSize,
this.btnFontSize,
this.btnSize,
required this.qrUrl,
required this.defImgPath,
}) : super(key: key);
@override
_CeshiState createState() => _CeshiState();
}
class _CeshiState extends State<Ceshi> {
Uint8List? qrData;
@override
initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
await AppPermissionStatus.instance.saveAlbum.getStatus();
getImageAsUint8List(widget.qrUrl, AssetsUtil.successQr);
});
}
createDefQr(double scale) {
return Image.asset(AssetsUtil.successQr,
width: 200 * scale, height: 200 * scale, fit: BoxFit.fitWidth);
}
showSuccessToast(double scale) {
SmartDialog.showToast('',
alignment: Alignment.center,
displayTime: const Duration(seconds: 3), builder: (context) {
return FinishToast(message: '已保存到相册', scale: scale);
});
}
void showErrorToast(String message, double scale) {
SmartDialog.showToast('',
alignment: Alignment.center,
displayTime: const Duration(seconds: 3), builder: (context) {
return FinishToast(message: message, scale: scale);
});
}
getImageAsUint8List(String imageUrl, String localAssetName) async {
Dio dio = Dio();
Uint8List? imageBytes;
try {
// 尝试从网络获取图片
Response response = await dio.get(imageUrl,
options: Options(responseType: ResponseType.bytes));
imageBytes = response.data;
} catch (e) {
// 网络请求失败,从本地资产加载图片
final ByteData data = await rootBundle.load(localAssetName);
imageBytes = data.buffer.asUint8List();
}
if (mounted) {
setState(() {
qrData = imageBytes;
});
}
}
@override
Widget build(BuildContext context) {
double scale = widget.scale;
return Container(
width: 302.w * scale,
height: 384.w * scale,
padding: EdgeInsets.symmetric(
horizontal: 35.w * scale, vertical: 20.w * scale),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(26.9.w),
),
child: Column(
children: [
Text(
widget.title,
style: TextStyle(
color: Colors.black,
fontSize: widget.titleSize ?? 23.55.w,
fontWeight: FontWeight.w600,
),
),
Expanded(
child: Container(
width: 232.w * scale,
height: 232.w * scale,
alignment: Alignment.center,
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage(
AssetsUtil.successQrBorder), // AssetImage用于加载项目中的图片
fit: BoxFit.fitWidth, // 图片填充方式
),
),
child: qrData == null
? createDefQr(scale)
: Image.memory(qrData!,
width: 200.w * scale,
height: 200.w * scale,
fit: BoxFit.fitWidth),
),
),
GestureDetector(
onTap: () async {
if (qrData == null) {
showErrorToast('二维码图片不存在,无法保存', scale);
return;
}
bool isGranted = AppPermissionStatus.instance.saveAlbum.isGranted;
if (!isGranted) {
ShowPermissionRequestEvent event = ShowPermissionRequestEvent(
name: '媒体权限使用说明:',
deniedDescribe: '请开启存储权限',
tag: 'savePhone',
permissionAlbum: AppPermissionStatus.instance.saveAlbum,
describe: '用于保存二维码到相册');
EventManager.instance.eventBus.fire(event);
isGranted = await event.completer.future;
}
if (isGranted) {
await ImageGallerySaver.saveImage(qrData!);
showSuccessToast(scale);
}
},
child: Container(
width: widget.btnSize?.width ?? 161.5.w * scale,
height: widget.btnSize?.height ?? 53.8.w * scale,
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color(0xFFFF7613),
borderRadius: BorderRadius.circular(30.w * scale),
),
child: Text(
'保存二维码',
style: TextStyle(
color: Colors.white,
fontSize: widget.btnFontSize ?? 20.w,
fontWeight: FontWeight.w400,
),
),
),
),
],
),
);
}
}
和
import 'package:permission_handler/permission_handler.dart';
class AppPermissionStatus {
// 单例实例
static final AppPermissionStatus _instance = AppPermissionStatus._internal();
// 私有构造函数,防止外部实例化
AppPermissionStatus._internal() {
// 在这里添加初始化逻辑
// 例如:初始化权限状态、注册监听器等
_initPermissionStatus();
}
// 公共静态方法,用于获取单例实例
static AppPermissionStatus get instance => _instance;
// 保存到相册能力
PermissionAbility saveAlbum = PermissionAbility(list: [
Permission.storage,
Permission.photos,
]);
/// 相机权限
PermissionAbility camera = PermissionAbility(list: [Permission.camera]);
/// 相册权限
PermissionAbility mediaPhoto = PermissionAbility(list: [Permission.storage]);
///图片权限
PermissionAbility photos = PermissionAbility(list: [Permission.photos]);
/// 麦克风
PermissionAbility microphone =
PermissionAbility(list: [Permission.microphone]);
// 添加私有初始化方法
void _initPermissionStatus() async {
// 初始化逻辑的具体实现
}
}
/// 权限能力单元
class PermissionAbility {
List<Permission> list;
bool isGranted;
PermissionAbility({required this.list, this.isGranted = false});
Future<bool> getStatus() async {
bool granted = true;
int i = 0;
for (i = 0; i < list.length; i++) {
PermissionStatus status = await list[i].status;
if (status != PermissionStatus.granted) {
granted = false;
}
}
isGranted = granted;
return granted;
}
Future<bool> request() async {
bool granted = isGranted;
Map<Permission, PermissionStatus> status = await list.request();
for (int i = 0; i < list.length; i++) {
if (status[list[i]] == PermissionStatus.granted) {
granted = true;
break;
}
}
isGranted = granted;
return granted;
}
}
开发者您好,您可以查看这边新开楼层的回复。
【背景知识】
-
HarmoyOS提供了PhotoPicker组件来实现相册图片/视频读取,且组件有两种形式:
-
HarmoyOS提供了两种保存图片、视频到相册的形式:
-
应用沙箱目录:HarmoyOS中无论读取或写入,都需要先将文件拷贝到应用的沙箱目录才能够进行下一步操作。
-
Flutter已适配HarmoyOS进行相册文件读取的三方库:image_picker。
-
Flutter已适配HarmoyOS进行相册文件写入的三方库:flutter_image_gallery_saver。
-
如何Flutter MethodChannel实现和OS通信:Flutter MethodChannel。
【解决方案】
- Flutter桥的原理:
- 架构图:
- Flutter和OS的常见交互方式有三种桥通道,我们可以根据自己实际业务来选择交互通道类型,三种桥的使用场景对比:
- EventChannel:用于OS向Flutter持续发送事件流(如传感器数据)。
- MethodChannel:最常用方式,支持双向异步调用。Flutter通过通道名调用OS侧方法,OS侧代码返回结果。
- BasicMessageChannel:传递简单数据(如字符串/二进制),支持自定义编解码器。
- 架构图:
- 解决方案原理:
- Flutter端使用定义MethodChannel(‘xxx’)平台通道(Platform Channel),需约定唯一通道名称。
- Flutter调用OS:通过invokeMethod传递方法名和参数到OS端。
- 数据序列化:参数通过标准格式(如JSON)跨平台传递,避免类型不兼容。
- OS侧监听:MethodChannel.setMethodCallHandler监听平台通道中的方法并进行OS侧业务处理,返回处理结果给Flutter。
- OS侧业务处理:读取时使用PhotoPicker组件访问图片/视频拉起相册和选择图片;写入时使用弹窗授权保存媒体库资源进行图片保存到相册。
- (推荐)方案一:使用Flutter已适配HarmoyOS的三方库插件实现读取:
- image_picker访问相册读取图片;
- Flutter_image_gallery_saver实现图片保存;
- 核心代码如下:
// pubspec.yaml中通过中心仓引入的方式引用三方依赖
dependencies:
image_gallery_saver:
git:
url: "https://gitcode.com/openharmony-sig/flutter_image_gallery_saver.git"
image_picker_ohos:
git:
url: "https://gitcode.com/openharmony-sig/flutter_packages.git"
path: "packages/image_picker/image_picker"
// Dart中使用三方库提供的ImagePicker对象提供的各种方法获取相册图片,如:XFile对象、pickImage()、getMedia()、pickMultiImage()方法
...
XFile? _image;
Future<void> _pickImage() async {
final picker = ImagePicker();
final image = await picker.pickImage(
source: ImageSource.gallery,
maxWidth: 800,
);
setState(() => _image = image);
}
....
// Dart中使用三方库提供的ImageGallerySaver.saveImage()、saveFile()实现写入。
ByteData? byteData = await (image.toByteData(format: ui.ImageByteFormat.png));
if (byteData != null) {
final result = await ImageGallerySaver.saveImage(byteData.buffer.asUint8List());
}
...
- 方案二:利用Flutter提供的平台通道(Platform Channels)机制实现读写;核心代码参考如下: Flutter端核心代码:
...
List<String>? _mediaFileList;
// 定义指定名称的平台通道,用于Flutter和OS的交互
final _platform = const MethodChannel('samples.flutter.dev/image');
// 读取图片
Future<void> _getImage() async {
List<String>? mediaFiles;
try {
// invokeMethod('getImage')调用注册在平台通道的OS方法
final List<String>? list_paths = await platform.invokeMethod<List<dynamic>?>('getImage')
.then(
(List<dynamic>? paths) => paths?.map((dynamic path) => path as String).toList()
);
mediaFiles = list_paths;
} on PlatformException catch (e) {
print("Failed to get battery level: '${e.message}'.") ;
}
setState(() {
_mediaFileList = mediaFiles;
});
}
...
// 保存图片,imageBytes:图片内容。
FutureOr<dynamic> saveImage(Uint8List imageBytes) async {
final result = await platform.invokeMethod('saveImageToGallery',
<String, dynamic>{
'imageBytes': imageBytes,
'quality': 8,
'name': 'name1',
});
return result;
}
...
HarmoyOS端核心代码:
// EntryAbility.ets
// FlutterEngine中注册OS监听
export default class EntryAbility extends FlutterAbility {
configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
GeneratedPluginRegistrant.registerWith(flutterEngine)
// FlutterEngine中注册OS监听(ImagePlugin)
this.addPlugin(new ImagePlugin());
}
}
// ImagePlugin.ets (ImagePlugin implements FlutterPlugin)
// OS监听插件,初始化平台通道的监听的调用方法
onAttachedToEngine(binding: FlutterPluginBinding): void {
const api = new ImageApi(binding.getApplicationContext())
this.channel = new MethodChannel(binding.getBinaryMessenger(), "samples.flutter.dev/image");
let that = this;
this.channel.setMethodCallHandler({
onMethodCall(call: MethodCall, result: MethodResult) {
switch (call.method) {
case "getImage":
// 具体的OS侧代码实现
api.getImage(result);
break;
case "saveImageToGallery":
// 具体的OS侧代码实现
api.saveImage(result);
break;
default:
result.notImplemented();
break;
}
}
})
}
- 具体的OS侧代码实现参考:图片获取与保存实践。
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:experimental_stu/components/toast/finish_toast.dart';
import 'package:experimental_stu/event/app_events.dart';
import 'package:experimental_stu/event/event_manager.dart';
import 'package:experimental_stu/services/app_permission_status.dart';
import 'package:experimental_stu/utils/assets_util.dart';
import 'package:experimental_stu/utils/toast_util.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:image_gallery_saver/image_gallery_saver.dart';
class Ceshi extends StatefulWidget {
double scale = 1;
String qrUrl = '';
String defImgPath;
final String title;
double? titleSize;
double? btnFontSize;
Size? btnSize;
Ceshi({
Key? key,
this.scale = 1,
required this.title,
this.titleSize,
this.btnFontSize,
this.btnSize,
required this.qrUrl,
required this.defImgPath,
}) : super(key: key);
@override
_CeshiState createState() => _CeshiState();
}
class _CeshiState extends State<Ceshi> {
Uint8List? qrData;
@override
initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
await AppPermissionStatus.instance.saveAlbum.getStatus();
getImageAsUint8List(widget.qrUrl, AssetsUtil.successQr);
});
}
createDefQr(double scale) {
return Image.asset(AssetsUtil.successQr,
width: 200 * scale, height: 200 * scale, fit: BoxFit.fitWidth);
}
showSuccessToast(double scale) {
SmartDialog.showToast('',
alignment: Alignment.center,
displayTime: const Duration(seconds: 3), builder: (context) {
return FinishToast(message: '已保存到相册', scale: scale);
});
}
void showErrorToast(String message, double scale) {
SmartDialog.showToast('',
alignment: Alignment.center,
displayTime: const Duration(seconds: 3), builder: (context) {
return FinishToast(message: message, scale: scale);
});
}
getImageAsUint8List(String imageUrl, String localAssetName) async {
Dio dio = Dio();
Uint8List? imageBytes;
try {
Response response = await dio.get(imageUrl,
options: Options(responseType: ResponseType.bytes));
imageBytes = response.data;
} catch (e) {
final ByteData data = await rootBundle.load(localAssetName);
imageBytes = data.buffer.asUint8List();
}
if (mounted) {
setState(() {
qrData = imageBytes;
});
}
}
@override
Widget build(BuildContext context) {
double scale = widget.scale;
return Container(
width: 302.w * scale,
height: 384.w * scale,
padding: EdgeInsets.symmetric(
horizontal: 35.w * scale, vertical: 20.w * scale),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(26.9.w),
),
child: Column(
children: [
Text(
widget.title,
style: TextStyle(
color: Colors.black,
fontSize: widget.titleSize ?? 23.55.w,
fontWeight: FontWeight.w600,
),
),
Expanded(
child: Container(
width: 232.w * scale,
height: 232.w * scale,
alignment: Alignment.center,
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage(AssetsUtil.successQrBorder),
fit: BoxFit.fitWidth,
),
),
child: qrData == null
? createDefQr(scale)
: Image.memory(qrData!,
width: 200.w * scale,
height: 200.w * scale,
fit: BoxFit.fitWidth),
),
),
GestureDetector(
onTap: () async {
if (qrData == null) {
showErrorToast('二维码图片不存在,无法保存', scale);
return;
}
bool isGranted = AppPermissionStatus.instance.saveAlbum.isGranted;
if (!isGranted) {
ShowPermissionRequestEvent event = ShowPermissionRequestEvent(
name: '媒体权限使用说明:',
deniedDescribe: '请开启存储权限',
tag: 'savePhone',
permissionAlbum: AppPermissionStatus.instance.saveAlbum,
describe: '用于保存二维码到相册');
EventManager.instance.eventBus.fire(event);
isGranted = await event.completer.future;
}
if (isGranted) {
await ImageGallerySaver.saveImage(qrData!);
showSuccessToast(scale);
}
},
child: Container(
width: widget.btnSize?.width ?? 161.5.w * scale,
height: widget.btnSize?.height ?? 53.8.w * scale,
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color(0xFFFF7613),
borderRadius: BorderRadius.circular(30.w * scale),
),
child: Text(
'保存二维码',
style: TextStyle(
color: Colors.white,
fontSize: widget.btnFontSize ?? 20.w,
fontWeight: FontWeight.w400,
),
),
),
),
],
),
);
}
}
import 'package:permission_handler/permission_handler.dart';
class AppPermissionStatus {
static final AppPermissionStatus _instance = AppPermissionStatus._internal();
AppPermissionStatus._internal() {
_initPermissionStatus();
}
static AppPermissionStatus get instance => _instance;
PermissionAbility saveAlbum = PermissionAbility(list: [
Permission.storage,
Permission.photos,
]);
PermissionAbility camera = PermissionAbility(list: [Permission.camera]);
PermissionAbility mediaPhoto = PermissionAbility(list: [Permission.storage]);
PermissionAbility photos = PermissionAbility(list: [Permission.photos]);
PermissionAbility microphone = PermissionAbility(list: [Permission.microphone]);
void _initPermissionStatus() async {}
}
class PermissionAbility {
List<Permission> list;
bool isGranted;
PermissionAbility({required this.list, this.isGranted = false});
Future<bool> getStatus() async {
bool granted = true;
int i = 0;
for (i = 0; i < list.length; i++) {
PermissionStatus status = await list[i].status;
if (status != PermissionStatus.granted) {
granted = false;
}
}
isGranted = granted;
return granted;
}
Future<bool> request() async {
bool granted = isGranted;
Map<Permission, PermissionStatus> status = await list.request();
for (int i = 0; i < list.length; i++) {
if (status[list[i]] == PermissionStatus.granted) {
granted = true;
break;
}
}
isGranted = granted;
return granted;
}
}
image_gallery_saver:
git:
url: https://gitcode.com/openharmony-sig/flutter_image_gallery_saver.git
permission_handler:
git:
url: "https://gitcode.com/openharmony-sig/flutter_permission_handler.git"
path: "permission_handler"
开发者您好,您可以查看这边新开楼层的回复。
在HarmonyOS鸿蒙Next中,Flutter鸿蒙化后,通过hmimage_picker_saver
插件实现图片保存。首先在Flutter侧调用saveImage
方法,传入图片数据。该方法通过FFI桥接调用鸿蒙原生API,触发权限申请流程。原生侧使用@ohos.file.fs
和@ohos.security.permission
模块处理存储路径和权限验证。图片保存完成后,通过回调通知Flutter更新状态。
在HarmonyOS Next中实现Flutter页面保存图片到相册,需要通过Flutter插件桥接原生能力。以下是核心步骤:
1. 创建Flutter插件
使用flutter create --template=plugin
创建插件,在lib
目录定义Dart接口:
Future<bool> saveImageToGallery(Uint8List imageData) async {
return await _channel.invokeMethod('saveImageToGallery', {'imageData': imageData});
}
2. 实现Android端(Java/Kotlin)
在android/src/main
中处理权限申请和媒体存储逻辑:
private fun saveImage(byteArray: ByteArray) {
if (checkSelfPermission(WRITE_EXTERNAL_STORAGE) != PERMISSION_GRANTED) {
requestPermissions(arrayOf(WRITE_EXTERNAL_STORAGE), REQUEST_CODE)
} else {
MediaStore.Images.Media.insertImage(contentResolver, BitmapFactory.decodeByteArray(byteArray), "Title", null)
}
}
3. 实现HarmonyOS端(ArkTS)
在ohos/src/main
中调用媒体库接口:
const mediaLibrary = require('@ohos.multimedia.mediaLibrary')
async function saveImage(data: Uint8Array) {
let media = mediaLibrary.getMediaLibrary(context)
let image = media.createAsset(mediaLibrary.MediaType.IMAGE, "image.jpg")
await image.open('w')
await image.write(data)
image.close()
}
4. Flutter页面调用
onPressed: () async {
final imageData = await _getImageBytes(); // 获取图片数据
final success = await NativeGallery.saveImageToGallery(imageData);
print(success ? "保存成功" : "保存失败");
}
注意:需在config.json
中声明ohos.permission.WRITE_IMAGEVIDEO
权限,并在运行时动态请求权限。