当前位置: 首页 > news >正文

鸿蒙 Background Tasks Kit(后台任务开发服务)

设备返回主界面、锁屏、应用切换等操作会使应用退至后台。应用退至后台后,如果继续活动,可能会造成设备耗电快、用户界面卡顿等现象。为了降低设备耗电速度、保障用户使用流畅度,系统会对退至后台的应用进行管控,包括进程挂起(Suspend,即系统不再为应用进程分配 CPU 时间片,同时对应的公共事件等不再发给应用进程)和进程终止(Terminate)。

  • 应用退至后台一小段时间(由系统定义),应用进程会被挂起;
  • 资源不足时,系统会终止部分应用进程(即回收该进程的所有资源);

同时,为了保障后台音乐播放、日历提醒等功能的正常使用,系统提供了规范内受约束的后台任务,扩展应用在后台运行时间。

提示:对于鸿蒙系统中运行的所有进程,系统会给予一定的资源配额约束,包括进程在连续一段时间内内存的使用、CPU 使用占比,以及 24 小时磁盘写的 IO 量,均有对应的配额上限。
超过配额上限时,如果进程处于前台,系统会生成 warning 日志;如果进程处于后台,系统会终止该进程

后台任务类型

应用退至后台后如果有继续运行的需求,可申请使用如下几种受系统约束的后台任务:

  1. 短时任务:适用于实时性要求高、耗时不长的任务,例如状态持久化保存。
  2. 长时任务:适用于长时间运行在后台、用户可感知的任务,例如后台播放音乐、导航、设备连接等,使用长时任务避免应用进程被挂起。
  3. 延迟任务:对于实时性要求不高、可延迟执行的任务,系统提供了延迟任务,即满足条件的应用退至后台后被放入执行队列,系统会根据内存、功耗等统一调度;例如:2 小时后自动到邮件服务器收取邮件。
  4. 代理提醒:代理提醒是指应用退后台或进程终止后,系统会代理应用做相应的提醒。适用于定时提醒类业务,例如通知用户定时抢票应用等。

说明:
1、系统仅支持规范内受约束的后台任务。应用退至后台后,若未使用规范内的后台任务或选择的后台任务类型不正确,对应的应用进程会被挂起或终止。
2、应用申请了规范内的后台任务,仅会提升应用进程被回收的优先级。当系统资源严重不足时,即使应用进程申请了规范内的后台任务,系统仍会终止部分进程,用以保障系统稳定性。

短时任务

应用退至后台一小段时间后,会被系统挂起,无法执行对应的任务。如果应用在后台仍需要执行耗时不长的任务,如状态保存等,可以通过申请短时任务,扩展应用在后台的运行时间。

短时任务使用限制:

  1. 申请时机:应用需要在前台或 onBackground 回调内,申请短时任务,否则会申请失败。
  2. 数量限制:一个应用同一时刻最多申请执行 3 个 短时任务。
  3. 配额机制:一个应用会有一定的短时任务配额(根据系统状态和用户习惯调整),单日(24 小时内)配额默认为 10 分钟,单次配额最大为 3 分钟,低电量时单次配额默认为 1 分钟,配额消耗完后不允许再申请短时任务。
  4. 配额计算:仅当应用在后台时,对应用下的短时任务计时;同一个应用下的同一个时间段的短时任务,不重复计时。
  5. 超时:短时任务即将超时时,系统会回调应用,应用需要取消短时任务。如果超时不取消,系统会终止对应的应用进程。
主要接口

下面的 API 都在 backgroundTaskManager 名称空间下

接口名描述
requestSuspendDelay(reason: string, callback: Callback<void>): DelaySuspendInfo申请短时任务
getRemainingDelayTime(requestId: number): Promise<number>获取对应短时任务的剩余时间
cancelSuspendDelay(requestId: number): void取消短时任务
例子
import backgroundTaskManager from '@ohos.resourceschedule.backgroundTaskManager'@Entry
@Component
struct Index {requestId: number = 0 //申请编号,即应用申请延迟挂起这个操作的编号,即后台短时任务编号timerId: number = 0 //周期性定时器编号shortTimeBackgroundTask() {let count = 0this.timerId = setInterval(() => {count++console.log('--' + count + ' 后台任务正在执行', Date.now())}, 1000)}build() {Column({ space: 10 }) {Text('后台任务之一:短时任务').fontSize(30)Button('1.申请执行短时任务A(申请当前应用延迟一会儿再挂起)').onClick(async _ => {try {//请求系统延迟挂起当前应用,我想执行一些短时任务let info = backgroundTaskManager.requestSuspendDelay('申请状态持久化短时任务', () => {console.log('--短时任务时间配额即将到期!请立即执行一些最后的清理操作!')clearInterval(this.timerId) //清理当前页面正在占用的资源backgroundTaskManager.cancelSuspendDelay(this.requestId) //取消挂起延迟,即取消后台任务})//上述挂起延迟申请成功的话,下面的代码就是在“挂起延迟”这段时间内执行的短时后台任务://获得短时任务编号this.requestId = info.requestId//查看当前短时任务剩余的时间配额(以ms为单位)let time = await backgroundTaskManager.getRemainingDelayTime(this.requestId)if (time >= 60000) {console.log('--剩余时间配额足够,开始执行后台任务')this.shortTimeBackgroundTask()} else {console.log('--剩余的时间配额不足以执行目标任务')}} catch (err) {console.log('--申请挂起延迟用以执行短时后台任务失败:', JSON.stringify(err))}})Button('取消短时任务').onClick(_ => {backgroundTaskManager.cancelSuspendDelay(this.requestId)})}.height('100%').width('100%').padding(10)}
}

长时任务

应用退至后台后,在后台需要长时间运行用户可感知的任务,如播放音乐、导航等。为防止应用进程被挂起,导致对应功能异常,可以申请长时任务,使应用在后台长时间运行。
长时任务分为如下类型

参数名描述配置项场景举例
DATA_TRANSFER数据传输dataTransfer后台下载大文件,如浏览器后台下载等
AUDIO_PLAYBACK音视频播放audioPlayback音乐类应用在后台播放音乐
AUDIO_RECORDING录制audioRecording录音机在后台录音
LOCATION定位导航location导航类应用后台导航
BLUETOOTH_INTERACTION蓝牙相关bluetoothInteraction通过蓝牙传输分享的文件
MULTI_DEVICE_CONNECTION多设备互联multiDeviceConnection分布式业务连接(如手机与平板、电视等互联)
TASK_KEEPING计算任务(仅对 2IN1 开放)taskKeeping杀毒软件在后台进行病毒扫描等任务
约束与限制

1、申请限制:Stage 模型中,长时任务仅支持 UIAbility 申请;FA 模型中,长时任务仅支持 ServiceAbility 申请。
2、数量限制:一个 UIAbility 同一时刻仅支持申请一个长时任务,即在一个长时任务结束后才可能继续申请。如果一个应用同时需要申请多个长时任务,需要创建多个 UIAbility;一个应用的一个 UIAbility 申请长时任务后,整个应用下的所有进程均不会被挂起。
3、运行限制:在手机产品上,系统会进行长时任务校验,例如:

  • 场景 ①:若应用申请了长时任务,但未真正执行申请类型的长时任务或申请类型的任务已结束,系统会对应用进行管控。例如系统检测到应用申请了 AUDIO_PLAYBACK(音视频播放),但实际未播放音乐,长时任务会被取消。
  • 场景 ②:若应用没有申请对应的长时任务类型,但执行了相关类型的长时任务,系统会对应用进行管控。例如系统检测到应用只申请了 AUDIO_PLAYBACK(音视频播放),但实际上除了播放音乐,还在进行录制(对应 AUDIO_RECORDING 类型),系统会对应用进行管控。
  • 场景 ③:若运行长时任务的进程后台负载持续高于所申请类型的典型负载,系统会对应用进行管控。
主要接口
接口名描述
startBackgroundRunning(context: Context, bgMode: BackgroundMode, wantAgent: WantAgent): Promise<void>申请长时任务
stopBackgroundRunning(context: Context): Promise<void>取消长时任务

说明:
1、长时任务需申请系统级权限 ohos.permission.KEEP_BACKGROUND_RUNNING 才能使用
2、必须在 module.json5 文件中为 UIAbility 配置相应的长时任务类型 backgroundModes
3、申请长时任务成功后,系统会在通知栏显示相关通知,点击通知可以拉起任务对应的窗口

示例

后台定位服务

当前模块配置 module.json5

{"module": {"abilities": [{"name": "EntryAbility",//申请当前UIAbility可以哪些类型的“后台长时任务”"backgroundModes": ["location"]}//一个UIAbility某个时刻只能执行一个后台长时任务,但是可以先后执行多个长时任务],"requestPermissions": [//当前应用申请数据和功能的访问权限//系统授予级权限 —— 只需要声明name即可{"name": "ohos.permission.KEEP_BACKGROUND_RUNNING"//保持应用在后台持续运行},//用户授予级权限 —— 必须声明name/reason/usedScene三个属性{"name": "ohos.permission.APPROXIMATELY_LOCATION",//模糊定位"reason": "$string:app_name", // 这里用户会看到的申请权限原因,我这里乱写的"usedScene": {"abilities": ["EntryAbility"],"when": "always"}},{"name": "ohos.permission.LOCATION",//精确定位"reason": "$string:app_name",//当前应用需要向用户解释使用该权限的原因"usedScene": {//权限在何种场景下被使用"abilities": ["EntryAbility"],//哪些Ability/窗口中需要使用该权限"when": "always"//何时使用该权限  inuse:当前应用在前台运行时需要使用    always:总是需要该权限,即使应用没在运行}}]}
}
import { abilityAccessCtrl, bundleManager, Permissions, wantAgent } from '@kit.AbilityKit'
import { geoLocationManager } from '@kit.LocationKit'
import { backgroundTaskManager } from '@kit.BackgroundTasksKit'@Entry
@Component
struct Index {//页面显式时,先弹出“申请定位权限”授权窗口async onPageShow() {//① 声明需要用户授权的权限列表let list: Permissions[] = ['ohos.permission.APPROXIMATELY_LOCATION', 'ohos.permission.LOCATION']//② 获得当前应用的“访问令牌”let flags = bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION //需要获得整个应用的信息,而不是模块的/应用组件的let bundleInfo = await bundleManager.getBundleInfoForSelf(flags) //得到当前资源包信息let tokenId = bundleInfo.appInfo.accessTokenId //当前应用的当前分身在当前用户使用场景下,系统分配的令牌编号let atManager = abilityAccessCtrl.createAtManager() //At: Access Token,访问令牌,即当前应用的授权列表let grantStatus0 = await atManager.checkAccessToken(tokenId, list[0])let grantStatus1 = await atManager.checkAccessToken(tokenId, list[1])//③ 从访问令牌中查询,用户是否授予过定位权限if (grantStatus0 == -1 && grantStatus1 == -1) { //0表示已经通过授权了  -1表尚未尚未授权/之前拒绝授权了//④ 如果尚未授权过,则弹出申请授权对话框let result = await atManager.requestPermissionsFromUser(getContext(), list)if (result.authResults[0] == 0) {console.log('1.模糊定位权限已经从用户处申请到')} else {console.log('2.用户拒绝授予模糊定位权限')}if (result.authResults[1] == 0) {console.log('3.精确定位权限已经从用户处申请到')} else {console.log('4.用户拒绝授予精确定位权限')}}}longTimeBackgroundTask() {try {if (!geoLocationManager.isLocationEnabled()) {console.log('--当前系统没有打开定位开关!')return}let count = 0geoLocationManager.on('locationChange', {}, (loc) => {count++console.log('--当前设备位置改变了:', count, JSON.stringify(loc))})} catch (err) {console.log('--持续性定位失败!', JSON.stringify(err))}}build() {Column({ space: 10 }) {Text('后台任务之二:长时任务').fontSize(30)Button('1.申请执行后台长时任务:持续性定位').onClick(async _ => {geoLocationManager.getCurrentLocation({}, (err, loc) => {if (!err) console.log('单次定位成功:', loc);});try {let wa = await wantAgent.getWantAgent({//创建一个want代理,用于给系统通知被点击后跳转到哪个UIAbilitywants: [{ bundleName: 'com.example.quanguokefei', abilityName: 'EntryAbility' }],requestCode: Date.now(), // 使用者定义的一个私有值,这里为了避免重复导致任务无法开启actionType: wantAgent.OperationType.START_ABILITY, //必需属性,指定用户点击通知消息作何处理})console.log('--Want代理创建完成')//开启一个长时后台任务backgroundTaskManager.startBackgroundRunning(getContext(), //参数1:上下文backgroundTaskManager.BackgroundMode.LOCATION, //参数2:后台任务类型wa                                              //参数3:Want代理对象//()=>{ },                                      //参数4:长时任务的内容).then(() => {console.log('--长时任务已经开启,开始执行持续性定位操作...')this.longTimeBackgroundTask()})} catch (err) {console.log('--长时任务开启失败!', JSON.stringify(err))}})Button('2.关闭后台长时任务').onClick(async _ => {await backgroundTaskManager.stopBackgroundRunning(getContext()) //关闭当前后台任务,后续就可以继续开启其它后台任务了// await backgroundTaskManager.stopBackgroundRunning(getContext(), ()=>{})console.log('--停止后台长时任务成功')geoLocationManager.off('locationChange')console.log('--手工关闭持续性定位成功')})}.height('100%').width('100%').padding(10)}
}

延迟任务

应用退至后台后,需要执行实时性要求不高的任务,例如有网络时不定期主动获取邮件等,可以使用延迟任务。当应用满足设定条件(包括网络类型、充电类型、存储状态、电池状态、定时状态等)时,将任务添加到执行队列,系统会根据内存、功耗、设备温度、用户使用习惯等统一调度拉起应用

约束与限制

1、数量限制:一个应用同一时刻最多申请 10 个延迟任务。
2、执行频率限制:系统会根据应用的活跃分组,对延迟任务做分级管控,限制延迟任务调度的执行频率。

应用活跃分组延迟任务最小执行间隔备注
活跃分组2 小时高频使用应用
经常使用分组4 小时中频使用应用
常用使用分组24 小时低频使用应用
极少使用分组48 小时极少唤醒应用
受限使用分组禁止系统限制后台任务
从未使用分组禁止完全冻结后台行为

3、超时:WorkSchedulerExtensionAbility 单次回调最长运行 2 分钟。如果超时不取消,系统会终止对应的 Extension 进程。
4、调度延迟:系统会根据内存、功耗、设备温度、用户使用习惯等统一调度,如当系统内存资源不足或温度达到一定挡位时,系统将延迟调度该任务。
5、WorkSchedulerExtensionAbility 接口调用限制:为实现对 WorkSchedulerExtensionAbility 能力的管控,在其中限制以下接口的调用:后台任务管理/相机管理/音频管理/媒体服务

主要接口
接口名接口描述
startWork(work: WorkInfo): void申请延迟任务
stopWork(work: WorkInfo, needCancel?: boolean): void取消延迟任务
getWorkStatus(workId: number, callback: AsyncCallback<WorkInfo>): void获取延迟任务状态(Callback 形式)
getWorkStatus(workId: number): Promise<WorkInfo>获取延迟任务状态(Promise 形式)
obtainAllWorks(callback: AsyncCallback<Array<WorkInfo>>): void获取所有延迟任务(Callback 形式)
obtainAllWorks(): Promise<Array<WorkInfo>>获取所有延迟任务(Promise 形式)
stopAndClearWorks(): void停止并清除任务
isLastWorkTimeOut(workId: number, callback: AsyncCallback<boolean>): void获取上次任务是否超时(针对 RepeatWork,Callback 形式)
isLastWorkTimeOut(workId: number): Promise<boolean>获取上次任务是否超时(针对 RepeatWork,Promise 形式)
示例

第一步:使用 DevEco Studio 辅助创建 WorkSchedulerExtensionAbility

比如我创建的名字是 EntryWorkSchedulerExtAbility

import {workScheduler,WorkSchedulerExtensionAbility,
} from "@kit.BackgroundTasksKit";//扩展的延迟任务应用组件,会由系统提供的“任务调度器”来启动,运行在专有的ExtensionAbility进程中
export default class WorkSchedulerExtension extends WorkSchedulerExtensionAbility {timerId: number = 0;onWorkStart(workInfo: workScheduler.WorkInfo) {console.log("--WorkSchedulerExtension.onWorkStart:延迟任务开始执行了...",JSON.stringify(workInfo));//TODO: 发起HTTP请求,获取邮件服务器上当前用户最新的邮件...let count = 0;this.timerId = setInterval(() => {count++;console.log("--" + count + " 延迟任务执行中:", Date.now());}, 1000);}onWorkStop(workInfo: workScheduler.WorkInfo) {console.log("--WorkSchedulerExtension.onWorkStop:延迟任务终止执行了",JSON.stringify(workInfo));clearInterval(this.timerId);console.log("--已经手工清理了延迟任务相关的资源对象");}
}

第二步:申请开启延迟任务 和 取消延迟任务

import { workScheduler } from '@kit.BackgroundTasksKit'@Entry
@Component
struct Index {build() {Column({ space: 10 }) {Text('后台任务之三:延迟任务').fontSize(30)Button('1.申请注册一个延迟任务').onClick(_ => {const workId: number = Math.floor(1000 + Math.random() * 9000)const work: workScheduler.WorkInfo = {workId, //必需,延迟任务编号 —— 注意:同编号的任务再次添加,会抛出错误:9700005bundleName: 'com.example.quanguokefei', //必需,延迟任务所在的应用abilityName: 'EntryWorkSchedulerExtAbility', //必需,延迟任务所在的ExtensionAbility//必需属性:当前延迟任务在何种条件下可以获得执行:// isCharging: true,     //只有处于充电状态下才能执行// chargerType: workScheduler.ChargingType.CHARGING_PLUGGED_USB,  //只有在指定的充电设备类型时才执行batteryStatus: workScheduler.BatteryStatus.BATTERY_STATUS_OKAY, //只有电池状态处于OK(>20%)时才执行// networkType: workScheduler.NetworkType.NETWORK_TYPE_WIFI,      //只有网络进入指定的状态类型时才执行// storageRequest: workScheduler.StorageRequest.STORAGE_LEVEL_OKAY, //只有外存剩余存储空间达到指定状态才执行// repeatCycleTime: 1000*60*60*2                //间隔多久执行一次当前任务}try {workScheduler.startWork(work) //向系统注册一个延迟任务console.log('--UIAbility/主进程中注册了一个延迟任务,编号为:' + workId)} catch (err) {console.log('--UIAbility/主进程中注册延迟任务失败', JSON.stringify(err))}})}.height('100%').width('100%').padding(10)}
}

效果是自动输出 2 分钟后因为超时被系统自动杀死

代理提醒

应用退到后台或进程终止后,仍然有一些提醒用户的定时类任务,例如购物类应用抢购提醒等,为满足此类功能场景,系统提供了代理提醒(reminderAgentManager)的能力。当应用退至后台或进程终止后,系统会代理应用做相应的提醒。当前支持的提醒类型包括:倒计时、日历和闹钟。

  • 倒计时类(Timer):基于倒计时的提醒功能,例如:2 小时后提醒吃药。
  • 日历类(Calendar):基于日历的提醒功能,例如:12 月 12 日凌晨提醒抢票。
  • 闹钟类(Alarm):基于时钟的提醒功能,例如:08:00 叫我起床。
约束与限制

1、个数限制:一个三方应用支持最多 30 个有效提醒(有效即发布成功),一个系统应用支持最多 2000 个有效提醒,整个系统最多支持 12000 个有效提醒。

2、跳转限制:点击提醒通知后跳转的应用必须是申请代理提醒的本应用。

3、管控限制:为了防止代理提醒被用于滥用于广告、营销类提醒,影响用户体验,代理增加了管控机制。管控后可通过日历 Calendar Kit 替代代理提醒,实现相应的提醒功能。

所以,这小节的示例代码能跑,但是被限制死了。提醒不是不可以发,得有些实力和背景
{"data":1700002,"code":1700002,"message":"The number of reminders exceeds the limit."}

主要接口
接口名描述
publishReminder(reminderReq: ReminderRequest): Promise<number>发布一个定时提醒类通知
cancelReminder(reminderId: number): Promise<void>取消一个指定的提醒类通知
getValidReminders(): Promise<Array<ReminderRequest>>获取当前应用设置的所有有效的提醒
cancelAllReminders(): Promise<void>取消当前应用设置的所有提醒
addNotificationSlot(slot: NotificationSlot): Promise<void>注册一个提醒类需要使用的通知通道(NotificationSlot)
removeNotificationSlot(slotType: notification.SlotType): Promise<void>删除指定的通知通道(NotificationSlot)

说明:
1、代理提醒需申请系统级权限 ohos.permission. PUBLISH_AGENT_REMINDER 才能使用
2、应用需要获取用户授权才能发送通知。在通知发布前调用 requestEnableNotification() 方法

{"module": {"requestPermissions": [{"name": "ohos.permission.PUBLISH_AGENT_REMINDER" //允许当前应用委托鸿蒙系统发起一个通知(称为代理提醒)}]}
}
示例
import { notificationManager } from '@kit.NotificationKit'
import { common } from '@kit.AbilityKit'
import { reminderAgentManager } from '@kit.BackgroundTasksKit'
import { JSON } from '@kit.ArkTS'@Entry
@Component
struct Index {//页面即将显示时,弹出提醒用户允许当前应用发起通知或代理通知 —— 类似于用户级权限授权,但代码不一样async onPageShow() {if (!notificationManager.isNotificationEnabledSync()) {console.log('--当前应用 尚未 向用户申请过“发起通知”弹窗')let ctx = getContext()  as  common.UIAbilityContextawait notificationManager.requestEnableNotification(ctx)console.log('--当前应用 成功 获得了用户的通知弹窗许可')}else {console.log('--当前应用 已经 向用户申请过“发起通知”弹窗,可能通过了,也可能是未通过')}}build() {Column({ space: 10 }) {Text('后台任务之四:代理提醒').fontSize(30)Button('1.申请发布一个代理提醒 —— 倒计时类型').onClick(async _ => {let req:reminderAgentManager.ReminderRequestTimer  = {reminderType: reminderAgentManager.ReminderType.REMINDER_TYPE_TIMER,  //提醒类型:倒计时triggerTimeInSeconds: 15,     //多少秒钟后触发提醒title: '温馨提示',              //通知标题content: '距离吃完饭已经2个小时,可以吃药了',wantAgent: {                  //通知被点击后,拉起哪个UIAbilitypkgName: 'com.example.quanguokefei',abilityName: 'EntryAbility'}}try{let rid = await reminderAgentManager.publishReminder(req)console.log('--代理提醒发布完成,提醒的编号:', rid)}catch(err){console.log('--代理提醒发布失败', JSON.stringify(err))//提醒:2024年10月28日之后,华为把应用可发布的代理提醒数量降为:0}})Button('2.申请发布一个代理提醒 —— 日历类型').onClick(async _ => {let req:reminderAgentManager.ReminderRequestCalendar  = {reminderType: reminderAgentManager.ReminderType.REMINDER_TYPE_CALENDAR,dateTime: {year:2025, month:12, day:12, hour:0, minute:0, second:0},  //必需是未来时间title: '抢券提示',content: '双12活动正式开始!!!',wantAgent: {pkgName: 'com.example.quanguokefei',abilityName: 'EntryAbility'},}try{let rid = await reminderAgentManager.publishReminder(req)console.log('--代理提醒发布完成,提醒的编号:', rid)}catch(err){console.log('--代理提醒发布失败', JSON.stringify(err))//提醒:2024年10月28日之后,华为把应用可发布的代理提醒数量降为:0}})Button('3.申请发布一个代理提醒 —— 闹钟类型').onClick(async _ => {let req:reminderAgentManager.ReminderRequestAlarm  = {reminderType: reminderAgentManager.ReminderType.REMINDER_TYPE_ALARM,hour: 8,minute: 0,title: '温馨提示',content: '早上8点了,该起床了',wantAgent: {pkgName: 'com.example.quanguokefei',abilityName: 'EntryAbility'},}try{let rid = await reminderAgentManager.publishReminder(req)console.log('--代理提醒发布完成,提醒的编号:', rid)}catch(err){console.log('--代理提醒发布失败', JSON.stringify(err))//提醒:2024年10月28日之后,华为把应用可发布的代理提醒数量降为:0}})}.height('100%').width('100%').padding(10)}
}

四大类后台任务对比

任务类型数量限制时长限制任务内容失效性
短时任务33min/单任务,10min/所有任务无限制立即执行
长时任务1无限制七大类立即执行
延迟任务10m/单任务只有四类不允许未来条件满足时执行
代理提醒30(需申请)仅能发提醒仅能发提醒未来条件满足时发提醒

相关文章:

  • 全局配置文件
  • 如何确保低空经济中的数据安全?
  • Flink概述
  • 排序复习/下(C语言版)
  • Scala语言基础与函数式编程详解
  • Web3:Ubuntu系统 使用Docker-compose方式部署blockscout浏览器配置版本-v5.2.3-beta+charts图表
  • Web 技术与 Nginx 网站环境部署
  • 大数据hadoop小文件处理方案
  • CRMEB多商户预约服务上门师傅端
  • 可编辑98页PPT | 某大型制造业数字化转型战略规划项目方案
  • 使用PowerShell备份和还原Windows环境变量
  • vue2.0 组件生命周期
  • MYSQL故障排查和环境优化
  • 学习黑客 PowerShell 详解
  • 远程医疗结合贴肤芯片技术对体育院校学生提升运动表现的路径分析
  • 详解Oracle HASH CHAIN和HASH BUCKET
  • PostgreSQL基本用法
  • 数据分析入门指南:从历史到实践
  • Linux详解基本指令(一)
  • HttpMessageConverter 的作用是什么? 它是如何实现请求体到对象、对象到响应体的自动转换的(特别是 JSON/XML)?
  • 4年间职务侵占、受贿逾亿元,北京高院:严惩民企内部腐败
  • 一周人物|收藏家瓦尔特捐出藏品,女性艺术家“对话”摄影
  • 解放日报“解码上海AI产业链”:在开源浪潮中,集聚要素抢先机
  • 83岁山水花鸟画家、书法家吴静山离世,系岭南画派代表人物
  • 上海:到2027年,实现近海航线及重点海域5G网络高质量覆盖
  • 马上评|重病老人取款身亡,如何避免类似悲剧?