携程Android 10适配踩坑指南

2020 年 4 月 16 日

携程Android 10适配踩坑指南

2019 年 9 月 3 日,Google 发布了 Android 10 正式版。Android 10 聚焦移动创新、安全隐私和数字健康三大主题,全面打造最佳用户体验。

背景

目前携程旅行线上最新版本已适配到 Android 10(API =29),由于从 API=26 升级到 API=29,跨度较大,我们提前对相关适配进行了调研,希望其中一些经验能对其他开发者有一定的帮助。

在 Android 10 版本中,官方的改动较大,相应的开发者适配成本还是很高的。基于前期调研,我们主要基于以下几方面进行 Android 10 的适配:

  • Android X
  • 分区存储
  • 设备 ID
  • 明文 HTTP 限制

一、AndroidX

AndroidX 对原始 Android Support 库进行了重大改进,后者现在已不再维护。AndroidX 软件包完全取代了支持库,不仅提供同等的功能,而且提供了新的库。

1.1 什么是 AndroidX

Android 系统在刚刚面世的时候,可能连它的设计者也没有想到它会如此成功。随着 Android 系统版本不断地迭代更新,每个版本中都会加入很多新的 API 进去,但是新增的 API 在老版系统中并不存在,因此这就出现了一个向下兼容的问题。

于是 Android 团队推出了一个鼎鼎大名的 Android Support Library,用于提供向下兼容的功能。比如我们熟知的 support-v4 库,appcompat-v7 库都是属于 Android Support Library 的。4 在这里指的是 Android API 版本号,对应的系统版本是 1.6。support-v4 的意思就是这个库中提供的 API 会向下兼容到 Android 1.6 系统。类似地,appcompat-v7 指的是将库中提供的 API 向下兼容至 API 7,也就是 Android 2.1 系统。

随着时间的推移,Android1.6、2.1 系统早已被淘汰了,现在 Android 官方支持的最低系统版本已经是 4.0.1,对应的 API 版本号是 15。support-v4、appcompat-v7 库也不再支持那么久远的系统了,但是它们的名字却一直保留了下来,虽然它们现在的实际作用已经对不上当初命名的原因了。

Android 团队也意识到这种命名已经非常不合适了,于是对这些 API 的架构进行了一次重新的划分,推出了 AndroidX。因此,AndroidX 本质上其实就是对 Android Support Library 进行的一次升级。

1.2 为什么要升级 AndroidX

  • 版本 28.0.0 是 Android Support 库的最后一个版本。官方将不再发布 android.support 库版本。所有新功能都将在 AndroidX 命名空间中开发。
  • 长远来看。AndroidX 重新设计了包结构,旨在鼓励库的小型化,支持库和架构组件包的名字进行了简化。而且这也是减轻 Android 生态系统碎片化的有效方式。
  • 与 Android Support 库不同,AndroidX 软件包是单独维护和更新的。这些 AndroidX 包使用严格的语义版本控制,从版本 1.0.0 开始,您可以单独更新项目中的 AndroidX 库。

1.3 适配步骤

1.3.1 环境准备

  • AndroidStudio 3.2.0+
  • gradle:gradle-4.6+

另外修改相关 app、library 模块中 build.gradle 的 compileSdkVersion、targetSdkVersion、buildToolsVersion 的配置,都设置为 29,示例如下:

复制代码
android {
compileSdkVersion 29
buildToolsVersion 29.0.2
defaultConfig {
targetSdkVersion 29
}
...
}

1.3.2 修改当前项目的 gradle.properties

复制代码
android.useAndroidX=true
android.enableJetifier=true

其中:

  • android.useAndroidX=true 表示当前项目启用 AndroidX;
  • android.enableJetifier=true 表示将依赖包也迁移到 AndroidX 。如果取值为 false , 表示不迁移依赖包到 AndroidX,但在使用依赖包中的内容时可能会出现问题,如果你的项目中没有使用任何三方依赖,此项可以设置为 false。

1.3.3 修改项目中的 build.gradle 依赖库

复制代码
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'com.android.support:design:28.0.0'
implementation 'com.google.android.material:material:1.0.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'

映射关系:

https://developer.android.com/jetpack/androidx/migrate/artifact-mappings

1.3.4 修改支持库类

将原来 import 的 android.包删除,重新 import 新的 androidx.包;

import android.support.v7.app.AppCompatActivity; →import androidx.appcompat.app.AppCompatActivity;

1.3.5 迁移

官方迁移指南:

https://developer.android.com/jetpack/androidx/migrate#migrate

在 AndroidStudio 3.2 或更高版本(截图中 AndroidStudio 为 3.5 版本)中执行如下操作:菜单 >Refactor > Migrate to AndroidX(如果迁移失败,就需要重复上面 1,2,3,4 步手动去修改迁移)

注意:

  • 使用 AS 迁移工具并不能完全修改完毕,需要手动修改
  • support 包名涉及到资源修改,切记检查资源中的类路径

二、分区存储

2.1 背景介绍

为了更好的保护用户数据并限制设备冗余文件增加,以 Android 10(API 级别 29)及更高版本为目标平台的应用在默认情况下被赋予了对外部存储设备的分区访问权限(即分区存储), 对外部存储文件访问方式重新设计,便于用户更好的管理外部存储文件。

应用只能看到本应用专有的目录(通过 Context.getExternalFilesDir() 访问)以及特定类型的媒体。除非您的应用需要访问存放在应用的专有目录以及 MediaStore 之外的文件,否则最好使用分区存储。

要点:

  • Android Q 文件存储机制修改成了沙盒模式
  • APP 只能访问自己目录下的文件和公共媒体文件
  • Android Q 版本以下机型,还是使用老的文件存储方式
  • Android Q 及以上版本机型,所有应用均需要分区存储, 所以应用需要提前确保支持分区存储

需要注意:在适配 AndroidQ 的时候还要兼容 Q 系统版本以下的,使用 SDK_VERSION 区分

2.2 新特性概览

2.2.1 外部存储

外部存储被分为应用私有目录以及共享目录两个部分:

  • 应用私有目录:存储应用私有数据,外部存储应用私有目录对应 Android/data/packagename,内部存储应用私有目录对应 data/data/packagename;
  • 共享目录:存储其他应用可访问文件, 包含媒体文件、文档文件以及其他文件,对应设备 DCIM、Pictures、Alarms, Music, Notifications,Podcasts, Ringtones、Movies、Download 等目录

1)私有目录

应用私有目录文件访问方式与之前 Android 版本一致,可以通过 File path 获取资源。

2)共享目录

共享目录文件需要通过 MediaStore API 或者 Storage Access Framework 方式访问。

  • MediaStore API 在共享目录指定目录下创建文件或者访问应用自己创建文件,不需要申请存储权限
  • MediaStore API 访问其他应用在共享目录创建的媒体文件 (图片、音频、视频), 需要申请存储权限,未申请存储权限,通过 ContentResolver 查询不到文件 Uri,即使通过其他方式获取到文件 Uri,读取或创建文件会抛出异常;
  • MediaStore API 不能够访问其他应用创建的非媒体文件 (pdf、office、doc、txt 等), 只能够通过 Storage Access Framework 方式访问;

2.3 受影响的变更

2.3.1 图片位置信息

一些图片会包含位置信息,因为位置对于用户属于敏感信息, Android 10 应用在分区存储模式下图片位置信息默认获取不到,应用通过以下两项设置可以获取图片位置信息:

  • 在 manifest 中申请 ACCESS_MEDIA_LOCATION
  • 调用 MediaStore setRequireOriginal(Uri uri) 接口更新图片 Uri

2.3.2 访问数据

MediaStore.Files 应用分区存储模式下,MediaStore.Files 集合只能够获取媒体文件信息 (图片、音频、视频), 获取不到非 media(pdf、office、doc、txt 等) 文件。

2.3.3 File Path 路径访问受影响接口

开启分区存储新特性, Andrioid 10 不能够通过 File Path 路径直接访问共享目录下资源,以下接口通过 File 路径操作文件资源,功能会受到影响,应用需要使用 MediaStore 或者 SAF 方式访问。

类名称受影响的接口
FilecreateNewFile()
delete()
renameTo(File dest)
mkdir()
mkdirs()
FileInputStreamFileInputStream(File file)
FileInputStream(String name)
FileOutputStreamFileOutputStream(String name)
FileOutputStream(String name, boolean append)
FileOutputStream(File file)
FileOutputStream(File file, boolean append)
BitmapFactorydecodeFile(String pathName)
decodeFile(String pathName, Options opts)

2.3.4 存储特性 Android 版本差异概览

存储位置路径版本存储权限
内部存储data/data/packagename所有getFilesDir()、getCacheDir()
外部存储私有目录Android/data/packagename4.4 以上getExternalFilesDir()、getExternalCacheDir()、SAF
共享目录DCIM、Pictures、Alarms, Music, Notifications,Podcasts, Ringtones、Movies、Download<10Environment.getExternalStorageDirectory()
SAF
>=10访问其他应用 media 文件 -->MediaStore API
访问其他应用创建的非 media 文件 --> SAF
访问自己应用创建的文件 -->MediaStore API
SAF

2.4 兼容模式

应用未完成外部存储适配工作,可以临时以兼容模式运行, 兼容模式下应用申请存储权限,即可拥有外部存储完整目录访问权限,通过 Android10 之前文件访问方式运行,以下两种方法设置应用以兼容模式运行。

2.4.1 AndroidManifest 中申明

tagretSDK 大于等于 Android 10(API level 29), 在 manifest 中设置 requestLegacyExternalStorage 属性为 true。

复制代码
<manifest ...>
...
<application android:requestLegacyExternalStorage="true" ... >
...
</manifest>

2.4.2、判断兼容模式接口

复制代码
// 返回值
//true : 应用以兼容模式运行
//false:应用以分区存储特性运行
Environment.isExternalStorageLegacy();

备注:应用已完成存储适配工作且已打开分区存储开关,如果当前应用以兼容模式运行,覆盖安装后应用仍然会以兼容模式运行,卸载重新安装应用才会以分区存储模式运行

2.5 适配方案

2.5.1 方案概览

分区存储适配包含文件迁移以及文件访问兼容性适配两个部分:

1)文件迁移

文件迁移是将应用共享目录文件迁移到应用私有目录或者 Android10 要求的 media 集合目录。

  • 针对只有应用自己访问并且应用卸载后允许删除的文件,需要迁移文件到应用私有目录文件,可以通过 File path 方式访问文件资源,降低适配成本。
  • 允许其他应用访问,并且应用卸载后不允许删除的文件,文件需要存储在共享目录,应用可以选择是否进行目录整改,将文件迁移到 Android10 要求的 media 集合目录。

2)文件访问兼容性

共享目录文件不能够通过 File path 方式读取,需要使用 MediaStore API 或者 Storage Access Framework 框架进行访问。

2.5.2 适配指导

AndroidQ 中使用 ContentResolver 进行文件的增删改查。

1)获取 (创建) 私有目录下的文件夹

复制代码
// 在自身目录下创建 apk 文件夹
File apkFile = context.getExternalFilesDir("apk");

2)创建私有目录文件

生成需要下载的路径,通过输入输出流读取写入

复制代码
String apkFilePath = context.getExternalFilesDir("apk").getAbsolutePath();
File newFile = new File(apkFilePath + File.separator + "demo.apk");
OutputStream os = null;
try {
os = new FileOutputStream(newFile);
if (os != null) {
os.write("file is created".getBytes(StandardCharsets.UTF_8));
os.flush();
}
} catch (IOException e) {
} finally {
try {
if (os != null) {
os.close();
}catch (IOException e1) {
}
}

3)创建共享目录文件夹

复制代码
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ContentResolver resolver = context.getContentResolver();
ContentValues values = new ContentValues();
values.put(MediaStore.Downloads.DISPLAY_NAME, fileName);
values.put(MediaStore.Downloads.DESCRIPTION, fileName);
// 设置文件类型
values.put(MediaStore.Downloads.MIME_TYPE, "application/vnd.android.package-archive");
// 注意 MediaStore.Downloads.RELATIVE_PATH 需要 targetVersion=29,
// 故该方法只可在 Android10 的手机上执行
values.put(MediaStore.Downloads.RELATIVE_PATH, "Download" + File.separator + "apk");
Uri external = MediaStore.Downloads.EXTERNAL_CONTENT_URI;
Uri insertUri = resolver.insert(external, values);
return insertUri;
}else{
...
}

4)在共享目录指定文件夹下创建文件

主要是在公共目录下创建文件或文件夹拿到本地路径 uri,不同的 Uri,可以保存到不同的公共目录中。接下来使用输入输出流就可以写入文件。

重点:AndroidQ 中不支持 file:// 类型访问文件,只能通过 uri 方式访问。

复制代码
/**
* 创建图片地址 uri, 用于保存拍照后的照片 Android 10 以后使用这种方法
*/
private Uri createImageUri() {
String status = Environment.getExternalStorageState();
// 判断是否有 SD 卡, 优先使用 SD 卡存储, 当没有 SD 卡时使用手机存储
if (status.equals(Environment.MEDIA_MOUNTED)) {
return getContext().getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, new ContentValues());
} else {
return getContext().getContentResolver().insert(MediaStore.Images.Media.INTERNAL_CONTENT_URI, new ContentValues());
}
}

5)通过 MediaStore API 读取公共目录下的文件

复制代码
if (cursor != null && cursor.moveToFirst()) {
do {
...
int _id = cursor.getInt(cursor.getColumnIndex(MediaStore.Images.Media._ID));
Uri imageUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, _id);
...
} while (!cursor.isLast() && cursor.moveToNext());
} else {
...
}
复制代码
// 通过 uri 获取 bitmap
public Bitmap getBitmapFromUri(Context context, Uri uri) {
ParcelFileDescriptor parcelFileDescriptor = null;
FileDescriptor fileDescriptor = null;
Bitmap bitmap = null;
try {
parcelFileDescriptor = context.getContentResolver().openFileDescriptor(uri, "r");
if (parcelFileDescriptor != null && parcelFileDescriptor.getFileDescriptor() != null) {
fileDescriptor = parcelFileDescriptor.getFileDescriptor();
// 转换 uri 为 bitmap 类型
bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor);
}
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
if (parcelFileDescriptor != null) {
parcelFileDescriptor.close();
}catch (IOException e) {
}
}
return bitmap;
}

6)使用 MediaStore 删除文件

复制代码
context.getContentResolver().delete(fileUri, null, null);

三、设备 ID

从 Android 10 开始已经无法完全标识一个设备,曾经用 mac 地址、IMEI 等设备信息标识设备的方法,从 Android 10 开始统统失效。而且无论你的 APP 是否适配过 Android 10。

3.1 IMEI 等设备信息

从 Android10 开始普通应用不再允许请求权限 android.permission.READ_PHONE_STATE。而且,无论你的 App 是否适配过 Android Q(既 targetSdkVersion 是否大于等于 29),均无法再获取到设备 IMEI 等设备信息。

受影响的 API:

复制代码
Build.getSerial();
TelephonyManager.getImei();
TelephonyManager.getMeid()
TelephonyManager.getDeviceId();
TelephonyManager.getSubscriberId();
TelephonyManager.getSimSerialNumber();
  • targetSdkVersion<29 的应用,其在获取设备 ID 时,会直接返回 null
  • targetSdkVersion>=29 的应用,其在获取设备 ID 时,会直接抛出异常 SecurityException

如果您的 App 希望在 Android 10 以下的设备中仍然获取设备 IMEI 等信息,可按以下方式进行适配:

复制代码
<uses-permission android:name="android.permission.READ_PHONE_STATE"
android:maxSdkVersion="28"/>

3.2 Mac 地址随机分配

从 Android10 开始,默认情况下,在搭载 Android 10 或更高版本的设备上,系统会传输随机分配的 MAC 地址。(即从 Android 10 开始,普通应用已经无法获取设备的真正 mac 地址,标识设备已经无法使用 mac 地址)

3.3 如何标识设备唯一性

3.3.1 Google 解决方案:

如果您的应用有追踪非登录用户的需求,可用 ANDROID_ID 来标识设备。

  • ANDROID_ID 生成规则:签名 + 设备信息 + 设备用户
  • ANDROID_ID 重置规则:设备恢复出厂设置时,ANDROID_ID 将被重置
复制代码
String androidId = Settings.Secure.getString(this.getContentResolver(), Settings.Secure.ANDROID_ID);

3.3.2 信通院统一 SDK(OAID)

统一标识依据电信终端产业协会 (TAF)、移动安全联盟 (MSA) 联合推 出的团体标准《移动智能终端补充设备标识规范》开发,移动智能终端补充设备标识体系统一调用 SDK 集成设备厂商提供的接口,并获得主流设备厂商的授权。

移动安全联盟 (MSA) 组织中国信息通信研究院 (以下简称“中国信通院”) 与终端生产企业、互联网企业共同研究制定了“移动智能终端补充设备标识体系”,定义了移动智能终端补充设备标识体系的体系架构、功能要求、接口要求以及安全要求,使设备生产企业统一开发接口,为移动应用开发者提供统一调用方式,方便移动应用接入,降低维护成本。

1)SDK 获取

MSA 统一 SDK 下载地址:

移动安全联盟官网, http://www.msa-alliance.cn/

2)接入方式

  • 解压 miit_mdid_sdk_v1.0.13.rar,
  • 把 miit_mdid_1.0.13.aar 拷贝到项目中,并设置依赖。
  • 将 supplierconfig.json 拷贝到项目 assets 目录下,并修改里边对应 内容,特别是需要设置 appid 的部分。需要设置 appid 的部分需要去对应的厂 商的应用商店里注册自己的 app。
复制代码
{
"supplier":{
"xiaomi":{
"appid":"***"
},
"huawei":{
"appid":"***"
}
...
}
}
  • 在初始化方法中调用 JLibrary.InitEntry
复制代码
try {
JLibrary.InitEntry(FoundationContextHolder.getContext());
} catch (Throwable e) {
}
  • 实例化 MSA SDK
复制代码
public static void initMSASDK(Context context){
int code = 0;
try {
code = MdidSdkHelper.InitSdk(context,true,listener);
if (code == ErrorCode.INIT_ERROR_MANUFACTURER_NOSUPPORT){//1008611, 不支持的厂商
}else if (code == ErrorCode.INIT_ERROR_DEVICE_NOSUPPORT){//1008612, 不支持的设备
}else if (code == ErrorCode.INIT_ERROR_LOAD_CONFIGFILE){//1008613, 加载配置文件失败
}else if (code == ErrorCode.INIT_ERROR_RESULT_DELAY){//1008614, 信息将会延迟返回,获取数据可能在异步线程,取决于设备
}else if (code == ErrorCode.INIT_HELPER_CALL_ERROR){//1008615, 反射调用失败
}
//code 可记录异常供分析
}catch (Throwable throwable){
}
}
static IIdentifierListener listener = new IIdentifierListener() {
@Override
public void OnSupport(boolean support, IdSupplier idSupplier) {
try{
isSupport = support;
if (null != idSupplier && isSupport){
// 是否支持补充设备标识符获取
oaid = idSupplier.getOAID();
aaid = idSupplier.getAAID();
vaid = idSupplier.getVAID();
}else {
...
}
}catch (Exception e){
}
}
};
  • 通过以上方法获取到 OAID 等设备标识之后,即可作为唯一标识使用。

四、明文 HTTP 限制

当 SDK 版本大于 API 28 时,默认限制了 HTTP 请求,并出现相关日志“ java.net .UnknownServiceException: CLEARTEXT communication to xxx not permitted by network security policy“。

该问题有两种解决方案:

1)在 AndroidManifest.xml 中 Application 节点添加如下代码

复制代码
<application android:usesCleartextTraffic="true">

2)在 res 目录新建 xml 目录,已建的跳过 在 xml 目录新建一个 xml 文件 network_security_config.xml,然后在 AndroidManifest.xml 中 Application 添加如下节点代码。

复制代码
android:networkSecurityConfig="@xml/network_config"

network_config.xml(命名随机)

复制代码
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>

五、展望

2020 年 2 月 20 号,Google 提前发布了 Android 11 预览版,通过 5G、折叠屏、内置机器学习等新技术,照亮了移动设备的未来。Android 11 依然致力于让用户畅享最新科技,并始终确保将安全和隐私放在首位,帮助用户管理敏感数据和文件的访问权限。此外还对平台的关键区域做出了强化,以保持操作系统的弹性和安全性。

对于像 Android 这样的开放性 OS 来说,占有的市场份额越大,整个 Android 生态系统的发展会越好。随着 Android 对于碎片化的整理、用户隐私和安全性的重视、5G 和机器学习等新技术的引入,已逐步抓住快速增长的中产阶级用户,未来的市场份额增长量将是不可预估的。

参考文档:

1、AndroidX 概览

https://developer.android.google.cn/jetpack/androidx

2、Android 10 介绍

https://developer.android.com/about/versions/10

3、Android 11 预览版介绍

https://developer.android.com/preview

4、Android Q Adaptation Guide

https://chinesefoodstudio.com/index.php/2019/11/21/android-q-adaptation-guide/

5、Android 10 分区存储介绍及百度 APP 适配实践

https://segmentfault.com/a/1190000021760036

作者介绍

曙光,携程资深软件工程师,负责市场营销相关研发及管理工作。

本文转载自公众号携程技术(ID:ctriptech)。

原文链接

https://mp.weixin.qq.com/s?__biz=MjM5MDI3MjA5MQ==&mid=2697269503&idx=2&sn=f5505724dcee64ebd9904ee16a2bfedb&chksm=8376efcbb40166ddf0f301003b0c05b89f110957fa0872c8ba741cb49b61c404ce849c769978&scene=27#wechat_redirect

2020 年 4 月 16 日 10:10 1565

评论

发布
暂无评论
发现更多内容

netty案例,netty4.1中级拓展篇十《Netty接收发送多种协议消息类型的通信处理方案》

小傅哥

Java Netty 小傅哥

netty案例,netty4.1基础入门篇一《嗨!NettyServer》

小傅哥

Java Netty

netty案例,netty4.1基础入门篇三《NettyServer字符串解码器》

小傅哥

Java Netty 小傅哥

netty案例,netty4.1中级拓展篇一《Netty与SpringBoot整合》

小傅哥

Java Netty

netty案例,netty4.1中级拓展篇五《基于Netty搭建WebSocket,模仿微信聊天页面》

小傅哥

Java Netty 小傅哥

netty案例,netty4.1中级拓展篇八《Netty心跳服务与断线重连》

小傅哥

Netty 小傅哥

理论 | 三天两夜,万字长文,吃透TCP/IP

简爱W

Java TCP

netty案例,netty4.1基础入门篇四《NettyServer收发数据》

小傅哥

Java Netty 小傅哥

netty案例,netty4.1中级拓展篇二《Netty使用Protobuf传输数据》

小傅哥

Java Netty 小傅哥

netty案例,netty4.1中级拓展篇四《Netty传输文件、分片发送、断点续传》

小傅哥

Netty 小傅哥

netty案例,netty4.1中级拓展篇七《Netty请求响应同步通信》

小傅哥

Java Netty 小傅哥

Stream 流

HeGuang

JAVA stream

netty案例,netty4.1基础入门篇二《NettyServer接收数据》

小傅哥

Java Netty 小傅哥

netty案例,netty4.1基础入门篇十《关于ChannelOutboundHandlerAdapter简单使用》

小傅哥

Netty 小傅哥

netty案例,netty4.1中级拓展篇三《Netty传输Java对象》

小傅哥

Java Netty 小傅哥

netty案例,netty4.1基础入门篇十二《简单实现一个基于Netty搭建的Http服务》

小傅哥

Java Netty

netty案例,netty4.1中级拓展篇九《Netty集群部署实现跨服务端通信的落地方案》

小傅哥

Java Netty 小傅哥

没有亮点的简历,要用详历来弥补

escray

面试 学习笔记 简历 面试现场

实战 | Vue + Element UI 页面创建

简爱W

Java 架构师

一把年龄,技术一般,怎么去面试

escray

面试 学习笔记 面试现场

netty案例,netty4.1基础入门篇六《NettyServer群发消息》

小傅哥

Java Netty 小傅哥

netty案例,netty4.1基础入门篇九《自定义编码解码器,处理半包、粘包数据》

小傅哥

Java Netty

netty案例,netty4.1基础入门篇七《嗨!NettyClient》

小傅哥

Netty 小傅哥

netty案例,netty4.1基础入门篇八《NettyClient半包粘包处理、编码解码处理、收发数据方式》

小傅哥

Netty 小傅哥

netty案例,netty4.1基础入门篇十一《netty udp通信方式案例Demo》

小傅哥

Java Netty

netty案例,netty4.1中级拓展篇六《SpringBoot+Netty+Elasticsearch收集日志信息数据存储》

小傅哥

Java Netty

程序开发中的持续集成、持续交付、持续部署

石云升

持续集成 持续交付 持续部署 自动化部署

LeetCode题解:11. 盛最多水的容器,for循环双指针,JavaScript,详细注释

Lee Chen

LeetCode 前端进阶训练营

世界很大,我想去看看

escray

面试 学习笔记 面试现场

netty案例,netty4.1基础入门篇零《初入JavaIO之门BIO、NIO、AIO实战练习》

小傅哥

Java Netty 小傅哥

netty案例,netty4.1基础入门篇五《NettyServer字符串编码器》

小傅哥

Java Netty

携程Android 10适配踩坑指南-InfoQ