四大组件 - ContentProvider

参考:Android 这 13 道 ContentProvider 面试题,你都会了吗?

参考:《Android 开发艺术探索》 第 9.5 节(ContentProvider 的工作过程)

参考:内容提供者程序

参考:<provider>>

1. ContentProvider 概述

ContentProvider 是一种内容共享型组件,实现了不同 App 进程之间的数据共享。

Messenger 一样,基于 ContentProvider 的进程间通信的底层实现同样也是 Binder

ContentProvider 所在的进程启动时,ContentProvider 组件会同时启动并被发布到 AMS 进程中。

1.1 ContentProvider 组件的优势

相比于直接访问数据库,ContentProvider 具有如下优势:

  1. ContentProvider 对外提供统一的 CRUD 接口,使得外界可以按照统一的方式访问不同来源提供的数据,而不用关心这些数据是怎么来的。(数据可以来源于数据库、xml 文件、网络请求等)

  2. ContentProvider 提供一种可跨进程的数据共享方式。

  3. ContentProvider 提供了数据更新时的通知机制。即:当 ContentProvider 所在进程中的数据更新时,访问这些数据的其他进程可以通过 ContentProvider 收到相关的通知。

1.2 ContentProvider & ContentResolver

ContentProvider 组件主要是用于进程间的数据共享的。其中:

  1. 提供数据的 App 进程中通过 ContentProvider 向其他 App 进程提供访问数据的 API 方法;

  2. 访问数据的 App 进程中通过 ContentResolver 访问提供数据的 App 进程中的 ContentProvider

1.3 ContentProvider 对外提供的数据形式 & 内部的数据存储方式

1.3.1 ContentProvider 以表格的形式对外提供数据

ContentProvider 主要以表格的形式来组织数据,并且可以包含多个表,这点和数据库很类似。

ContentProvider 可以对外提供文件数据,通过将文件的句柄保存在表格中提供给外界。从而让外界可以根据查询到的文件句柄来访问 ContentProvider 所提供的文件。

Android 系统所提供的 MediaStore 功能就是包含文件类型数据的 ContentProvider,详细实现可以参考 MediaStore

1.3.2 ContentProvider 对内部的数据存储方式没有任何要求

虽然 ContentProvider 对外提供的数据看起来像是来自数据库中的数据,但是 ContentProvider 对内部的数据存储方式没有任何要求:我们既可以使用 SQLite 数据库;也可以使用普通的文件;甚至可以采用内存中的一个对象来进行数据的存储。

也就是说,ContentProvider 对外提供的数据,可以是数据库数据、文件数据、内存数据等。

2. ContentProvider 的使用方式

2.1 自定义 ContentProvider 的子类

/* ContentProvider.java */
/*onCreate 代表 ContentProvider 的创建,一般来说我们需要在 onCreate 中做一些初始化工作,如获取数据库实例。注意:1. onCreate 方法在主线程中执行,不允许执行耗时操作,否则会导致 App 进程的启动时间延长。2. ContentProvider 的 onCreate 方法的调用时机先于 Application 的 onCreate 方法。
*/
public abstract boolean onCreate();// 返回一个 Uri 请求所对应的 MIME 类型(媒体类型)。比如:图片("image/png")、视频("video/mp4")等。
// 如果 ContentProvider 提供的数据不需要配置 MIME 类型,那么可以返回 null 或者 "*/*" 
public abstract String getType(Uri uri);public abstract Uri insert(Uri uri, ContentValues values);public abstract int delete(Uri uri, String selection, String[] selectionArgs);public abstract int update(Uri uri, ContentValues values, String selection, String[] selectionArgs);public abstract Cursor query(Uri uri, String[] projection,String selection, String[] selectionArgs,String sortOrder);

自定义 ContentProvider 子类时需要重写如上的抽象方法,其中 insert/delete/update/query 方法对应于 CRUD 操作,即实现对数据表的增删改查功能。

2.2 注册 ContentProvider 子类 & 相关属性解读

<provider android:authorities="list"android:directBootAware="true|false"android:enabled="true|false"android:exported="true|false"android:grantUriPermissions="true|false"android:icon="drawable resource"android:initOrder="integer"android:label="string resource"android:multiprocess="true|false"android:name="string"android:permission="string"android:process="string"android:readPermission="string"android:syncable="true|false"android:writePermission="string"><grant-uri-permission android:path="string"android:pathPattern="string"android:pathPrefix="string" /></provider>

其中:

  1. <provider>

    包含于 <application> 标签中,用来注册自定义的 ContentProvider。
    
  2. android:authorities

    是 ContentProvider 的唯一标识,提供给外界用来访问当前注册的 ContentProvider。
    建议命名时加上包名前缀。
    当声明了多个唯一标识时,用分号 ";" 分隔开
    
  3. android:directBootAware

    ContentProvider 是否可感知直接启动(direct-boot);即,它是否可以在用户解锁设备之前运行。
    
  4. android:enabled

    系统是否可以实例化 ContentProvider。true 可以;false 不可以。默认 true。
    
  5. android:permission

    其他 App 需要申请该属性指定的权限后才能读写 ContentProvider 提供的数据
    
  6. android:readPermission

    其他 App 需要申请该属性指定的权限后才能读 ContentProvider 提供的数据
    当 permission 和 readPermission 同时存在时,读权限取决于 readPermission;写权限取决于 permission
    
  7. android:writePermission

    其他 App 需要申请该属性指定的权限后才能写 ContentProvider 提供的数据
    当 permission 和 writePermission 同时存在时,写权限取决于 writePermission;读权限取决于 permission
    
  8. android:exported

    true 表示允许其他 App 在申请了 permission/readPermission/writePermission 指定权限的情况下访问 ContentProviderfalse 表示不允许其他 App 访问 ContentProvider。只能在如下情况下访问 ContentProvider:1. 同一个 App 进程中;2. 其他具有相同 userId 的进程中;3. 注册 ContentProvider 时设置了属性 android:grantUriPermissions = true,使得其他 App 进程可以获取一次性访问 ContentProvider 的临时权限。
    
  9. android:grantUriPermissions

    true 表示允许其他 App 获取一次性访问此 ContentProvider 所提供的所有数据的临时权限。所谓临时权限,就如打开刚安装的 App 时,会弹框向用户询问是否允许某个权限,并为用户提供了几种选项:1. 始终允许;2. 使用期间允许;3. 仅本次使用时允许;3. 禁止。(不同手机可能存在区别)而临时权限就相当于选择了 "2. 仅本次使用时允许",即在本次访问 ContentProvider 之后,就不再拥有权限了,当下次再访问时,仍然会询问用户是否允许权限。即:临时权限就是允许用户一次性地访问。false 不允许其他 App  获取一次性访问此 ContentProvider 所提供的所有数据的临时权限。(默认值)但是,此情况下,我们还可以通过 <provider> 下的子标签 <grant-uri-permission> 声明指定路径下的数据是可以让其他 App 在临时权限下一次性访问的。
    
  10. <grant-uri-permission>

    在 android:grantUriPermissions = false 的情况下,
    可以通过此标签来声明指定路径下的某些或某一数据仍然可以让其他 App 在临时权限下一次性访问的。
    此标签下的三个属性都是用来指定数据的路径的,但一个 <grant-uri-permission> 标签只能指定一个路径,
    也就是说,一个标签中只能使用以下三个属性中的一个:
    1. android:path:某一数据的完整路径
    2. android:pathPrefix:某些数据的公共路径
    3. pathPattern:包含通配符的完整路径
    

2.3 其他进程中通过 ContentResolver 来访问 ContentProvider

/* Context.java */
public abstract ContentResolver getContentResolver();

ContentResolver 提供的用于访问 ContentProvider 的方法,与 ContentProvider 子类重写的方法基本一样。事实上,ContentResolver 内部就是通过基于 binder 机制的跨进程通信来调用 ContentProvider 对应的方法的。

/* ContentResolver.java */
String getType(Uri url)Uri insert(Uri url, ContentValues values)int delete(Uri url, String where, String[] selectionArgs)int update(Uri uri, ContentValues values, String where, String[] selectionArgs)Cursor query( // query(uri, projection, selection, selectionArgs, sortOrder, null);Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder, CancellationSignal cancellationSignal) {Bundle queryArgs = createSqlQueryBundle(selection, selectionArgs, sortOrder);return query(uri, projection, queryArgs, cancellationSignal);
}Cursor query(Uri uri, String[] projection, Bundle queryArgs, CancellationSignal cancellationSignal)

2.4 ContentProvider 的数据更新通知机制

2.4.1 在其他 App 中通过 ContentResolver 监听数据的更新
/* ContentResolver.java */
/*当参数 Uri 标识的 ContentProvider 中的与 Uri 的 path 部分匹配的数据更新时,参数 observer 的 onChange 方法就会回调。参数 notifyForDescendants 为 false 表示监听与 Uri 的 path 路径完全匹配的数据的更新,或者与 path 路径的父路径匹配的数据的更新。参数 notifyForDescendants 为 true 表示还会监听与 Uri 的 path 下的子路径匹配的数据的更新。
*/
void registerContentObserver(Uri uri, boolean notifyForDescendants, ContentObserver observer)void unregisterContentObserver(ContentObserver observer)

ContentProvider 中的数据发生变更时(如调用了 ContentProviderinsert/delete/update),需要在 ContentProvider 中调用 ContentResolvernotifyChange 方法。于是,其他 App 中通过 ContentResolverregisterContentObserver 方法注册的 ContentObserver 才会收到通知(即才会回调 onChange 方法)。

/* ContentObserver.java */
/*回调方法 onChange 会在 Handler 线程中执行,如果传入 null,不指定 Handler,那么回调方法 onChange 会在 Binder 线程中执行。
*/
public ContentObserver(Handler handler)/*当在 ContentProvider 中调用 ContentResolver.nofityChange(Uri, null) 方法后,其他 App 中的注册了的对 Uri 进行监听的 ContentObserver 的 onChange 方法就会回调。ContentObserver 提供了如下三个重载的 onChange 方法:1. 首先回调的是 onChange(selfChange, uri, userId) 2. 在 onChange(selfChange, uri, userId) 中只是调用了 onChange(selfChange, uri) 3. 在 onChange(selfChange, uri) 中只是调用了 onChange(selfChange) 需要用到哪些参数,就重写带哪些参数的 onChange 方法。
*/public void onChange(boolean selfChange, Uri uri, int userId)public void onChange(boolean selfChange, Uri uri)public void onChange(boolean selfChange)
2.4.2 在 ContentProvider 中通过 ContentResolver 发送数据更新的通知

在调用 ContentProviderupdateinsertdelete 方法后,一般会引起数据源的改变。
此时,可以在 ContentProvider 中调用 ContentResolvernotifyChange 方法向其他 App 进程发出数据更新的通知。

可以通过 ContentProvider 组件中的上下文 Context 对象获取 ContentResolver 实例。

即在 ContentProvider 中调用 getContext().getContentResolver() 方法获取 ContentResolver 实例。

/* ContentResolver.java */
/*此方法一般在 ContentProvider 中的 insert、update、delete 方法内操作完数据后调用,用于向其他 App 进程发出数据更新的通知。参数 observer 一般传 null,表示其他 App 进程中只要调用 ContentResolver.registerContentObserver 方法,注册了的监听此 Uri 的 ContentObserver 都会收到通知,即都会回调 onChange 方法。
*/
public void notifyChange(Uri uri, ContentObserver observer)

3. ContentProviderUri 结构 & 通过 UriMatcher 管理多个 path

3.1 Uri 结构 & 操作 UriAPI 方法

通常的 Uri 的结构如下所示:

在这里插入图片描述

对于唯一标识 ContentProviderUri,由上图中的三部分组成:

  1. scheme

    ContentProvider 的 Uri 的 scheme 固定为 content。即:ContentProvider 的 Uri 的开头部分固定为 "content://"
    
  2. authority

    ContentProvider 的 Uri 的 authority 在注册 ContentProvider 时通过属性 android:authorities 声明
    
  3. path

    ContentProvider 的 Uri 的 path 部分,用来指定所要访问的数据具体是哪些数据。path 部分可以指定访问一张表中的所有数据,如访问 user 表的 Uri 为:"content://authority/user" path 部分也可以指定访问一张表中的一条数据,如访问 user 表中 id 为 1 的那条数据的 Uri 为:"content://authority/user/1" 总之,path 部分是完全自定义的,从 ContentResolver 传过来的 Uri 中将 path 解析出来后,
    与我们在 ContentProvider 中已自定义好的 path 常量进行匹配,即可知道外界需要访问的是什么数据。
    

Uri.java 提供了如下的 API 来对 Uri 进行操作:

/* Uri.java */
/*将符合 Uri 结构的字符串转换成 Uri 对象
*/
public static Uri parse(String uriString)/*返回表示一个文件的 Uri 对象
*/
public static Uri fromFile(File file)/* 获取 Uri 中的 scheme 部分
*/
public abstract String getScheme();/* 获取 Uri 中的 authority 部分
*/
public abstract String getAuthority();/* 获取 Uri 中的 path 部分
*/
public abstract String getPath();

3.2 通过 UriMatcher 管理多个 path

UriMatcher 的作用就是对 Uri 中的 path 部分进行标识,用标识码(code)来表示不同的 path

使用 UriMatcher 可以简化对 Uri 的解析,直接将 Uri 传给 UriMatcher 就能获取到对应不同 path 的标识码(code)。

于是,根据自定义的标识码(code)在 ContentProvider 内部查询对应的数据即可。

UriMatcher.java 提供的 API 方法如下:

/* UriMatcher.java */
/*传入 UriMatcher.NO_MATCH 即可。(NO_MATCH = -1)参数 code 表示当调用 match(Uri) 方法时,如果 Uri 中不存在 authority 和 path 部分,则返回此 code
*/
public UriMatcher(int code)/*将参数 authority 和 path 注册到 UriMatcher 对象中,并用参数 code 标记这条注册记录。当调用 match(Uri) 方法时,如果传入的 Uri 的 authority 和 path 与已注册的 authority 和 path 匹配,则返回匹配的注册记录的标记 code如果传入的 Uri 的 authority 和 path 不与任何一条注册记录匹配,则返回 -1(即返回 NO_MATCH)
*/
public void addURI(String authority, String path, int code)public int match(Uri uri)

4. ContentProvider 的生命周期方法:onCreate

ContentProvider 只有一个 onCreate() 生命周期方法。

ContentProvider.onCreate 方法会在 ContentProvider 所在的进程启动时调用一次。且调用时机优先于 Application.onCreate 方法。

注意:ContentProvideronCreate 方法在整个进程的生命周期中只会被回调一次。

5. ContentProvider 的工作线程

5.1 onCreate 方法在主线程中执行

ContentProvideronCreate 在主线程中执行,且优先于 ApplicationonCreate 方法执行,所以在 ContentProvideronCreate 方法中不能做耗时操作,否则会使 App 的启动变慢。

5.2 query/update/insert/delete/getTypeBinder 线程中执行

由于 query/update/insert/delete/getType 是在 Binder 线程中执行的,所以调用这些方法并不会阻塞 ContentProvider 所在进程的主线程。

但是,如果其他 App 进程中通过 ContentResolverquery/update/insert/delete/getType 方法访问 ContentProvider 时,是在主线程中执行的,那么其他 App 进程的主线程就可能会阻塞。

因此,其他 App 进程中通过 ContentResolver 访问 ContentProvider 时仍然需要在子线程中进行访问。

也就是说,使用 ContentResolver 发起的 query/update/insert/delete/getType 这些 binder 调用,是会阻塞线程的。

5.3 ContentProvider 的线程安全问题

queryupdateinsertdelete 这些方法是在 Binder 线程池中提供的 Binder 线程中执行的,因此会存在多线程并发访问的情况,所以这些方法内部需要做好线程同步。

ContentProvider 中的数据都保存在一个 SQLiteDatabase 中时,因为 SQLiteDatabase 内部对数据库的操作是有同步处理的,所以可以正确应对多线程的情况。

但是如果通过多个 SQLiteDatabase 对象来操作数据库就无法保证线程同步,因为 SQLiteDatabase 对象之间无法进行线程同步。

如果 ContentProvider 中的数据是保存在内存中的,如保存在同一个 List 容器中,那么这种情况下对 List 容器的遍历、插入、删除操作就需要进行线程同步,否则就会引起多线程并发问题。

6. 其他 API

6.1 ContentValues

6.2 Cursor

7. 示例

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/661814.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Nucleosome, Recombinant Human, H2BK120ub1 dNuc, Biotinylated

EpiCypher&#xff08;国内授权代理商欣博盛生物&#xff09;是一家为表观遗传学和染色质生物学研究提供高质量试剂和工具的专业制造商。EpiCypher生产的在E. coli中表达的重组人单核小体(组蛋白H2A、H2B、H3和H4各2个;accession numbers:H2A-P04908;H2B-O60814;H3.1-P68431;H4…

SpringMVC处理ajax请求之@ResponseBody注解,将后端数据响应到浏览器

上一篇文章讲到SpringMVC处理ajax请求用到的RequestBody注解SpringMVC处理ajax请求&#xff08;RequestBody注解&#xff09;&#xff0c;ajax向后端传递的数据格式详解-CSDN博客&#xff0c;这个注解帮我们解决了如何将客户端的数据通过json数据传递到服务器&#xff0c;简单说…

数据探索与可视化:可视化分析数据关系-中

目录 一、前言 二、介绍 Ⅰ.一个分类变量和一个连续变量 Ⅱ.两个分类变量的一个连续变量 Ⅲ.两个分类变量和两个连续变量 Ⅳ.一个分类变量和多个连续变量 ①.平行坐标轴 ②.矩阵散点图 三、结语 一、前言 在做数据分析的时候&#xff0c;很少会遇到连续变量和分类变量…

PyTorch如何支持 GPU 加速计算

在 PyTorch 中&#xff0c;torch.Tensor 是核心的数据结构&#xff0c;它与 NumPy 的 ndarray 类似&#xff0c;用于存储和操作多维数据。但与 NumPy 不同的是&#xff0c;PyTorch 的 Tensor 除了能在 CPU 上运行之外&#xff0c;还能够无缝地利用 GPU 进行计算加速。 GPU 加速…

读千脑智能笔记01_新皮质

作者简介 1988年至1992年&#xff0c;创造了平板电脑GridPad&#xff0c;它属于第一批平板电脑 1992年&#xff0c;成立了Palm公司&#xff0c;之后在长达10年的时间内&#xff0c;设计了一些最早的掌上电脑和智能手机&#xff0c;如PalmPilot和Treo 在2002年创立了红木神经科学…

《HTML 简易速速上手小册》第9章:HTML5 新特性(2024 最新版)

文章目录 9.1 HTML5 新增标签和属性9.1.1 基础知识9.1.2 案例 1&#xff1a;创建一个结构化的博客页面9.1.3 案例 2&#xff1a;使用新的表单元素创建事件注册表单9.1.4 案例 3&#xff1a;创建一个具有高级搜索功能的搜索表单 9.2 HTML5 表单增强9.2.1 基础知识9.2.2 案例 1&a…

VScode注释快捷键,RStudio注释快捷键, Texmaker注释快捷键

VScode&#xff1a;一款界面简单的代码编辑器&#xff1b;RStudio: R语言的IDE&#xff0c;包含代码编辑器&#xff0c;运行&#xff0c;绘图窗口等&#xff1b;Texmaker&#xff1a;Latex编译器&#xff08;编辑编译&#xff09;&#xff0c;界面简单&#xff0c;个人认为比Te…

揭秘远程控制APP的便捷之美!

在这个科技日新月异的时代&#xff0c;我们的生活被各种手机软件所包围。几乎每个人都有一个甚至多个手机&#xff0c;你是否也有遇到过需要远程操作自己某一台手机的场景呢&#xff1f;今天&#xff0c;我要向大家推荐一款神奇的手机远程操作神器&#xff0c;让你可以随时随地…

Kore.ai获10亿元融资,提供定制化类ChatGPT助手

1月31日&#xff0c;生成式AI和企业对话平台Kore.ai在官网宣布&#xff0c;获得1.5 亿美元&#xff08;约10.7亿元&#xff09;融资。本次由FTV Capital 领投&#xff0c;英伟达等跟投。 Kore.ai主要提供银行、医疗、零售、营销、人力资源等多种领域的&#xff0c;定制化类Cha…

【动态规划】【C++算法】1340. 跳跃游戏 V

作者推荐 【动态规划】【字符串】【表达式】2019. 解出数学表达式的学生分数 本文涉及知识点 动态规划汇总 LeetCode1340跳跃游戏 V 给你一个整数数组 arr 和一个整数 d 。每一步你可以从下标 i 跳到&#xff1a; i x &#xff0c;其中 i x < arr.length 且 0 < x…

zsh: command not found: mysql (mac通过安装MySQL后终端cmd找不到mysql命令)

考虑是mysql环境变量没有配置的问题 1.查找mysql安装路径 ps -ef|grep mysql 2.先启动上安装的mysql 3. 查看 .bash_profile 文件 ls -al 查看是否有(.bash_profile)文件 如果没有就输入以下命令创建一个&#xff0c;再查看 touch .bash_profile 4.打开 .bash_profile 文件 …

kubekey网页版安装k8s集群操作流程

kubekey可以一键拉起k8s集群并完成kubesphere的部署&#xff0c;以后kubekey简称kk。kk 3.2版本以前都是在宿主机上完成对应的创建集群、添加节点、升级等操作的&#xff0c;3.2版本后开始往页面操作的方向演进&#xff0c;kk 3.2版本现在还是alpha&#xff0c;所以不推荐在生产…

SpringBoot使用Rabbit详解含完整代码

1. 摘要 本文将详细介绍如何在Spring Boot应用程序中集成和使用RabbitMQ消息队列。RabbitMQ是一个开源的消息代理和队列服务器&#xff0c;用于通过轻量级和可靠的消息在应用程序或系统之间进行异步通信。本文将通过步骤说明、代码示例和详细注释&#xff0c;指导读者在Spring…

【数据分享】1929-2023年全球站点的逐年最高气温数据(Shp\Excel\免费获取)

气象数据是在各项研究中都经常使用的数据&#xff0c;气象指标包括气温、风速、降水、湿度等指标&#xff0c;其中又以气温指标最为常用&#xff01;说到气温数据&#xff0c;最详细的气温数据是具体到气象监测站点的气温数据&#xff01; 之前我们分享过1929-2023年全球气象站…

python如何实现异步并发

下面是一个示例代码&#xff0c;展示了如何设计一个异步线程池&#xff0c;并实现线程池满了就等待&#xff0c;空了就继续扔的功能&#xff1a; import concurrent.futures import time # 创建一个线程池 thread_pool concurrent.futures.ThreadPoolExecutor(max_workers8) …

el-table动态合并

废话就不多说了&#xff0c;直接上代码&#xff01;&#xff01;&#xff01; 合并行 // 方法一 <template><div class"container"><el-table :data"dataSource" :border"true":header-cell-style"{ font-weight: normal,…

Kotlin中的内置函数-apply、let

在使用Kotlin的过程中会经常用到其内置函数&#xff0c;包括apply&#xff0c;let&#xff0c;run&#xff0c;with&#xff0c;also&#xff0c;takeIf,takeUnless函数等&#xff0c;想要更好熟悉Kotlin&#xff0c;这些函数必须烂熟于心&#xff0c;接下来让我们来逐步了解&a…

大语言模型之LlaMA系列- LlaMA 2及LLaMA2_chat(上)

LlaMA 2是一个经过预训练与微调的基于自回归的transformer的LLMs&#xff0c;参数从7B至70B。同期推出的Llama 2-Chat是Llama 2专门为对话领域微调的模型。 在许多开放的基准测试中Llama 2-Chat优于其他开源的聊天模型&#xff0c;此外Llama 2-Chat还做了可用性与安全性评估。 …

CKS1.28【1】kube-bench 修复不安全项

Context 针对 kubeadm 创建的 cluster 运行 CIS 基准测试工具时&#xff0c;发现了多个必须立即解决的问题。 Task 通过配置修复所有问题并重新启动受影响的组件以确保新的设置生效。 修复针对 API 服务器发现的所有以下违规行为&#xff1a; 1.2.7 Ensure that the --authoriz…

JVM技术文档-Arthas--线上内存问题定位

阿丹&#xff1a; 记录一次线上问题的定位&#xff0c;以及解释和讲解一下在docker容器中arthas的使用。 arthas使用文档&#xff1a; 我先给官方的文档放在这里&#xff0c;在文章中我使用的代码和解释我也会写&#xff0c;但是针对于每个人遇到的情况不一样&#xff0c;我这…