Android R(Android 11 API 30)于2020年9月9日正式发布,随国内各终端厂商在售Android设备的版本更新升级,应用软件对Android R 版本的兼容适配已迫在眉睫。

对于Android R的新特性,这里按照以下几个方面进行了归纳:分区存储、权限、隐私、性能、安全

官方文档描述:https://developer.android.google.cn/about/versions/11

一、分区存储

从Android 10(API 29)开始,Android默认开启分区存储功能,不过Android 10 可通过增加android:requestLegacyExternalStorage="true"配置停用分区存储

从Android 11(API 30)开始,强制执行分区存储,对于Android 11及以上设备,android:requestLegacyExternalStorage="true"配置将不再有效。

Android 11 分区存储官方描述:

https://developer.android.google.cn/training/data-storage#scoped-storage

Android 10 默认开启分区存储:

https://xiaxl.blog.csdn.net/article/details/103125117

1.1、访问目录

开启分区存储后,应用默认情况下只能访问应用专属目录(内部存储、外部存储应用专属目录),以及本应用所创建的特定类型的媒体文件

  • 应用专属目录

    包括内部存储外部存储专属目录(若应用包名com.xiaxl.demo):

    /data/data/com.xiaxl.demo/files,

    /sdcard/Android/data/com.xiaxl.demo/files

    分别采用以下API进行访问:

    File appFile = new File(context.getFilesDir(), filename);

    File appExternalFile = new File(context.getExternalFilesDir(), filename);

  • 共享存储目录

    包括媒体、文档和其他文件。例如DCIM、Pictures、Movies、Download等目录;

    注:

    Android 10(Android Q)中共享存储目录使用MediaStore API访问;

    Android 11(Android R)中共享存储目录支持MediaStore API与File API访问。

    为保证应用在Android 10、Android 11设备中,使用File API对共享存储目录具有相同的文件访问权限。建议在应用 AndroidManifest配置文件中,增加requestLegacyExternalStorage="true"标识,以关闭Android 10设备上的分区存储功能,使分区存储只对Android 11以上设备生效

1.2、访问所需权限

  • 应用专属目录

    应用专属目录(内部存储外部存储专属目录)的读写,Android 4.4以上设备不需要任何权限;
  • 共享存储目录

    共享存储路径的读写,需要READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE 权限;

Android 11以上设备中,如果您的应用再次请求READ_EXTERNAL_STORAGE权限时,动态权限申请弹窗将变化为“您的应用正在请求访问照片和媒体”

文件媒体访问 官方描述:

https://developer.android.google.cn/training/data-storage#scoped-storage

1.3、共享文件

如果需要与其他应用共享单个文件或应用数据,可以使用API:

  • FileProvider(分享自己的一个或多个文件)

    如果应用需要将自己的一个或多个文件提供给其他应用,安全的做法是向接收方应用发送文件的内容 URI,并授予对该 URI 的临时访问权限。

    Android FileProvider 组件提供了 getUriForFile() 方法,用于生成文件的内容 URI
  • ContentProvider(获取替他应用提供的数据)

    如果您需要向其他应用提供数据,可以使用ContentProvider

    ContentProvider是一种标准接口,可将一个进程中的数据与另一个进程中运行的代码进行连。

Android 11 共享文件官方描述:

https://developer.android.google.cn/training/data-storage#scoped-storage

1.4、所有文件的访问权限

有一些应用需要获取所有文件的访问权限,例如:文件管理器软件。

获取所有文件的访问权限,可申请MANAGE_EXTERNAL_STORAGE权限。

// 权限配置
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /> // 是否拥有MANAGE_EXTERNAL_STORAGE权限判断
Environment.isExternalStorageManager(); // 跳转到设置页,请求用户授权
Intent intent = new Intent();
intent.setAction(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
startActivity(intent);

MANAGE_EXTERNAL_STORAGE相关官方描述:

https://developer.android.google.cn/training/data-storage/manage-all-files

二、权限

Android 11 中对权限进行了如下更改:

  • 新增 READ_PHONE_NUMBERS权限,获取手机号码;
  • 后台访问位置权限调整;
  • 用户多次针对某项特定的权限请求拒绝,表示用户希望不再询问
  • 应用长时间未使用,系统会自动重置用户已授予敏感权限
  • 针对位置、麦克风、摄像头授权弹窗新增仅限这一次授权按钮;
  • SYSTEM_ALERT_WINDOW 权限授权方式改变为系统自动授权;

参考 Android 11 权限更新官方文档:

https://developer.android.google.cn/about/versions/11/privacy/permissions#one-time

2.1、新增 READ_PHONE_NUMBERS 权限

当应用的 targetSdkVersion>=30 时,使用以下API获取手机号码时,需要申请READ_PHONE_NUMBERS权限,而不再是READ_PHONE_STATE 权限。

  • TelephonyManager 类和 TelecomManager 类中的 getLine1Number() 方法。
  • TelephonyManager 类中不受支持的 getMsisdn() 方法。

在Android 10及之前的设备,可以继续使用READ_PHONE_STATE获取手机号;

对Android11及以上设备,需获取READ_PHONE_NUMBERS权限,才能获取手机号;

<manifest>
<!-- 仅在Android 10及以下设备获取READ_PHONE_STATE权限,以获取终端手机号码-->
<uses-permission android:name="READ_PHONE_STATE"
android:maxSdkVersion="29" />
<!-- Android 11及以上设备获取READ_PHONE_NUMBERS权限,以获取终端手机号码-->
<uses-permission android:name="READ_PHONE_NUMBERS" />
</manifest>

对于READ_PHONE_STATE权限

READ_PHONE_NUMBERS权限官方API描述:

https://developer.android.google.cn/reference/android/Manifest.permission#READ_PHONE_NUMBERS

2.2、后台访问位置权限调整

  • 在Android10设备上,同时申请前台、后台位置权限时,并在用户选择始终允许后,才能获得后台位置权限。
  • 在Android11设备上,对于targetSdkVersion<=29(Android 10)的应用,同时申请前台、后台位置权限时,对话框不再提示始终允许字样,而是提供了位置权限的设置入口,需要用户在设置页面选择始终允许才能获得后台位置权限。
  • 在Android11设备上,对于targetSdkVersion=30(Android 11)的应用,同时申请前台、后台位置权限时,系统会忽略该请求,无任何响应(需首先获取前台位置权限,再次申请后台位置权限)。
  • 在Android11设备上,对于targetSdkVersion=30(Android 11)的应用,先申请前台位置权限,后申请后台位置权限

后台访问位置权限 官方描述:

https://developer.android.google.cn/training/location/background

a、Android10设备

在Android10设备上,同时申请前台、后台位置权限时,并在用户选择始终允许后,才能获得后台位置权限。

// 在Android10设备上,同时 申请前台、后台位置权限
ActivityCompat.requestPermissions(this,
new String[]{
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_BACKGROUND_LOCATION}, 101);

b、Android11设备 targetSdkVersion<=29

在Android11设备上,对于targetSdkVersion<=29(Android 10)的应用,同时申请前台、后台位置权限时,对话框不再提示始终允许字样,而是提供了位置权限的设置入口,需要用户在设置页面选择始终允许才能获得后台位置权限。

// 在Android11设备上,targetSdkVersion<=29的应用,同时 申请前台、后台位置权限
ActivityCompat.requestPermissions(this,
new String[]{
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_BACKGROUND_LOCATION}, 101);

c、Android11设备 targetSdkVersion=30 同时申请前台、后台位置权限

  • 在Android11设备上,对于targetSdkVersion=30(Android 11)的应用,同时申请前台、后台位置权限时,系统会忽略该请求,无任何响应(需首先获取前台位置权限,再次申请后台位置权限)。
// 在Android11设备上,targetSdkVersion=30的应用,同时 申请前台、后台位置权限
// 请求无反应,此为错误写法
ActivityCompat.requestPermissions(this,
new String[]{
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_BACKGROUND_LOCATION}, 101);

d、Android11设备 targetSdkVersion=30 依次申请前台、后台位置权限

在Android11设备上,对于targetSdkVersion=30(Android 11)的应用,先申请前台位置权限,后申请后台位置权限

// 在Android11设备上,targetSdkVersion=30的应用,申请前台位置权限
ActivityCompat.requestPermissions(this,
new String[]{
Manifest.permission.ACCESS_COARSE_LOCATION}, 101);

Android11设备上,targetSdkVersion=30的应用,申请后台位置权限,直接跳转到设置页面。

// 在Android11设备上,targetSdkVersion=30的应用,申请后台位置权限
ActivityCompat.requestPermissions(this,
new String[]{
Manifest.permission.ACCESS_BACKGROUND_LOCATION}, 101);

2.3、用户多次针对某项特定的权限请求拒绝

在 Android 11 中,用户多次针对某项特定的权限请求点击了拒绝,那么应用再次请求该项权限时,用户将不会看到系统权限弹窗,该操作表示用户希望不再询问

2.4、长时间未使用,自动重置已授予敏感权限

在 Android 11 中,当targetSdkVersion>=30时,应用在一段时间内未使用,系统会通过自动重置用户已授予应用的运行时敏感权限来保护用户数据;

2.5、新增“仅限这一次”授权按钮

从 Android 11(API 级别 30)开始,当应用请求与位置、麦克风、摄像头相关权限时,面向用户的授权对话框会包含仅限这一次选项;如果用户在对话框中选择仅限这一次,系统会向应用授予临时的单次授权。

权限申请API使用方式不变:

private void showCameraPreview() {
// 判断是否拥有Camera权限
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
== PackageManager.PERMISSION_GRANTED) {
// 进入Camera页面
// startCamera();
} else {
// 请求Camera权限
requestCameraPermission();
}
} private void requestCameraPermission() {
// 判断Camera权限,之前是否已被用户"拒绝"
if (ActivityCompat.shouldShowRequestPermissionRationale(this,
Manifest.permission.CAMERA)) {
// 弹窗告诉用户,为什么需要Camera权限
Snackbar.make(mLayout, R.string.camera_access_required,
Snackbar.LENGTH_INDEFINITE).setAction(R.string.ok, new View.OnClickListener() {
@Override
public void onClick(View view) {
// 请求Camera权限
ActivityCompat.requestPermissions(MainActivity.this,
new String[]{Manifest.permission.CAMERA},
PERMISSION_REQUEST_CAMERA);
}
}).show(); } else {
// 请求Camera权限
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.CAMERA}, PERMISSION_REQUEST_CAMERA);
}
} @Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
if (requestCode == PERMISSION_REQUEST_CAMERA) {
// 用户授权Camera(用户选择"使用使用时允许"、"仅这一次允许")
if (grantResults.length == 1
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// Permission has been granted. Start camera preview Activity.
Snackbar.make(mLayout, R.string.camera_permission_granted,
Snackbar.LENGTH_SHORT)
.show();
startCamera();
}
// 用户选择"拒绝"
else {
// Permission request was denied.
Snackbar.make(mLayout, R.string.camera_permission_denied,
Snackbar.LENGTH_SHORT)
.show();
}
}
}

源码参考:

https://github.com/android/permissions-samples/tree/main/RuntimePermissionsBasic

2.6、SYSTEM_ALERT_WINDOW 权限授权方式

在 Android 11 中,SYSTEM_ALERT_WINDOW 权限授权方式更改为:根据请求自动向某些应用授予 SYSTEM_ALERT_WINDOW 权限

  • 系统会自动向具有 ROLE_CALL_SCREENING 且请求 SYSTEM_ALERT_WINDOW 的所有应用授予该权限。如果应用失去 ROLE_CALL_SCREENING,就会失去该权限。

    ROLE_CALL_SCREENINGRoleManager中的常量类,多用于通知用户将我们的应用替换掉手机自带的预搭载应用(短信、电话拨号);
  • 系统会自动向通过 MediaProjection 截取屏幕且请求 SYSTEM_ALERT_WINDOW 的所有应用授予该权限,除非用户已明确拒绝向应用授予该权限。当应用停止截取屏幕时,就会失去该权限。此用例主要用于游戏直播应用。

SYSTEM_ALERT_WINDOW权限 官方描述:

https://developer.android.google.cn/about/versions/11/privacy/permissions#system-alert

三、隐私保护

主要更改涉及以下几个方面:

  • 软件包可见性:获取其他应用信息需在AndroidManifest中增加<queries>标签;
  • 前台服务:访问位置信息、摄像头、麦克风限制;
  • 永久 SIM 卡标识符 ICCID 获取受限;
  • 新增AppOpsManager.OnOpNotedCallback监听危险权限的调用,从而保护用户的私密数据;

    这样对于第三方依赖库的权限使用申请可以做一个监控

3.1、软件包可见性

  • 在 Android 11 及更高版本设备中,当应用的 targetSdkVersion>=30 时,如果应用希望获取其他应用的信息(比如:包名、软件名称),原有方式将无法获取到。
  • 如需获取其他应用信息,需要在AndroidManifest中增加<queries>元素标签,告知系统希望获取哪些应用的信息或者哪一类应用的信息。
  • 如果需要获取所有应用的信息(比如:Launcher应用、设备管理器应用):这种情况只需要在AndroidManifest中添加QUERY_ALL_PACKAGES权限即可。

    QUERY_ALL_PACKAGES权限为普通权限,不需要进行动态申请。但提交应用市场后,应用市场可能会进行审核

软件包可见性 官方描述:

https://developer.android.google.cn/about/versions/11/privacy/package-visibility

 <manifest package="com.xiaxl.myapp">

	// 1、若知道具体应用的包名
<queries>
<package android:name="com.xiaxl.otherapp01" />
<package android:name="com.xiaxl.otherapp01" />
</queries>
// 2、不知道包名,但想知道某一类App的应用信息
<queries>
<intent>
<action android:name="android.intent.action.SEND" />
<data android:mimeType="image/jpeg" />
</intent>
</queries>
</manifest>

3.2、前台服务:访问位置信息、摄像头、麦克风限制

当应用的 targetSdkVersion>=30 时,前台服务访问位置信息、摄像头、麦克风时,需添加foregroundServiceType

<manifest>
// 前台服务访问:位置信息、摄像头、麦克风
<service
android:foregroundServiceType="location|camera|microphone" />
</manifest>

前台服务 官方描述:

https://developer.android.google.cn/about/versions/11/privacy/foreground-services

3.3、永久 SIM 卡标识符 ICCID 获取受限

在 Android 11 及更高版本中,使用 SubscriptionInfo.getIccId() 方法访问不可重置的 ICCID 受到限制。

SubscriptionInfo.getIccId() 方法会返回一个非null的空字符串

如需唯一标识设备上安装的 SIM 卡,请改用 getSubscriptionId() 方法。SubscriptionId会提供一个索引值,用于唯一识别已安装的 SIM 卡(包括实体 SIM 卡和电子 SIM 卡),除非设备恢复出厂设置,否则此标识符的值对于给定 SIM 卡是保持不变的。

3.4、监听危险权限的调用

Android 11新增AppOpsManager.OnOpNotedCallback为开发者提供对应用危险权限的使用监听,从而保护用户的私密数据

当应用以及应用的依赖包中,申请某项危险权限时,AppOpsManager.OnOpNotedCallback的对应回调方法将会被调用,从而打印申请的权限对应的API调用栈

举例:

使用位置权限获取位置信息时,将会回调AppOpsManager.OnOpNotedCallback中的onNoted方法,并打印使用的权限对应的API调用栈

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//
AppOpsManager.OnOpNotedCallback appOpsCallback =
new AppOpsManager.OnOpNotedCallback() {
private void logPrivateDataAccess(String opCode, String trace) {
Log.i("xiaxl: ", "opCode: " + opCode + "\n trace: " + trace);
} @Override
public void onNoted(@NonNull SyncNotedAppOp syncNotedAppOp) {
Log.i("xiaxl: ", "---onNoted---");
logPrivateDataAccess(syncNotedAppOp.getOp(),
Arrays.toString(new Throwable().getStackTrace()));
} @Override
public void onSelfNoted(@NonNull SyncNotedAppOp syncNotedAppOp) {
Log.i("xiaxl: ", "---onSelfNoted---");
logPrivateDataAccess(syncNotedAppOp.getOp(),
Arrays.toString(new Throwable().getStackTrace()));
} @Override
public void onAsyncNoted(@NonNull AsyncNotedAppOp asyncNotedAppOp) {
Log.i("xiaxl: ", "---onAsyncNoted---");
logPrivateDataAccess(asyncNotedAppOp.getOp(),
asyncNotedAppOp.getMessage());
}
}; AppOpsManager appOpsManager = getSystemService(AppOpsManager.class);
if (appOpsManager != null) {
appOpsManager.setOnOpNotedCallback(getMainExecutor(), appOpsCallback);
}
} public void getLocation() {
// 创建归因
Context attributionContext = createAttributionContext("shareLocation");
// 获取位置信息
LocationManager locationManager =
attributionContext.getSystemService(LocationManager.class);
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED
&& ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
return;
}
Location lastKnownLocation = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
}

打印日志如下:

---onNoted---
opCode: android:coarse_location
trace:
[com.xiaxl.android_test.MainActivity$1.onNoted(MainActivity.java:42),
android.app.AppOpsManager.readAndLogNotedAppops(AppOpsManager.java:8204),
android.os.Parcel.readExceptionCode(Parcel.java:2304),
android.os.Parcel.readException(Parcel.java:2279),
android.location.ILocationManager$Stub$Proxy.getLastLocation(ILocationManager.java:1225),
android.location.LocationManager.getLastKnownLocation(LocationManager.java:648),
com.xiaxl.android_test.MainActivity.getLocation(MainActivity.java:87),
com.xiaxl.android_test.MainActivity$2.onClick(MainActivity.java:70),
android.view.View.performClick(View.java:7448),
com.google.android.material.button.MaterialButton.performClick(MaterialButton.java:967),
android.view.View.performClickInternal(View.java:7425),
android.view.View.access$3600(View.java:810),
android.view.View$PerformClick.run(View.java:28305),
android.os.Handler.handleCallback(Handler.java:938),
android.os.Handler.dispatchMessage(Handler.java:99),
android.os.Looper.loop(Looper.java:223),
android.app.ActivityThread.main(ActivityThread.java:7656),
java.lang.reflect.Method.invoke(Native Method),
com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592),
com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)]

从以上日志可以看出,当应用申请ACCESS_COARSE_LOCATION权限并获取位置信息时,打印了应用申请的权限对应的API调用栈

AppOpsManager 相关官方描述:

https://developer.android.google.cn/guide/topics/data/audit-access#audit-by-attribution-tag

四、性能

  • JobScheduler使用频率进行限制

4.1、JobScheduler使用频率进行限制

Android 11 为对JobScheduler使用频率进行一定限制。

对于 debuggable 清单属性设置为 true 的应用,过多的调用 JobScheduler API 将返回 RESULT_FAILURE

JobScheduler主要用于在未来某个时间下满足一定条件时触发执行某项任务,例如:当设备在空闲状态, 并且使用wifi时, 自动下载Apk

JobScheduler典型的使用举例如下:

 JobScheduler scheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
ComponentName jobService = new ComponentName(this, MyJobService.class); //任务Id等于123
JobInfo jobInfo = new JobInfo.Builder(123, jobService)
// 任务最少延迟时间
.setMinimumLatency(5000)
// 任务deadline,当到期没达到指定条件也会开始执行
.setOverrideDeadline(60000)
// 网络条件,网络无需付费时执行
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
// 是否充电
.setRequiresCharging(true)
// 是否在空闲时执行
.setRequiresDeviceIdle(true)
// 设备重启后是否继续执行
.setPersisted(true)
// 设置退避/重试策略
.setBackoffCriteria(3000,JobInfo.BACKOFF_POLICY_LINEAR)
.build();
scheduler.schedule(jobInfo);

官方描述参考:

https://developer.android.google.cn/about/versions/11/behavior-changes-all

官方Demo参考:

https://github.com/googlearchive/android-JobScheduler

五、安全

  • 非 SDK 接口限制

5.1、非 SDK 接口限制

官方从 Android 9(API 级别 28)开始,对应用使用的非 SDK 接口实施了限制。

如果你的APP通过引用非 SDK 接口或尝试使用反射或 JNI 来获取句柄,这些限制就会起作用。官方给出的解释是为了提升用户体验、降低应用崩溃风险

a、非SDK接口检测工具

官方给出了一个检测工具,下载地址:veridex

veridex使用方法:

appcompat.sh --dex-file=apk.apk

b、blacklist、greylist、greylist-max-o、greylist-max-p含义

以上截图中,blacklist、greylist、greylist-max-o、greylist-max-p含义如下:

  • blacklist 黑名单:禁止使用的非SDK接口,运行时直接Crash(因此必须解决)
  • greylist 灰名单:即当前版本仍能使用的非SDK接口,但在下一版本中可能变成被限制的非SDK接口
  • greylist-max-o: 在targetSDK<=O中能使用,但是在targetSDK>=P中被禁止使用的非SDK接口
  • greylist-max-p: 在targetSDK<=P中能使用,但是在targetSDK>=Q中被禁止使用的非SDK接口

非SDK接口限制 官方描述:

https://developer.android.google.cn/about/versions/11/non-sdk-11

========== THE END ==========

Android R 新特性分析及适配指南的更多相关文章

  1. Android O新特性和行为变更总结zz

    https://mp.weixin.qq.com/s/Ezfm-Xaz3fzsaSm0TU5LMw Android O 行为变更https://developer.android.google.cn/ ...

  2. Android N 新特性

    2016年5月19日,谷歌在美国加州的山景城举办了 Google I/O 开发者大会中发布.2016年6月,Android N正式命名为“牛轧糖” 本届I/O开发者大会上,Google重点介绍了And ...

  3. 可能是最早的学习Android N新特性的文章

    可能是最早的学习Android N新特性的文章 Google在今天放出了Android N开发者预览版.Android N支持Nexus6及以上的设备.5太子Nexus5不再得到更新. Android ...

  4. Android 13 新特性及适配指南

    Android 13(API 33)于 2022年8月15日 正式发布(发布时间较往年早了一些),正式版Release源代码也于当日被推送到AOSP Android开源项目. 截止到笔者撰写这篇文章时 ...

  5. 了解与建设有中国特色的Android M&N(Android6.0和7.0新特性分析)

    http://geek.csdn.NET/news/detail/110434 Android N已经发布有段时间,甚至马上都要发布android 7.1,相信不少玩机爱好者已经刷入最新的Androi ...

  6. Android 4.4 新特性分析-15项大改进!

    Google发布了Android 4.4 KitKat,并其同时面世的还有新旗舰Nexus 5.Android 4.4 KitKat有怎样的改进.是否值得升级呢,下面就为大家呈现Android 4.4 ...

  7. 从开发者角度解析 Android N 新特性!

    大清早看到 Google 官方博客发布 Android N 的开发者预览版,立马从床上跳起来开始仔仔细细的读起来. 从开发者角度来看,Android N 的更新并不算大.网上之前流传的一些 Andro ...

  8. iOS6、7、8、9新特性汇总和适配说明

    iOS6新特性 一.关于内存警告 ios6中废除了viewDidUnload,viewWillUnload这两个系统回调,收到内存警告时在didReceiveMemoryWarning中进行相关的处理 ...

  9. iOS -- iOS11新特性,如何适配iOS11

    前言 这几天抽空把WWDC的Session看了一些,总结了一些iOS11新的特性,可能对我们的App有影响,需要我们进行适配.本文作为一个总结. 本文内容包括:集成了搜索的大标题栏.横向选项卡栏.Ma ...

随机推荐

  1. 【非原创】ZOJ - 4062 Plants vs. Zombies【二分】

    题目:戳这里 题意:机器人走过一个花,可以给那个花浇水,给定步数下,问花的最小的最大能量值. 学习博客:戳这里 本人代码: 1 #include <bits/stdc++.h> 2 typ ...

  2. 全局ID生成--雪花算法

    分布式ID常见生成策略: 分布式ID生成策略常见的有如下几种: 数据库自增ID. UUID生成. Redis的原子自增方式. 数据库水平拆分,设置初始值和相同的自增步长. 批量申请自增ID. 雪花算法 ...

  3. TypeScript enum 枚举实现原理

    TypeScript enum 枚举实现原理 反向映射 https://www.typescriptlang.org/docs/handbook/enums.html enum Direction { ...

  4. javascript nested object merge

    javascript nested object merge deep copy Object reference type function namespace(oNamespace, sPacka ...

  5. wifi IP address scanner on macOS

    wifi IP address scanner on macOS Nmap Network Scanning https://nmap.org/book/inst-macosx.html https: ...

  6. js & anti craw & crawler spam

    js & anti craw & crawler spam demo & X-Sign , function(t, e, n) { "use strict" ...

  7. taro 进阶指南

    taro 进阶指南 配置 https://nervjs.github.io/taro/docs/config.html https://nervjs.github.io/taro/docs/confi ...

  8. 宝塔部署Nestjs

    1. 在宝塔上下载pm2 2. 打包你的服务端代码 "npm run build && cp ./package.json ./dist/" 3. 在宝塔文件&qu ...

  9. NGK公链:在规则明确的环境下运行超级节点机制

    首先要跟大家明确的一点是,21个超级节点是投票选举出来的,并不是系统在创立之初就已经确定好了的.那么相信大家也一定很好奇,这21个超级节点是通过什么方式产生? NGK.IO对分布式超级节点使用了一个自 ...

  10. 面试必知:String、StringBuilder、StringBuffer的区别

    你知道String.StringBuilder.Stringbuffer的区别吗?当你创建字符串的时候,有考虑过该使用哪个吗? 别急,这篇文章带你解决这些问题. 可变性 首先,String是字符串,我 ...