5

【精心整理】安卓各个版本特性与适配方案

 3 years ago
source link: https://blog.csdn.net/ljphhj/article/details/116019944
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

【精心整理】安卓各个版本特性与适配方案

Android系统版本版本特性注意点实现方案Android 6.0 - SDK 23动态权限控制分为正常权限 、危险权限

如果您的应用在其清单中列出了正常权限,系统将自动授予该权限。如果您列出了危险权限,则用户必须明确批准您的应用使用这些权限。RxPermissions

EasyPermission

AndPermission休眠和应用待机模式(Doze and App Standby)若判断用户在连续的一段时间内没有使用手机,就延缓终端中APP后台的CPU和网络活动,以达到减少电量消耗的目的。注意,只是延缓并没有杀死进程。

1.Screen off 屏幕熄灭
2.Stationary 手机静止 , 即不发生位移
3.On Battery 使用电池供电
当处于上述三种状态一段时间后即进入了Doze模式,这时所有的活动都暂时被停止

Doze模式没有杀死进程,而待机模式则是做了这些操作:
1、暂停网络访问。
2、系统忽略所有的WakeLock。
3、标准的AlarmManager alarms被延缓到下一个maintenance window。
但使用AlarmManager的 setAndAllowWhileIdle、setExactAndAllowWhileIdle和setAlarmClock时,alarms定义事件仍会启动。
在这些alarms启动前,系统会短暂地退出Doze模式。
4、系统不再进行WiFi扫描。
5、系统不允许sync adapters运行。
6、系统不允许JobScheduler运行Android6.0及更高版本还提供了Doze模式白名单列表,通过设置应用程序进入白名单列表, 引导用户设置白名单, 可逃脱Doze模式的各种限制。


1.检测应用程序是否存在白名单list里面,可使用PowerManager的isIgnoringBatteryOptimizations()方法。

2.引导到电池优化界面,让用户去管理白名单

在DeviceIdle中有三中类型的白名单列表:

1.Doze模式下被限制的系统级App: mPowerSaveWhitelistAppsExceptIdle

2.所有模式下(Doze和app standby)被限制的系统级App mPowerSaveWhitelistApps

3.所有模式下被限制的用户级App: mPowerSaveWhitelistUserApps硬件标识符访问权对于使用 WLAN API 和 Bluetooth API 的应用,Android 移除了对设备本地硬件标识符的编程访问权。WifiInfo.getMacAddress() 方法和 BluetoothAdapter.getAddress() 方法现在会返回常量值 02:00:00:00:00:00

要通过蓝牙和 WLAN 扫描访问附近外部设备的硬件标识符,您的应用必须拥有 ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION 权限。

WifiManager.getScanResults()

BluetoothDevice.ACTION_FOUND

BluetoothLeScanner.startScan()Apk验证如果在清单中声明的文件在 APK 中并不存在,该 APK 将被视为已损坏。移除任何内容后必须重新签署 APK通知变化移除了 Notification.setLatestEventInfo() 方法。请改用 Notification.Builder 类来构建通知。

要重复更新通知,请重复使用 Notification.Builder 实例。调用 build() 方法可获取更新后的 Notification 实例分割线【文章出处:https://blog.csdn.net/ljphhj】Android 7.0 - SDK 24

Android 7.1 - SDK 25应用间共享文件规则禁止使用file://URI, 否则抛出FileUriExposedException异常

应该通过content://URI,并授权URI 临时访问权限需要自定义一个MyFileProvider继承于FileProvider,在清单文件中注册该<provider>,并将exported设置为false,在<provider>标签中配置<meta-data>,进行配置共享目录 Apk签名Scheme V2V1签名不会校验Apk文件的注释区

V2签名会校验Apk文件的注释区是否修改

V2签名增加一块签名区块,签名区块可以根据固定的id,获取签名信息

签名区块不会校验是否修改Toast会导致的BadTokenException异常7.1版本(sdk 25) 中,Toast的WindowManager.LayoutParams参数新增了一个Token属性,用于对添加的窗口进行校验。

当UI线程发生阻塞,导致TN.show()没有及时执行,NMS检测超时后会删除WMS中的这个Token,导致Token失效。反射获取mTN#mHandler,通过静态代理,在mHandler发送消息时加上try-catch部分机型WebView打不开X5多语言特性增加LocaleList规则,获取多语言列表

语言查找规则:

查找第一语言:
1.查找系统设置的第一语言(比如:中文简体),作为最匹配的资源
2.查找第一语言所属语言(比如:通用中文),作为匹配的资源
3.查找第一语言的其他分支语言(比如:中文简体-香港),作为匹配的资源

如果第一语言找不到,查看是否设置多个语言。
如果有设置第二语言,则查找第二语言,规则和第一语言类似,依次类推。

如果都没有,则加载values/strings.xml中的资源,如果还没有,就抛异常通知栏适配1.增加了API: Notification.DecoratedCustomViewStyle

Notification.DecoratedMediaCustomViewStyle

两个API更好地来装饰RemoteViews的通知消息

2.需要动态设置Builder.setShowWhen(true)
才会显示通知时间

3.支持Action的快捷回复,通过RemoteInput实现,且回复的消息内容支持立即添加到通知栏

4.支持通知消息分组,相似的消息达到一定数量后,会按照消息分组来显示

5.增加了NotificationManager.areNotificationsEnabled
来获取是否开启了通知权限后台优化删除3个隐式广播,来避免后台监听这些广播的应用,大量耗电
CONNECTIVITY_ACTION、ACTION_NEW_PICTURE、ACTION_NEW_VIDEOPopupWindow位置不正确1.当使用Update方法,同时设置了Gravity,Update方法在7.0以上会获取Gravity进行更新判断

2.PopupWindow高度为MATCH_PARENT时,调用showAsLocation显示的话,PopupWindow没有在指定控件的下方显示。如果使用showAsDropDown,会全屏显示。1.不要使用update方法

2.1设置高度是WRAP_CONTENT,调用showAsDropDown
2.2弹出前,先计算PopupWindow高度,再调用showAtLocation来显示。分割线【文章出处:https://blog.csdn.net/ljphhj】Android 8.0 - SDK 26

Android 8.1 - SDK 27运行时权限6.0之后引入了动态权限,应用运行时,请求权限且被授予该权限,系统会错误将在清单文件中注册的同一权限组的权限,一起授权给应用。

8.0 开始,系统只会授予应用明确请求的权限,同一权限组的权限,用户没申请,不授权。用户申请,则自动授权。按权限组来申请权限通知消息适配8.0 引入通知Channel机制,需要给通知创建渠道Channel号

用户界面将通知渠道称为通知类别悬浮窗适配授权了SYSTEM_ALERT_WINDOW权限的应用才能使用悬浮窗。
必须使用:TYPE_APPLICATION_OVERLAY
这种窗口类型才可以使用悬浮窗了。

无法再使用以下窗口类型进行悬浮窗的添加。
TYPE_PHONE
TYPE_PRIORITY_PHONE
TYPE_SYSTEM_ALERT
TYPE_SYSTEM_OVERLAY
TYPE_SYSTEM_ERROR清单文件中注册SYSTEM_ALERT_WINDOW权限

Settings.canDrawOverlays: 检测是否授权,没有的话6.0之后要动态授权

跳授权的设置页的Action:
Settings.ACTION_MANAGE_OVERLAY_PERMISSION安装Apk的区别8.0去除了“允许未知来源”选项

App有安装其他App/自身App的功能1.在 AndroidManifest 中 添加安装未知来源应用的权限
REQUEST_INSTALL_PACKAGES

2.可以使用:canRequestPackageInstalls
查询是否有此权限

3.没权限,可以通过Action : Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES
来跳转到设置页面让用户进行授权透明主题的Activity8.0 上有Bug,8.1上修复了。

8.0上的Bug : 只有全屏不透明的Activity才可以设置方向,否则报错:java.lang.IllegalStateException: Only fullscreen opaque activities can request orientation解决方案:
1.去掉透明activity的screenOrientation属性 / 设置旋转的代码.

2.去掉透明

(最佳方案)3.增加values-v26的目录,设置背景色在8.0上不是透明,单独处理8.0 集合的区别在8.0开始,AbstractCollection.removeAll(null) 和 AbstractCollection.retainAll(null)
都会引发 NullPointerException异常

而8.0之前集合为null不会引发NullPointerException异常要做操作前,需要判空检查一下后台服务、广播限制规则加入两方面限制:

1.后台服务限制
处于空闲状态时,应用可以使用的后台服务存在限制。 这些限制不适用于前台服务,因为前台服务更容易引起用户注意

【前台服务的认定规则】
1.1 具有可见Activity
1.2 具有前台服务
1.3 另一个前台应用已关联到该应用(关联方式不管是Service还是ContentProvider)
1.4 IME
1.5 壁纸服务
1.6 通知侦听器
1.7 语音或者文本服务

2.广播限制
除了有限的例外情况,无法再使用清单注册隐式广播
它们仍然可以在运行时注册这些广播,并且可以使用清单注册专门针对它们的显式广播。

需要签名权限的广播不受此限制所限,因为这些广播只会发送到使用相同证书签名的应用,而不是发送到设备上的所有应用。后台服务会被kill掉,官方推荐可使用AlarmManager、SyncAdapter、JobScheduler代替后台服务。

1.使用JobScheduler来解决这些限制 (参考 Android-JobScheduler)

2.后台任务google推荐方案使用 WorkManager,

WorkManager 内部维护着JobScheduler,自动维护后台任务,同时满足后台Service和静态广播,在6.0 以下系统版本会自动切换为 AlarmManager

3.调用 startForegroundService() 以启动应用中的某个前台服务
前台服务会以通知的形式持续显示在通知区域后台位置信息限制为降低耗电量,8.0开始对后台应用获取用户当前位置信息的频率进行限制,应用每小时仅能接收几次位置信息更新,会出现后台应用定位收不到定位信息。分割线【文章出处:https://blog.csdn.net/ljphhj】Android 9.0 - SDK 28新特性1.利用WI-FI RTT技术实现室内定位

2.刘海屏API支持

3.通知栏功能增强(支持Bubble)

4.多摄像头支持,摄像头更新

5.HDR VP9视频,HEIF图像压缩,Media API调整Non-SDK接口使用9.0开始,无法再通过反射,JNI间接地使用非SDK的API接口了

所以需要做一下适配,如果有使用这种方式的话挖孔屏适配有状态栏的页面,不会受挖孔屏特性的影响

全屏显示的页面,系统挖孔屏方案会把应用界面下移,避开挖空区域,进行显示1.新增挖孔屏尺寸和位置接口:

WindowInsets#getDisplayCutout()

2.使用了新的窗口布局模式,可通过API获取是否在挖空区域布局
WindowManager.LayoutParams#layoutInDisplayCutoutMode

2.1 LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT : 默认模式,全屏窗口不使用挖孔区域,非全屏窗口正常使用挖孔区域

2.2 LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS : 使用挖孔区域

2.3
LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER : 声明不使用挖孔区域Battery Improvements 功耗优化AAB(Auto Awesome Battery)
1.通过ML算法将应用进行分类,不同类型的应用功耗管控策略不一样
2.Firebase Cloud Messaging (FCM):管控三方消息接收的频率
3.应用的管控方法:Forced App Standby (FAS),谷歌不会通过清理应用来优化功耗

Extreme Battery Saver(EBS)谷歌超级省电模式

Smart screen brightness:屏幕亮度调节优化算法

影响:
谷歌功耗方案对三方应用各种管控,存在导致应用后台功能无法正常使用的可能不允许共享WebView数据目录不能再跨进程共享单个WebView数据目录,否则会崩溃移除对 Build.serial 的直接访问需要 Build.serial属性字段的话,必须请求 READ_PHONE_STATE 权限然后使用 9.0 新增的新 Build.getSerial() 函数来进行获取SELinux 禁止访问应用的数据目录不允许直接通过路径访问其他应用的数据目录可以使用进程间通信 (IPC) 机制(包括通过传递 FD)共享数据分割线【文章出处:https://blog.csdn.net/ljphhj】Android 10 - SDK 29定义媒体权限集合操作其他应用已创建的文件,必须首先请求相应的权限

Photos:READ_MEDIA_IMAGES 权限
Videos:READ_MEDIA_VIDEO 权限
Music:READ_MEDIA_AUDIO 权限
Downloads:无权限限制,但需要使用系统的文件选择器选择文件访问共享集合,需通过 MediaStore APIMediaStore.Images
MediaStore.Video
MediaStore.Audio
MediaStore.Downloads应用的私有目录访问变更为每个应用程序提供了一个独立的在外部存储设备的存储沙箱,没有其他应用可以直接访问您应用的沙盒文件。由于文件是私有的,因此访问这些文件不再需要任何权限。获取外部存储私有文件的最佳位置:即Context.getExternalFilesDir返回的位置,因为此位置在所有Android版本中表现一致

使用此方法时,请传入与要创建或打开的文件类型对应的媒体环境。例如,要访问或保存app-private图像,请调用Context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)新增ACCESS_MEDIA_LOCATION权限使用这个权限,可以获取照片中的位置信息//从照片流中读取位置信息
photoUri = MediaStore.setRequireOriginal(photoUri);
InputStream stream = getContentResolver().openInputStream(photoUri);存储新特性识别特定的外部存储设备 //获取卷名方式
Set<String> volumeNames = MediaStore.getAllVolumeNames(context);Activity后台活动限制对应用未经通知用户就启动App,进行了极大地限制
只有在满足以下一个或多个条件时才能启动Activity:

1. App具有可见Activity窗口
2.位于前台的另一个App发送属于该应用程序的PendingIntent
3.系统发送属于该App的PendingIntent (比如:点击通知消息)
4.系统向应用程序发送广播 (比如:SECRET_CODE_ACTION)Activity活动限制的兼容性建议我们后台应用程序都应创建通知,以便向用户提供信息,而不是直接启动活动

一些特殊情况如:来电或者警报,需要立刻启动 Activity,则可以通过创建高优先级的通知,并提供FullScreen Intent

创建优先级高的通知:NotificationCompat.Builder#setPriority()

如果设置为最高优先级,那么它会直接显示在屏幕顶部,确保这个通知对于用户来说确实是至关重要的,否则如果用户产生了反感设备位置权限的访问控制用户可以更好地控制应用何时可以访问设备位置,运行的应用程序请求位置访问时,会通过对话框的形式给用户进行授权提示。

此对话框允许用户授予对两个不同范围的位置访问权限:在使用中(仅限前台)或始终(前台和后台)。

新增权限:ACCESS_BACKGROUND_LOCATION如果你的应用针对 Android Q 并且需要在后台运行时访问用户的位置,则必须在应用的清单文件中声明新权限

ACCESS_COARSE_LOCATION

ACCESS_BACKGROUND_LOCATION位置限制的兼容性1.如果你的应用为 ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION 声明  标记,则系统会在安装期间自动为ACCESS_BACKGROUND_LOCATION 添加标记

2.如果你的应用请求 ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION,系统会自动将 ACCESS_BACKGROUND_LOCATION添加到请求中

3.虽然你的应用可以请求并接收 ACCESS_BACKGROUND_LOCATION,但用户可以通过选择您的应用仅应在前台访问位置信息,来撤消此权限,那就无法在后台获取位置允许应用程序降级当对商店更新后的版本后悔时,可以“回到过去”即回滚到旧版对数据和标识符的更改1.获取联系人信息,结果不再按交互频率排序

2.MAC地址随机化,默认传输随机的MAC 地址

3.唯一标识符
需要 READ_PRIVILEGED_PHONE_STATE 权限,才能访问设备的不可重置标识符,包括 IMEI、序列号

4.访问剪贴板数据
除默认输入法程序外,没有焦点的应用无法访问剪贴板数据

5.访问USB串行需要用户许可
只能在用户授予您访问USB设备权限后才能读取序列号

6.相机和连接相关更改
调用getCameraCharacteristics()方法获取返回的信息的广度,必须具有 CAMERA 权限才能访问getCameraCharacteristics()方法的返回值中包含的设备元数据获取随机MAC地址I:WifiConfiguration.getRandomizedMacAddress()
获取实际硬件MAC地址:WifiInfo.getFactoryMacAddress()启用、禁用Wi-Fi的限制1.无法启用、停用Wi-Fi
2.WifiManager.setWifiEnabled() 方法始终返回false
3.只能使用设置面板提示用户启用、禁用Wi-Fi分割线【文章出处:https://blog.csdn.net/ljphhj】Android 11 - SDK 30分区存储强制执行在Android 10中还可以设置android:requestLegacyExternalStorage="true",就可以不启动分区存储,让以前的文件读取正常使用。但是在Android 11开始就不行了,会强制开启分区存储。

1.公共目录:Downloads、Documents、Pictures 、DCIM、Movies、Music、Ringtones等
1.1 公共目录的文件在App卸载后,不会删除
1.2 拥有权限,也能通过路径直接访问
1.3 可以通过SAF(Storage Access Framework)、MediaStore接口访问

2.应用专属目录
2.1 应用专属目录只能自己直接访问
2.2 App卸载,数据会清除关于分区存储,在Android10就已经推行了,简单的说,就是应用对于文件的读写只能在沙盒环境,也就是属于自己应用的目录里面读写。其他媒体文件可以通过MediaStore进行访问。媒体文件访问权限为了在保证用户隐私的同时可以更轻松地访问媒体,Android 11 增加了以下功能。执行批量操作和使用直接文件路径和原生库访问文件

1.执行批量操作 (MediaStore API)
1.1 createWriteRequest() 用户向应用授予对指定媒体文件组的写入访问权限的请求
1.2 createFavoriteRequest()用户将设备上指定的媒体文件标记为“收藏”的请求。对该文件具有读取访问权限的任何应用都可以看到用户已将该文件标记为“收藏”
1.3 createTrashRequest() 用户将指定的媒体文件放入设备垃圾箱的请求。垃圾箱中的内容会在系统定义的时间段后被永久删除
1.4 createDeleteRequest() 用户立即永久删除指定的媒体文件(而不是先将其放入垃圾箱)的请求


2.Android11又恢复了使用直接文件路径访问访问媒体文件
2.1 File API去操作
2.2 So库 (例如 fopen )PendingIntent editPendingIntent = MediaStore.createWriteRequest(contentResolver, urisToModify);

startIntentSenderForResult(editPendingIntent.getIntentSender(),
EDIT_REQUEST_CODE, null, 0, 0, 0);软件包可见性如果你想去获取其他应用的信息,比如包名,名称等等,不能直接获取了,必须必须在清单文件中添加<queries>元素,告知系统你要获取哪些应用信息或者哪一类应用查询特定软件包及与之交互

给定 intent 过滤器的情况下查询应用及与之交互如果您知道要查询或与之交互的一组特定应用(例如,与您的应用集成的应用或您使用其服务的应用),请将其软件包名称添加到 <queries> 元素内的一组 <package> 元素中<manifest package="com.example.game">
<queries>
<package android:name="com.example.store" />
<package android:name="com.example.services" />
</queries>
...
</manifest>查询所有应用及与之交互在极少数情况下,您的应用可能需要查询设备上的所有已安装应用或与之交互,不管这些应用包含哪些组件。为了允许您的应用看到其他所有已安装应用,Android 11 引入了QUERY_ALL_PACKAGES权限

为了尊重用户隐私,您的应用应请求应用正常工作所需的最小软件包可见性。适合使用这个权限的一些APP:

1. Launcher App
2. Accessibility App
3. 浏览器
4. 点对点(P2P) 共享应用
5. 设备管理应用
6. 安全应用所有文件访问权限绝大多数需要共享存储空间访问权限的应用都可以遵循分区存储最佳做法,例如存储访问框架或 MediaStore API。但是,某些应用(如文件管理器应用、备份和恢复应用以及文档管理应用)的核心用例需要广泛访问设备上的文件,但无法采用注重隐私保护的存储最佳做法高效地完成这些操作。应用可通过执行以下操作,向用户请求名为“所有文件访问权限”的特殊应用访问权限:

在清单中声明MANAGE_EXTERNAL_STORAGE权限

使用ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION 作为Action, 将用户引导至一个系统设置页面,在该页面上,用户可以为您的应用启用以下选项:授予所有文件的管理权限。

如需确定您的应用是否已获得 MANAGE_EXTERNAL_STORAGE 权限,请调用Environment.isExternalStorageManager()。电话号码相关权限TelecomManager 类中的 getLine1Number() 方法

TelecomManager 类中的 getMsisdn() 方法

当用到这两个API的时候,原来的READ_PHONE_STATE权限不管用了,需要READ_PHONE_NUMBERS权限才行自定义消息框视图被屏蔽包含自定义视图的消息框在从后台发布时会被屏蔽(比如在后台弹出一个Toast)

如果您的应用仍尝试从后台发布包含自定义视图的消息框,系统不会向用户显示相应的消息,而会在Log中输出:

NotificationService: Blocking custom toast from package , <package> due to package not in the foreground媒体intent操作需要系统默认相机Android 11 开始,只有预装的系统相机应用可以响应以下 intent 操作

android.media.action.VIDEO_CAPTURE

android.media.action.IMAGE_CAPTURE

android.media.action.IMAGE_CAPTURE_SECURE

如果您希望自己的应用使用特定的第三方相机应用来代表其捕获图片或视频,可以通过为 intent 设置软件包名称或组件来使这些 intent 变得明确。5G1.Android 11 添加了在您的应用中支持 5G 的功能

2.检测是否连接到了5G网络 (TelephonyManager)

3.检查按流量计费性TelephonyDisplayInfo:

TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_LTE_ADVANCED_PRO -> 高级专业版 LTE (5Ge)

TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_NSA -> NR (5G) - 5G Sub-6 网络

TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_NSA_MMWAVE -> 5G+/5G UW - 5G mmWave 网络需要 APK 签名方案 v2用户无法在搭载 Android 11 的设备上安装或更新仅通过 APK 签名方案 v1 签名的应用,所以如果要保持兼容,那么还必须使用 APK 签名方案 v2 或更高版本进行签名后台位置信息访问权限在搭载 Android 11 的设备上,当应用中的某项功能请求在后台访问位置信息时,用户看到的系统对话框不再包含用于启用后台位置信息访问权限的按钮。如需启用后台位置信息访问权限,用户必须在设置页面上针对应用的位置权限设置一律允许选项。旧版本:
申请了前台位置权限,就会同时获得后台位置权限
requestPermissions(arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION), 100)

11后的版本:
必须单独申请后台位置权限,而且,要在获取前台权限之后,顺序还不能乱

requestPermissions(arrayOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION), 100)

如果在没获取前台权限的时候执行这个获取后台权限的代码会没反应,等获取前台权限(ACCESS_COARSE_LOCATION)之后,申请后台权限就会跳转到一个新的权限页面了,而且必须选择“Allow all the time ”(始终允许)才能获得后台位置权限数据访问审核Android 11 引入了数据访问审核功能

这个功能就是提供了可以监听危险权限调用的监听。主要涉及到的方法是AppOpsManager.OnOpNotedCallback。无论是应用本身,还是依赖库或者SDK中的代码,只要访问到私密数据(危险权限),都会回调给我们AppOpsManager

其中OnOpNotedCallback 一共三个回调方法:

1、onNoted 正常情况下都会回调到该方法

2、onAsyncNoted 如果数据访问并非发生在应用调用API期间,就会调用onAsyncNoted(),比如一些监听器的回调。

3、onSelfNoted 在极少数情况下,如果应用将自身的UID传递到 noteOp(),需要调用 onSelfNoted()用户可以选择权限单次授权(仅此一次)就是在申请与位置信息、麦克风或摄像头相关的权限时,系统会自动提供一个单次授权的选项,只供这一次权限获取,然后用户下次打开app的时候,系统会再次提示用户授予权限。这个影响应该不大,只要我们每次使用的时候都去判断权限,没有就去申请即可。
***转载请注明出处:https://blog.csdn.net/ljphhj (CSDN 胖虎)***

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK