速来报名!AICon北京站鸿蒙专场~ 了解详情
写点什么

携程 Android 10 适配踩坑指南

  • 2020-04-16
  • 本文字数:8818 字

    阅读完需:约 29 分钟

携程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=trueandroid.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获取bitmappublic 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-04-16 10:103943

评论

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

Ubuntu 22报错:PAM unable to dlopen(pam_tally2.so)

百度搜索:蓝易云

WingPro for Mac(强大的Python开发工具)

Mac相关知识分享

浏览器缓存清理工具Cookie for Mac

Mac相关知识分享

mcgs笔记 菜单栏中工具下的功能(上)

万里无云万里天

HMI 工厂运维 mcgs

Ubuntu22.04如何调试ROS2_humble的源代码

百度搜索:蓝易云

微软远程连接工具Microsoft Remote Desktop Beta for Mac

Mac相关知识分享

mcgs笔记 触摸校准程序

万里无云万里天

HMI 工厂运维 mcgs

mcgs笔记 系统变量

万里无云万里天

HMI 工厂运维 mcgs

如何进行一场高效的会议?

Anliven

团队管理 会议 效能

Java开发性能瓶颈TOP榜,你中招了吗?

巧手打字通

Java 性能优化 后端 经验总结 性能瓶颈

《让手机秒变超级电脑!ToDesk云电脑、易腾云、青椒云移动端评测》

鸽芷咕

云电脑 ToDesk云电脑

DockerCompose部署es和kibana

百度搜索:蓝易云

远程桌面连接工具Microsoft Remote Desktop for Mac

Mac相关知识分享

桌面连接工具

试验大模型的图像识别能力

AIGC.TWang

图像识别 多模态 大模型 AIGC AI大语言模型

mcgs笔记 有线网络 上传工程

万里无云万里天

HMI 工厂运维 mcgs

淘宝API接口深度解析:利用关键词搜索商品列表数据

代码忍者

API 接口 pinduoduo API

PIRF-410-Opinion-Is it funny?

EchoZhou

English

mcgs笔记 程序保护 设置各种密码

万里无云万里天

HMI 工厂运维 mcgs

mcgs笔记 动画组态 分段填充颜色

万里无云万里天

HMI 工厂运维 mcgs

mcgs笔记 优盘包制作

万里无云万里天

HMI 工厂运维 mcgs

AI城市跃迁之路,一城一云一模型提供强劲动力

脑极体

AI

基于波特图的控制系统设计算法

梦笔生花

AIGCC

解锁电商新视野:京东商品详情API——您的精准商品信息探索利器

代码忍者

API 测试 pinduoduo API

mcgs笔记 系统参数设置界面

万里无云万里天

HMI 工厂运维 mcgs

CheckBook Pro for mac优秀个人理财管理工具

Mac相关知识分享

mcgs笔记 菜单栏中工具下的功能(下)

万里无云万里天

HMI 工厂运维 mcgs

mcgs笔记 构件联动 滑动输入与百分比填充

万里无云万里天

HMI 工厂运维 mcgs

国企数字化转型必备!10款文档管理工具深度剖析

爱吃小舅的鱼

文档管理 文档管理工具

WSL中ubuntu安装mysql数据库

百度搜索:蓝易云

ubuntu22.04开机自启动Eureka服务

百度搜索:蓝易云

ChatGPT流程深度分析:一篇文章带你掌握大模型整体流程(专家篇)

肖哥弹架构

Java 大模型 ChatGPT LLM

携程Android 10适配踩坑指南_安全_曙光_InfoQ精选文章