执行时权限(Runtime Permission)是Android 6.0( 代号为 Marshmallow,API版本号为 23)及以上版本号新增的功能。相比于以往版本号,这是一个较大变化。

本文将介绍怎样在代码中加入并配置执行时权限功能。


如需阅读英文原文,请您点击这个链接:《Everything every Android Developer must know about new Android’s Runtime Permission》

如需阅读官方执行时权限的相关介绍,请您点击这个链接:《Working with System Permissions》


执行时权限介绍

一直以来,为了保证最大的安全性。安装Android应用时,系统总是让用户选择是否允许该应用所需的全部权限。一旦安装应用,就意味着该应用所需的全部权限均已获得。

若在使用某个功能时用到了某个权限。系统将不会提醒用户该权限正在被获取(比方微信须要使用摄像头拍照。在Android 6.0曾经的设备上。用户将不会被系统告知正在使用“使用系统摄像头”的权限)。


这在安全性上是个隐患:在不经用户允许的情况下。一些应用在后台能够自由地收集用户隐私信息而不被用户察觉。


从Android 6.0版本号開始,这个隐患最终被消除了:在安装应用时,该应用无法取得不论什么权限!

相反,在使用应用的过程中。若某个功能须要获取某个权限,系统会弹出一个对话框,显式地由用户决定是否将该权限赋予应用,仅仅有得到了用户的许可,该功能才干够被使用。



须要注意的是。在上述的右图中。对话框并不会自己主动弹出,而须要由开发人员手动调用。若程序调用的某个方法须要用户赋予对应权限,而此时该权限并未被赋予时。那么程序就会抛出异常并崩溃(Crash)。例如以下图所看到的。



除此之外,用户还能够在不论什么时候撤销赋予过的权限。



执行时权限无疑提升了安全性,有效地保护了用户的隐私。这对于用户来说确实是个好消息,但对于开发人员来说简直就是噩梦:由于这须要开发人员在调用方法时,检查该方法使用了什么系统权限——这仿佛颠覆了传统的编程的逻辑——开发人员编写每一句代码时都得小心翼翼。否则应用可能随时崩溃。


在程序中。设置目标SDK版本号(targetSDKVersion)为23及以上时(这意味着程序能够在Android 6.0及以上的版本号中执行),将应用安装在Android 6.0及以上机型中,执行时权限功能才干生效;若将其安装在Android 6.0曾经的机型中,权限检查仍将仅仅发生在安装应用时。


执行时权限与各版本号间的兼容性问题

假如将一个早期版本号的应用安装在Android 6.0版本号的机型上,应用是不会崩溃的。由于这仅仅有两种情况:1)该应用的targetSDKVersion < 23,在这样的情况下,权限检查仍是早期的形式(仅在安装时赋予权限。使用时将不被提醒)。2)该应用的targetSDKVersion ≥ 23时。则将使用新的执行时权限规则。



所以,这个早期版本号的应用将执行如常。

只是。将该应用安装在Android 6.0上,且targetSDKVersion ≥ 23时。用户仍然能够随时手动撤销权限,当然这样的做法不被官方推荐。



不被推荐的原因是,这样的做法easy导致应用崩溃。若targetSDKVersion < 23。当然不会出问题;若早期应用的targetSDKVersion ≥ 23。在使用应用时手动撤消了某个权限。那么程序在调用了须要这个权限才干执行的方法时,应用什么也不做,若该方法还有返回值,那么会依据实际情况返回 0 或者 null。

例如以下图所看到的。



若上述调用的方法没有崩溃,那么这种方法被其它方法调用时也会由于返回值是 0 或者 null 而崩溃。


只是好消息是,用户差点儿不会手动撤销已经赋予给应用的权限。


说了这么多。在避免应用崩溃的前提下,适配新的执行时权限功能才是王道:对于那些在代码中并未支持执行时权限的应用,请将targetSDKVersion设置为 < 23,否则应用有崩溃隐患;若代码中支持了执行时权限。再将targetSDKVersion设置为 ≥ 23。


请注意:在Android Studio中新建Project时。会自己主动赋予targetSDKVersion为最新版本号,若您的应用还临时无法全然支持执行时权限功能,建议首先将targetSDKVersion手动设置为22。


自己主动赋予应用的权限

以下罗列了在安装应用时,自己主动赋予应用的权限,这些权限无法在安装后手动撤销。

我们称其为基本权限(Normal Permission)

android.permission.ACCESS_LOCATION_EXTRA_COMMANDS
android.permission.ACCESS_NETWORK_STATE
android.permission.ACCESS_NOTIFICATION_POLICY
android.permission.ACCESS_WIFI_STATE
android.permission.ACCESS_WIMAX_STATE
android.permission.BLUETOOTH
android.permission.BLUETOOTH_ADMIN
android.permission.BROADCAST_STICKY
android.permission.CHANGE_NETWORK_STATE
android.permission.CHANGE_WIFI_MULTICAST_STATE
android.permission.CHANGE_WIFI_STATE
android.permission.CHANGE_WIMAX_STATE
android.permission.DISABLE_KEYGUARD
android.permission.EXPAND_STATUS_BAR
android.permission.FLASHLIGHT
android.permission.GET_ACCOUNTS
android.permission.GET_PACKAGE_SIZE
android.permission.INTERNET
android.permission.KILL_BACKGROUND_PROCESSES
android.permission.MODIFY_AUDIO_SETTINGS
android.permission.NFC
android.permission.READ_SYNC_SETTINGS
android.permission.READ_SYNC_STATS
android.permission.RECEIVE_BOOT_COMPLETED
android.permission.REORDER_TASKS
android.permission.REQUEST_INSTALL_PACKAGES
android.permission.SET_TIME_ZONE
android.permission.SET_WALLPAPER
android.permission.SET_WALLPAPER_HINTS
android.permission.SUBSCRIBED_FEEDS_READ
android.permission.TRANSMIT_IR
android.permission.USE_FINGERPRINT
android.permission.VIBRATE
android.permission.WAKE_LOCK
android.permission.WRITE_SYNC_SETTINGS
com.android.alarm.permission.SET_ALARM
com.android.launcher.permission.INSTALL_SHORTCUT
com.android.launcher.permission.UNINSTALL_SHORTCUT

开发人员仅须要在AndroidManifest.xml中声明这些权限。应用就能自己主动获取无需用户授权。


为应用适配新的执行时权限


为了设配新的执行时权限,首先须要将compileSdkVersiontargetSdkVersion设置为23:

android {
compileSdkVersion 23
... defaultConfig {
...
targetSdkVersion 23
...
}

以下演示了一个添加联系人的方法,该方法是需使用WRITE_CONTACTS的权限:

private static final String TAG = "Contacts";
private void insertDummyContact() {
// Two operations are needed to insert a new contact.
ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>(2); // 1、设置一个新的联系人
ContentProviderOperation.Builder op =
ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
.withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null)
.withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null);
operations.add(op.build()); // 1、为联系人设置姓名
op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
.withValue(ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
.withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME,
"__DUMMY CONTACT from runtime permissions sample");
operations.add(op.build()); // 3、使用ContentResolver加入该联系人
ContentResolver resolver = getContentResolver();
try {
resolver.applyBatch(ContactsContract.AUTHORITY, operations);
} catch (RemoteException e) {
Log.d(TAG, "Could not add a new contact: " + e.getMessage());
} catch (OperationApplicationException e) {
Log.d(TAG, "Could not add a new contact: " + e.getMessage());
}
}

调用这种方法须要配置WRITE_CONTACTS权限,否则应用将崩溃:在AndroidManifest.xml中配置例如以下权限:

<uses-permission android:name="android.permission.WRITE_CONTACTS"/>

接着,我们须要创建一个方法用于推断WRITE_CONTACTS权限是否确实被赋予;若方法为创建,那么能够弹出一个对话框向用户申请该权限。待权限被赋予后。方可新建联系人。


权限被归类成权限组(Permission Group)。例如以下表所看到的:



若应用被赋予了某个权限组中的一个权限(比方READ_CONTACTS权限被赋予)。那么该组中的其它权限将被自己主动获取(WRITE_CONTACTSGET_ACCOUNTS权限被自己主动获取)。


检查和申请权限的方法各自是Activity.checkSelfPermission()Activity.requestPermissions。这两个方法是在 API 23 中新增的。


final private int REQUEST_CODE_ASK_PERMISSIONS = 123;

private void insertDummyContactWrapper() {
//检查AndroidManiFest中是否配置了WRITE_CONTACTS权限
int hasWriteContactsPermission = checkSelfPermission(Manifest.permission.WRITE_CONTACTS);
//若未配置该权限
if (hasWriteContactsPermission != PackageManager.PERMISSION_GRANTED) {
//申请配置该权限
requestPermissions(new String[] {Manifest.permission.WRITE_CONTACTS},
REQUEST_CODE_ASK_PERMISSIONS);
//直接返回,不执行insertDummyContact()方法
return;
}
//若配置了该权限。才干调用方法
insertDummyContact();
}

若程序赋予了权限,insertDummyContact()方法将被调用。否则,requestPermissions()方法将弹出一个对话框申请权限,例如以下所看到的:



不管您选择的是“DENY”还是“ALLOW”,程序都将回调Activity.onRequestPermissionsResult()方法,并将选择的结果传到方法的第三个參数中:

@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
switch (requestCode) {
case REQUEST_CODE_ASK_PERMISSIONS:
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 用户选择了“ALLOW”,获取权限,调用方法
insertDummyContact();
} else {
// 用户选择了“DENY”。未获取权限
Toast.makeText(MainActivity.this, "WRITE_CONTACTS Denied", Toast.LENGTH_SHORT)
.show();
}
break;
default:
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}

这就是Android 6.0的全新执行时权限机制,为了提高安全性,添加代码量在所难免:为了匹配执行时权限机制。必须把处理方法的全部情况考虑在内。


处理 “不再询问”(“Never Ask Again”)


每当系统申请权限时,弹出的对话框会有一个“不再询问”(“Never Ask Again”)的勾选项。

若用户打了勾,并选择拒绝(“DENY”)。那么下次程序调用Activity。

requestPermissions()方法时。将不会弹出对话框。权限也不会被赋予。

这样的没有反馈的交互并非一个好的用户体验(User Experience)。所以,下次启动时,程序应弹出一个对话框,提示用户“您已经拒绝了使用该功能所须要的权限,若须要使用该功能。请手动开启权限”,应调用Activity.shouldShowRequestPermissionRationale()方法:


final private int REQUEST_CODE_ASK_PERMISSIONS = 123;

private void insertDummyContactWrapper() {
int hasWriteContactsPermission = checkSelfPermission(Manifest.permission.WRITE_CONTACTS);
if (hasWriteContactsPermission != PackageManager.PERMISSION_GRANTED) {
if (!shouldShowRequestPermissionRationale(Manifest.permission.WRITE_CONTACTS)) {
showMessageOKCancel("You need to allow access to Contacts",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
requestPermissions(new String[] {Manifest.permission.WRITE_CONTACTS},
REQUEST_CODE_ASK_PERMISSIONS);
}
});
return;
}
requestPermissions(new String[] {Manifest.permission.WRITE_CONTACTS},
REQUEST_CODE_ASK_PERMISSIONS);
return;
}
insertDummyContact();
} private void showMessageOKCancel(String message, DialogInterface.OnClickListener okListener) {
new AlertDialog.Builder(MainActivity.this)
.setMessage(message)
.setPositiveButton("OK", okListener)
.setNegativeButton("Cancel", null)
.create()
.show();
}

效果例如以下:


上述对话框应在两种情形下弹出:

1)应用第一次申请权限时。

2)用户勾选了“不再询问”复选框。


对于另外一种情况,Activity.onRequestPermissionsResult()方法将被回调,并回传參数PERMISSION_DENIED,该对话框将不再弹出。


一次性申请多个权限


有些功能须要申请多个权限。仍然能够像上述方式一样编写代码:

final private int REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS = 124;

private void insertDummyContactWrapper() {
//提示用户须要手动开启的权限集合
List<String> permissionsNeeded = new ArrayList<String>(); //功能所需权限的集合
final List<String> permissionsList = new ArrayList<String>();
//若用户拒绝了该权限申请,则将该申请的提示加入到“用户须要手动开启的权限集合”中
if (!addPermission(permissionsList, Manifest.permission.ACCESS_FINE_LOCATION))
permissionsNeeded.add("GPS");
if (!addPermission(permissionsList, Manifest.permission.READ_CONTACTS))
permissionsNeeded.add("Read Contacts");
if (!addPermission(permissionsList, Manifest.permission.WRITE_CONTACTS))
permissionsNeeded.add("Write Contacts"); //若在AndroidManiFest中配置了全部所需权限,则让用户逐一赋予应用权限,若权限都被赋予,则执行方法并返回
if (permissionsList.size() > 0) {
//若用户赋予了一部分权限。则须要提示用户开启其余权限并返回。该功能将无法执行
if (permissionsNeeded.size() > 0) {
// Need Rationale
String message = "You need to grant access to " + permissionsNeeded.get(0);
for (int i = 1; i < permissionsNeeded.size(); i++)
message = message + ", " + permissionsNeeded.get(i);
//弹出对话框,提示用户须要手动开启的权限
showMessageOKCancel(message,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
requestPermissions(permissionsList.toArray(new String[permissionsList.size()]),
REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS);
}
});
return;
}
requestPermissions(permissionsList.toArray(new String[permissionsList.size()]),
REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS);
return;
} insertDummyContact();
}
//推断用户是否授予了所需权限
private boolean addPermission(List<String> permissionsList, String permission) {
//若配置了该权限,返回true
if (checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
//若未配置该权限,将其加入到所需权限的集合,返回true
permissionsList.add(permission);
// 若用户勾选了“永不询问”复选框,并拒绝了权限,则返回false
if (!shouldShowRequestPermissionRationale(permission))
return false;
}
return true;
}

当用户设置了每一个权限是否可被赋予后。Activity.onRequestPermissionsResult()方法被回调,并传入第三个參数:


@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
switch (requestCode) {
case REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS:
{
//初始化Map集合,当中Key存放所需权限,Value存放该权限是否被赋予
Map<String, Integer> perms = new HashMap<String, Integer>();
// 向Map集合中加入元素,初始时全部权限均设置为被赋予(PackageManager.PERMISSION_GRANTED)
perms.put(Manifest.permission.ACCESS_FINE_LOCATION, PackageManager.PERMISSION_GRANTED);
perms.put(Manifest.permission.READ_CONTACTS, PackageManager.PERMISSION_GRANTED);
perms.put(Manifest.permission.WRITE_CONTACTS, PackageManager.PERMISSION_GRANTED);
// 将第二个參数回传的所需权限及第三个參数回传的权限结果放入Map集合中。由于Map集合要求Key值不能反复,所以实际的权限结果将覆盖初始值
for (int i = 0; i < permissions.length; i++)
perms.put(permissions[i], grantResults[i]);
// 若全部权限均被赋予,则执行方法
if (perms.get(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
&& perms.get(Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED
&& perms.get(Manifest.permission.WRITE_CONTACTS) == PackageManager.PERMISSION_GRANTED) {
// All Permissions Granted
insertDummyContact();
}
//否则弹出toast,告知用户需手动赋予权限
else {
// Permission Denied
Toast.makeText(MainActivity.this, "Some Permission is Denied", Toast.LENGTH_SHORT)
.show();
}
}
break;
default:
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}

使用支持库(Support Library)提高程序的兼容性


虽然上述代码在Android 6.0版本号的设备上能够正常执行。但执行在早前版本号的设备上。程序将崩溃。

简单直接的方式是事先进行版本号推断:


if (Build.VERSION.SDK_INT >= 23) {
// Marshmallow+
} else {
// Pre-Marshmallow
}

但这样会使程序变得臃肿。

比較好的解决方案是使用Support Library v4支持库中的方法替换原来的方法,这将省去为不同版本号的设备分别提供代码的麻烦:


// 将Activity.checkSelfPermission()方法替换为例如以下方法
ContextCompat.checkSelfPermission()

// 将Activity.requestPermissions()方法替换为例如以下方法
ActivityCompat.requestPermissions()

//将Activity.shouldShowRequestPermissionRationale()方法替换为例如以下方法,在早期版本号中。该方法直接返回false
ActivityCompat.shouldShowRequestPermissionRationale()

不管哪个版本号,调用上面的三个方法都须要Content或Activity參数。


以下是使用Support Library v4支持库中的方法替换原代码中对应方法后的程序:


private void insertDummyContactWrapper() {
int hasWriteContactsPermission = ContextCompat.checkSelfPermission(MainActivity.this,
Manifest.permission.WRITE_CONTACTS);
if (hasWriteContactsPermission != PackageManager.PERMISSION_GRANTED) {
if (!ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this,
Manifest.permission.WRITE_CONTACTS)) {
showMessageOKCancel("You need to allow access to Contacts",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
ActivityCompat.requestPermissions(MainActivity.this,
new String[] {Manifest.permission.WRITE_CONTACTS},
REQUEST_CODE_ASK_PERMISSIONS);
}
});
return;
}
ActivityCompat.requestPermissions(MainActivity.this,
new String[] {Manifest.permission.WRITE_CONTACTS},
REQUEST_CODE_ASK_PERMISSIONS);
return;
}
insertDummyContact();
}

须要注意的是。若程序中用到了Fragment,也最好使用android.support.v4.app.Fragment,这样能够兼容更低的版本号。使应用适配很多其它设备。


使用第三方开源库(3rd Party Library)简化代码


为了是代码更加简洁,推荐一个第三方框架。该框架能够方便地集成执行时权限机制并有效兼容新旧版本号。


在应用打开时撤销权限所带来的问题


如上所述,用户能够随时撤销赋予应用的权限,若某个应用正在执行时,用户撤消了其某些权限,应用所在进程会立马终止(application’s process is suddenly terminated),所以尽量不要在应用执行时,改变其权限规则。


总结与建议

总结:

执行时权限机制大大提高了应用的安全性,只是开发人员须要为此改动代码以匹配新的版本号,只是好消息是。大部分经常使用的权限都被自己主动赋予了,所以,仅仅有非常小一部分代码须要改动。


建议:

  1. 使用执行时机制时应该以版本号的兼容作为前提。

  2. 不要将未适配执行时机制的程序的targetSdkVersion设置为 23 及以上。


感谢

特别感谢原创作者的付出,以下是作者的介绍信息:

Android 6.0及以上版本号的执行时权限介绍的更多相关文章

  1. Android6.0执行时权限解析,RxPermissions的使用,自己封装一套权限框架

    Android6.0执行时权限解析,RxPermissions的使用.自己封装一套权限框架 在Android6.0中,新添加了一个执行时的权限,我相信非常多人都已经知道了.预计也知道怎么用了,这篇博客 ...

  2. Android 6.0 执行时权限处理全然解析

    转载请标明出处: http://blog.csdn.net/lmj623565791/article/details/50709663: 本文出自:[张鸿洋的博客] 一.概述 随着Android 6. ...

  3. Android Studio2.0 Beta 2版本号更新说明及注意事项

    我们刚刚向canary channel推送了Android Studio2.0 Beta 2版本号 老毕译注: ---------- canary channel: 金丝雀版本号,平均1到2周就会更新 ...

  4. Android学习之6.0系统执行时权限设置

    今天讲讲工作中遇见的6.0运行时权限处理问题.起因就是设置版本号更新时,在6.0系统会报错,起因就是6.0动态权限设置,由于在google为了安全考虑,对于一些特定权限会征询客户授权,这当然会大大添加 ...

  5. 【译】如何在 Android 5.0 上获取 SD卡 的读写权限

    因为最近项目需要,涉及到 SD卡 的读写操作,然而申请 <!-- 读写权限 --> <uses-permission android:name="android.permi ...

  6. Android数据存储之Android 6.0运行时权限下文件存储的思考

    前言: 在我们做App开发的过程中基本上都会用到文件存储,所以文件存储对于我们来说是相当熟悉了,不过自从Android 6.0发布之后,基于运行时权限机制访问外置sdcard是需要动态申请权限,所以以 ...

  7. android 5.0新特性

    Android Lollipop 面向开发人员的主要功能 Material Design 设计 注重性能 通知 以大屏幕呈现 以文档为中心 连接性能再上一级 高性能图形 音频处理功能更强 摄像头和视频 ...

  8. Android 6.0 Changes

    原文链接:http://developer.android.com/about/versions/marshmallow/android-6.0-changes.html 伴随着新特性和功能,Andr ...

  9. Android 8.0 功能和 API

    Android 8.0 为用户和开发者引入多种新功能.本文重点介绍面向开发者的新功能. 用户体验 通知 在 Android 8.0 中,我们已重新设计通知,以便为管理通知行为和设置提供更轻松和更统一的 ...

随机推荐

  1. 洛谷P1522 牛的旅行

    题目描述 农民 John的农场里有很多牧区.有的路径连接一些特定的牧区.一片所有连通的牧区称为一个牧场.但是就目前而言,你能看到至少有两个牧区通过任何路径都不连通.这样,Farmer John就有多个 ...

  2. knockout 表单绑定 要怎么Mapping才好

    问题 之前有了解过knockout,学习过绑定语法,结合帮助文档,做个Demo倒也不成问题,但是部分地方很不爽,不知道是我的用法不对,还是功力不够. 比如说,标签里定义的data-bind属性名,必须 ...

  3. JSP中的:request.getScheme()+"://"+request.getServerName()+":"+request.getServer

    String path = request.getContextPath();  String basePath = request.getScheme()+"://"+reque ...

  4. 华为上机测试题(数字字符串转二进制-java)

    PS:此题满分,可参考 /*  * 题目:数字字符串转二进制 * 描述: 输入一串整数,将每个整数转换为二进制数,如果倒数第三个Bit是“0”,则输出“0”,如果是“1”,则输出“1”. 题目类别: ...

  5. bottle框架学习(1):命令行

    在最初的一段代码,内容如下: if __name__ == '__main__': from optparse import OptionParser _cmd_parser = OptionPars ...

  6. WORDPRESS改为https部署

    1.确保你已经正确开启了httpd 2.4.6的https配置,并且配置了该网站的虚拟主机,如下所示: <VirtualHost *:443> DocumentRoot "/ap ...

  7. [Math Review] Statistics Basics: Main Concepts in Hypothesis Testing

    Case Study The case study Physicians' Reactions sought to determine whether physicians spend less ti ...

  8. mysql里的知识

    1.mysql基础 (1)mysql存储结构:数据库->表-> 数据   sql语句 (2)管理数据库: 增加: create database 数据库 default character ...

  9. 使用参数化SQL

    Java.C#等语言提供了参数化SQL机制,使用参数化SQL开发人员为在运行时才能确定的参数值设置占位符,在执行的时候再指定这些占位符所代表的值.示例代码如下: string user=txtUser ...

  10. 两个imageView实现图片轮播

    前言 在不少的项目中,都会用到图片轮播这个功能,现在网上关于图片轮播的轮子也层出不穷,千奇百怪,笔者根据自己的思路,用两个imageView也实现了图片轮播,这里给大家介绍笔者的主要思路以及大概步骤. ...