Android 8.0 Oreo (系统篇)
Android 8.0 Oreo 更智能、更迅捷、更强大。广受世人喜爱的不仅仅是曲奇饼干,更是为您打造的全新 Android 系统。Android 8.0 为用户和开发者引入多种新功能。本文重点介绍面向开发者的新功能。
欢迎关注微信公众号:程序员Android
微信公众号:ProgramAndroid
我们不是牛逼的程序员,我们只是程序开发中的垫脚石。
1.新的 StrictMode 检测程序
Android 8.0 添加了三个新的 StrictMode 检测程序,帮助识别应用可能出现的错误:
detectUnbufferedIo()
将检测您的应用何时读取或写入未缓冲的数据,这可能极大影响性能。detectContentUriWithoutPermission()
将检测您的应用在其外部启动 Activity 时何时意外忘记向其他应用授予权限。detectUntaggedSockets()
将检测您的应用何时使用网络流量,而不使用 setThreadStatsTag(int) 将流量标记用于调试目的。
2 . 缓存数据
Android 8.0 优化了缓存数据的导航和行为。现在,每个应用均获得一定的磁盘空间配额,用于存储 getCacheQuotaBytes(UUID)
返回的缓存数据。
当系统需要释放磁盘空间时,将开始从超过配额最多的应用中删除缓存文件。因此,如果将您的缓存数据量始终保持低于配额的水平,则在必须清除系统中的某些文件时,您的缓存文件将能坚持到最后。系统在决定删除您的应用中的哪些缓存文件时,将首先考虑删除最旧的文件(由修改时间确定)。
您还可以针对每个目录启用两种新行为,以控制系统如何释放缓存数据:
StorageManager.setCacheBehaviorAtomic()
可用于指示某个目录及其所有内容应作为一个不可分割的整体进行删除。setCacheBehaviorTombstone(File, boolean)
可用于指示不应删除某个目录内的文件,而应将它们截断到 0 字节长度,使空文件保持完好。allocateBytes(FileDescriptor, long) API
,它将自动清除属于其他应用的缓存文件(根据需要),以满足您的请求。在确定设备是否有足够的磁盘空间保存您的新数据时,请调用getAllocatableBytes(UUID)
而不要使用getUsableSpace()
,因为前者会考虑系统要为您清除的任何缓存数据。
3. 内容提供程序分页
我们已更新内容提供程序以支持加载大型数据集,每次加载一页。例如,一个具有大量图像的照片应用可查询要在页面中显示的数据的子集。内容提供程序返回的每个结果页面由一个 Cursor 对象表示。客户端和提供程序必须实现分页才能利用此功能。
如需了解有关内容提供程序变更的详细信息,请参阅 ContentProvider
和 ContentProviderClient
。
4 . 内容刷新请求
现在,ContentProvider
和ContentResolver
类均包含 refresh()
函数,这样,客户端可以更轻松地知道所请求的信息是否为最新信息。
您可以扩展ContentProvider
以添加自定义的内容刷新逻辑。请务必重写refresh()
函数,以返回true
,告知提供程序的客户端您已尝试自行刷新数据。
您的客户端应用可通过调用另一个函数(又称 refresh())
,显式请求已刷新的内容。在调用此函数时,传入待刷新数据的URI
。
注:由于您可能通过网络不断请求数据,您应仅在有明显迹象表明内容确已过时时才从客户端调用 refresh()。执行此类内容刷新最常见的原因是响应滑动刷新手势,该手势显式请求当前界面显示最新内容。
5. JobScheduler 改进
Android 8.0 引入了对 JobScheduler
的多项改进。由于您通常可以使用计划作业替代现在受限的后台服务或隐式广播接收器,这些改进可以让您的应用更轻松地符合新的后台执行限制。
JobScheduler 的更新包括:
您现在可以将工作队列与计划作业关联。要将一个工作项添加到作业的队列中,请调用
JobScheduler.enqueue()
。当作业运行时,它可以将待定工作从队列中剥离并进行处理。这种功能可以处理之前需要启动后台服务(尤其是实现IntentService
的服务)的许多用例。您现在可以通过调用
JobInfo.Builder.setClipData()
的方式将ClipData
与作业关联。利用此选项,您可以将 URI 权限授予与作业关联,类似于这些权限传递到Context.startService()
的方式。您也可以将 URI 权限授予用于工作队列上的intent
。计划作业现在支持多个新的约束条件:
JobInfo.isRequireStorageNotLow()
JobInfo.isRequireBatteryNotLow()
NETWORK_TYPE_METERED
6. 自定义数据存储
Android 8.0 允许您为首选项提供自定义数据存储,如果您的应用将首选项存储在云或本地数据库中,或者如果首选项特定于某个设备,此功能会非常有用。如需了解有关实现数据存储的详细信息,请参阅自定义数据存储。
7. findViewById() 签名变更
现在,findViewById()
函数的全部实例均返回 <T extends View> T
,而不是 View
。此变更会带来以下影响:
例如,如果
someMethod(View)
和someMethod(TextView)
均接受调用findViewById()
的结果,这可能导致现有代码的返回类型不确定。在使用 Java 8 源语言时,这需要在返回类型不受限制时(例如,
assertNotNull(findViewById(...)).someViewMethod()))
显式转换为 View。重写非最终的
findViewById()
函数(例如,Activity.findViewById())将需要更新其返回类型。
8. 媒体增强功能
有一个新的 VolumeShaper
类。您可以用它来执行简短的自动音量转换,例如淡入、淡出和交叉淡入淡出。
音频焦点增强功能
音频应用通过请求和舍弃音频焦点的方式在设备上共享音频输出。应用通过启动或停止播放或者闪避音量的方式处理处于聚焦状态的变更。有一个新的 AudioFocusRequest
类。对于此类,应用在处理音频焦点变化时会使用新功能:自动闪避和延迟聚焦
。
媒体指标
新的 getMetrics()
函数将返回一个包含配置和性能信息的 PersistableBundle
对象,用一个包含属性和值的地图表示。为以下媒体类定义 getMetrics()
函数:
MediaPlayer.getMetrics()
MediaRecorder.getMetrics()
MediaCodec.getMetrics()
MediaExtractor.getMetrics()
为每个实例单独收集指标,并持续到实例的生命周期结束为止。如果没有可用的指标,则此函数将返回 null。返回的实际指标取决于类。
9 . MediaPlayer
Android 8.0 为 MediaPlayer
类添加了多种新函数。这些函数可以从多个方面增强您的应用处理媒体播放的能力:
在搜索帧时进行精细控制。
音频录制器
音频录制器现在支持对流式传输有用的 MPEG2_TS 格式:
mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_2_TS);
请参阅 MediaRecorder.OutputFormat
MediaMuxer
现在可以处理任意数量的音频和视频流,而不再仅限于一个音频曲目和/或一个视频曲目。使用addTrack()
可混录所需的任意数量的曲目。MediaMuxer
还可以添加一个或多个包含用户定义的每帧信息的元数据曲目。元数据的格式由您的应用定义。仅对 MP4 容器支持元数据曲目。
在添加元数据曲目时,曲目的 MIME
格式必须以前缀“application/”
开头。除了数据不是来源于 MediaCodec
以外,写入元数据的操作与写入视频/音频数据相同。相反,应用将包含相关时间戳的 ByteBuffer
传递给 writeSampleData()
函数。时间戳必须和视频及音频曲目处于相同的时基。
生成的 MP4 文件使用 ISOBMFF 的 12.3.3.2 部分定义的 TextMetaDataSampleEntry
,指示元数据的 MIME 格式。在使用MediaExtractor 提取包含元数据曲目的文件时,元数据的 MIME 格式将提取到
MediaFormat` 中。
10. 音频播放控制
Android 8.0 允许您查询和请求设备产生声音的方式。对音频播放的以下控制将让您的服务更轻松地仅在有利的设备条件下产生声音。
Google 智能助理的新音频使用类型AudioAttributes
类包含一种新的声音类型,即 USAGE_ASSISTANT
,对应于 Google 智能助理在设备上的回答。
设备音频播放的变更
如果您希望自己的服务仅在特定的设备音频配置处于活动状态时开始产生声音,您可以使用 AudioManager
类注册一个AudioManager.AudioPlaybackCallback
实例,后者的onPlaybackConfigChanged()
函数可以帮助您确定当前活动的音频属性集。
显式请求音频焦点
您的服务可以使用 requestAudioFocus()
函数提交一个更精细的设备级音频焦点接收请求。传入一个 AudioFocusRequest
对象,您可以使用 AudioFocusRequest.Builder
创建这个对象。在这个构建类中,您可以指定以下选项:
您希望获得的焦点类型,例如
AUDIOFOCUS_GAIN_TRANSIENT 或 AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
。当另一个音频服务获得设备焦点时,您的服务应以更安静的方式继续,还是完全暂停。
您的服务能否等待获得焦点,直至设备就绪。
注:构建您的
AudioFocusRequest
实例时,如果您通过调用setAcceptsDelayedFocusGain()
指示您的服务可以等待产生声音,您也必须调用setOnAudioFocusChangeListener()
,以便您的服务了解它何时可以开始产生声音。
11. 增强的媒体文件访问功能
存储访问框架 (SAF)
允许应用显示自定义 DocumentsProvider
,后者可以为其他应用提供访问数据源中的文件的权限。事实上,文档提供程序甚至可以提供驻留在网络存储区或使用媒体传输协议 (MTP)
等协议的文件的访问权限。
但是,访问远程数据源中的大媒体文件面临一些挑战:
媒体播放器需要以寻址方式访问来自文档提供程序的文件。当大媒体文件驻留在远程数据源上时,文档提供程序必须事先提取所有数据,并创建快照文件描述符。媒体播放器无法播放没有文件描述符的文件,因此在文档提供程序完成文件下载前,无法开始播放。
照片应用等媒体集合管理器必须通过作用域文件夹遍历一系列访问 URI 才能访问存储在外部 SD 卡上的媒体。这种访问模式会让媒体上的批量操作(例如移动、复制和删除)变得非常缓慢。
媒体集合管理器无法根据文档的 URI 确定其位置。这就让这些类型的应用难以允许用户选择媒体文件的保存位置。
12. 自定义文档提供程序
从 Android 8.0 开始,存储访问框架允许自定义文档提供程序为驻留在远程数据源中的文件创建可寻址的文件描述符。SAF 可打开文件,获取原生可寻址的文件描述符。然后 SAF 向文档提供程序提交离散字节请求。此功能使文档提供程序可以返回媒体播放器应用请求的准确字节范围,而不必事先缓存整个文件。
要使用此功能,您需要调用新的 StorageManager.openProxyFileDescriptor()
函数。openProxyFileDescriptor()
函数可接受 ProxyFileDescriptorCallback
对象作为回调。任何时候,当客户端应用对文档提供程序返回的文件描述符执行文件操作时,SAF 都会调用回调。
直接文档访问
从 Android 8.0 开始,您可以使用 getDocumentUri() 函数获得与给定 mediaUri 引用相同文档的 URI。不过,由于返回的 URI 由 DocumentsProvider 提供支持,媒体集合管理器可以直接访问文档,不用遍历作用域目录树。因此,媒体管理器能够以明显加快的速度对文档执行文件操作。
注意:getDocumentUri() 函数仅可以定位媒体文件;无法授予应用访问这些文件的权限。要详细了解如何获取媒体文件的访问权限,请参阅参考文档。
文档路径
在 Android 8.0 中使用存储访问框架时,您可以根据文档的 ID,使用 findDocumentPath()
函数(存在于 DocumentsContract
和 DocumentsProvider
类中)从文件系统的根目录中确定路径。该函数将在 DocumentsContract.Path
对象中返回此路径。如果文件系统对相同文档有多个定义的路径,该函数将返回访问具有给定 ID 的文档时最常使用的路径。
此功能在下列情况下特别有用:
您的应用使用可以显示特定文档位置的“另存为”对话框。
您的应用在搜索结果视图中显示文件夹并且如果用户选择某个文件夹,应用必须加载此特定文件夹内的子文档。
注:如果您的应用仅具有路径中某些文档的访问权限,那么 findDocumentPath() 的返回值将仅包含您的应用可以访问的文件夹和文档。
13. 连接
WLAN 感知
Android 8.0 新增了对 WLAN 感知的支持,此技术基于周边感知联网 (NAN) 规范。在具有相应 WLAN 感知硬件的设备上,应用和附近设备可以通过 WLAN 进行搜索和通信,无需依赖互联网接入点。我们正在与硬件合作伙伴合作,以尽快将 WLAN 感知技术应用于设备。要了解有关如何将 WLAN 感知集成到您的应用中的信息,请参阅 WLAN 感知。
蓝牙
Android 8.0 通过增加以下功能,增强了平台对蓝牙的支持:
支持 AVRCP 1.4 标准,该标准支持音乐库浏览。
配套设备配对
在尝试通过蓝牙、BLE 和 WLAN 与配套设备配对时,Android 8.0 提供的 API 允许您自定义配对请求对话框。如需了解详细信息,请参阅配套设备配对。
如需了解有关在 Android 上使用蓝牙的详细信息,请参阅蓝牙指南。有关对蓝牙所作的特定于 Android 8.0 的变更,请参阅 Android 8.0 行为变更页面的蓝牙部分。
14. 共享
智能共享
Android 8.0 了解用户的个性化分享首选项,在通过哪些应用分享各个类型的内容方面,也有着更好的把握。例如,如果用户为一张收据拍照,Android 8.0 可以建议费用跟踪应用;如果用户自拍,一款社交媒体应用可以更好地处理图像。Android 8.0 可以根据用户的个性化首选项自动学习所有这些模式。
智能分享适用于 image 之外的内容类型,例如 audio、video、text 和 URL 等。
要启用智能分享,请将具有最多三个字符串注释的 ArrayList 添加到分享内容的 intent。这些注释应说明内容中的主要部分或主题。下面的代码示例显示了如何向 intent 添加注释:
ArrayList<String> annotations = new ArrayList<>();
annotations.add("topic1");
annotations.add("topic2");
annotations.add("topic3");
intent.putStringArrayListExtra(
Intent.EXTRA_CONTENT_ANNOTATIONS,
annotations
);
如需了解有关智能分享注释的详细信息,请参阅 EXTRA_CONTENT_ANNOTATIONS。
15. 智能文本选择
在兼容设备上,Android 8.0 让应用可以帮助用户以更有意义的方式与文本交互。当用户长按某个实体中可识别格式的单词(例如某个地址或餐馆名称)时,系统会选中整个实体。用户会看到一个浮动工具栏,该工具栏包含可以处理所选文本实体的应用。例如,如果系统识别出某个地址,它可以将用户导向地图应用。
系统识别的实体包括地址、网址、电话号码和电子邮件地址。如需了解详细信息,请参阅 TextClassifier。
16. 无障碍功能
Android 8.0 支持开发者使用以下无障碍功能创建自己的无障碍服务。如需了解有关如何让您的应用更便于访问的更多信息,请参阅无障碍功能。
无障碍功能按钮
您的无障碍服务现在可以请求在系统的导航区域显示无障碍功能按钮,该按钮让用户可从其设备上的任意位置快速激活您的服务功能。要执行此操作,请在某个 AccessibilityServiceInfo
对象的 android:accessibilityFlags
属性中添加 FLAG_REQUEST_ACCESSIBILITY_BUTTON
标志。稍后,您可以使用registerAccessibilityButtonCallback()
注册回调。
注:此功能仅适用于提供软件渲染导航区域的设备。请始终使用
isAccessibilityButtonAvailable()
,并通过实现onAvailabilityChanged()
根据无障碍功能按钮的可用性来响应变更。通过该方式,用户可以始终访问您的服务功能,即使该无障碍功能按钮不受支持或变得不可用。
17. 独立的音量调整
Android 8.0 引入了STREAM_ACCESSIBILITY
音量类别,允许您单独控制无障碍服务音频输出的音量,而不会影响设备上的其他声音。
要使用这个新的流类型来控制无障碍服务音量,请在无障碍服务中设置 FLAG_ENABLE_ACCESSIBILITY_VOLUME
选项。然后,您可以使用adjustStreamVolume()
更改设备的无障碍服务音频音量。
18 .指纹手势
您的无障碍服务也可以响应替代的输入机制,即沿设备的指纹传感器按特定方向滑动(上、下、左和右)。要接收有关这些交互的回调,请完成以下一系列步骤:
1.声明
USE_FINGERPRINT
权限和CAPABILITY_CAN_REQUEST_FINGERPRINT_GESTURES
功能。
在
android:accessibilityFlags 属性中设置
FLAG_REQUEST_FINGERPRINT_GESTURES `标志。
使用
registerFingerprintGestureCallback()
注册回调。isHardwareDetected()
函数识别设备是否支持此传感器。即使对于包含指纹传感器的设备,您的服务也只有在指纹传感器不用于身份验证目的时才可使用它。要识别此传感器何时可用,请调用isGestureDetectionAvailable()
函数并实现onGestureDetectionAvailabilityChanged()
回调。
19. 字词级突出显示
要确定 TextView
对象中可见字符的位置,您可以在 EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY
中将其作为第一个参数传递到 refreshWithExtraData()
中。随后会更新您为refreshWithExtraData()
提供的作为第二个参数的 Bundle
对象,使之包含一个可打包的 Rect
对象数组。每个 Rect 对象代表某个特定字符的边界框。
如果您的服务使用 TextToSpeech
对象朗读屏幕上出现的内容,您可以获取有关文本到语音转换引擎何时开始朗读单个合成字词时的准确时间信息,前提是文本到语音转换引擎提供此信息。当引擎即将开始播放特定范围文本的音频时,Text-to-Speech API
会通知您的服务,将使用 onRangeStart()
函数开始朗读此范围的文本。
如果您创建自己的 TextToSpeechService
实现,您可以使用rangeStart()
函数支持这一新功能。
20. 标准化单端范围值
AccessibilityNodeInfo
的一些实例使用 AccessibilityNodeInfo.RangeInfo
的某个实例来表明界面元素可接受一定范围的值。使用 RangeInfo.obtain()
创建范围或使用getMin()
和 getMax()
检索此范围的极值时,请注意,Android 8.0 规定了标准化单端范围:
对于没有最小值的范围,Float.NEGATIVE_INFINITY 表示最小值。
对于没有最大值的范围,Float.POSITIVE_INFINITY 表示最大值。
21. 提示文本
Android 8.0 包含可用于与文本可编辑对象的提示文本进行交互的多个函数:
isShowingHintText() 和 setShowingHintText() 函数分别显示和设置节点的当前文本内容是否表示节点的提示文本。如果节点不包含可编辑文本,则它不应包含提示文本。
要访问提示文本本身,请使用 getHintText()。即使某个对象当前未显示提示文本,系统也能成功调用
getHintText()。
连续的手势分派
您的服务现在可以使用 GestureDescription.StrokeDescription
构造函数中的最后一个参数 willContinue,指定属于同一设定手势的笔划的顺序
22. 安全性与隐私
权限
Android 8.0 引入了多个与电话有关的新权限:
ANSWER_PHONE_CALLS
允许您的应用通过编程方式接听呼入电话。要在您的应用中处理呼入电话,您可以使用acceptRingingCall()
函数。READ_PHONE_NUMBERS
权限允许您的应用读取设备中存储的电话号码。危险
类别,属于 PHONE 权限组。
23. 新的帐号访问和 Discovery API
Android 8.0 对应用访问用户帐号的方式引入多项改进。对于由身份验证器管理的帐号,身份验证器在决定对应用隐藏帐号还是显示帐号时可以使用自己的策略。Android 系统跟踪可以访问特定帐号的应用。
在以前的 Android 版本中,想要跟踪用户帐号列表的应用必须获取有关所有帐号的更新,包括具有不相关类型的帐号。Android 8.0 添加了 addOnAccountsUpdatedListener(android.accounts.OnAccountsUpdateListener, android.os.Handler, boolean, java.lang.String[])
函数,其允许应用指定应接收帐号变更的帐号类型列表。
API 变更AccountManager
提供六个新函数以帮助身份验证器管理哪些应用可以查看某个帐号:
setAccountVisibility(android.accounts.Account, java.lang.String, int)
:针对特定用户帐号和软件包组合设置可见性级别。getAccountVisibility(android.accounts.Account, java.lang.String)
:获取特定用户帐号和软件包组合的可见性级别。getAccountsAndVisibilityForPackage(java.lang.String, java.lang.String)
:允许身份验证器获取帐号和给定软件包的可见性级别。getPackagesAndVisibilityForAccount(android.accounts.Account)
:允许身份验证器获取存储的给定帐号的可见性值。addAccountExplicitly(android.accounts.Account, java.lang.String, android.os.Bundle, java.util.Map<java.lang.String, java.lang.Integer>)
:允许身份验证器初始化帐号的可见性值。addOnAccountsUpdatedListener(android.accounts.OnAccountsUpdateListener, android.os.Handler, boolean, java.lang.String[])
:将OnAccountsUpdateListener
侦听器添加到AccountManager
对象。无论设备上的帐号列表何时发生变化,系统都将调用此侦听器。setAccountVisibility(android.accounts.Account, java.lang.String, int)
函数指定未设置的应用的可见性级别。PACKAGE_NAME_KEY_LEGACY_VISIBLE
可见性值应用于具有GET_ACCOUNTS
权限的应用,并且其目标 Android 版本低于 Android 8.0,或其签名与针对任意 Android 版本的身份验证器匹配。PACKAGE_NAME_KEY_LEGACY_NOT_VISIBLE
为之前未设置的应用提供默认的可见性值,对于此类应用,PACKAGE_NAME_KEY_LEGACY_VISIBLE
不适用。
如需了解有关新的帐号访问和发现 API 的详细信息,请参阅 AccountManager
和OnAccountsUpdateListener
参考。
24. Google Safe Browsing API
WebView 类现在添加了一个Safe Browsing API
来增强网络浏览的安全性。如需了解详细信息,请参阅 Google Safe Browsing API。
25. 测试
仪器测试
Android 8.0 为应用的仪器测试提供以下几项额外支持。
针对非默认应用进程运行
现在,您可以指定针对您的应用的默认进程以外的进程运行特定仪器测试。如果您的应用包含多个在不同进程中运行的操作组件,此配置非常有用。
要定义非默认进程仪器测试,请导航至您的清单文件,然后导航至所需的<instrumentation>
元素。添加 android:targetProcess
属性,并将它的值设置为以下值之一:
特定进程的名称。
以逗号分隔的进程名称列表。
通配符("*"),允许针对任何执行 android:targetPackage 属性中指定的软件包中的代码的已启动进程运行仪器测试。
在测试过程中报告结果
用于测试的模拟 Intent
为了更轻松地为您应用的操作组件创建隔离、独立的界面测试,Android 8.0 引入了 onStartActivity() 函数。要处理您的测试类调用的特定 intent,您可以在 Instrumentation.ActivityMonitor
类的自定义子类中替换此函数。
当您的测试类调用 intent 时,该函数将返回一个存根 Instrumentation.ActivityResult 对象,而不是执行 intent 本身。通过在您的测试中使用这种模拟 intent 逻辑,您可以侧重于自己的操作组件如何准备和处理您传递到不同操作组件或完全不同的应用中的 intent。
26. 运行时和工具
平台优化
Android 8.0 为平台引入了运行时优化和其他优化,这些优化将带来多项性能改进。这些优化包括并发压缩垃圾回收、更有效的内存利用和代码区域。
它们可以加快启动时间,并为 OS 和应用带来更好的性能。
更新的 Java 支持
Android 8.0 添加了对更多 OpenJDK Java API 的支持:
OpenJDK 8 中的
java.time
。OpenJDK 7 中的
java.nio.file
和java.lang.invoke
。
如果您想要在 Android Studio 中使用 Java 8 语言功能,您应下载最新的预览版本。
更新的 ICU4J Android Framework API
Android 中使用的 ICU、CLDR 和 Unicode 版本
如需详细了解针对受支持的 ICU4J API 的更新,请阅读版本说明。
27. Android 企业版
已为运行 Android 8.0 的设备引入新的企业功能和 API。重要功能包括如下:
完全托管的设备中的工作资料使企业可以在管理工作数据与个人数据的同时,将它们分离开来。
欢迎关注微信公众号:程序员Android
微信公众号:ProgramAndroid
我们不是牛逼的程序员,我们只是程序开发中的垫脚石。
点击阅读原文,获取更多福利
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!