HarmonyOs应用权限申请,system_grant和user_grant区别。本文附头像上传申请user-grant权限代码示例

news2024/12/24 2:10:20

HarmonyOs应用权限申请,system_grant和user_grant区别。本文附头像上传申请user-grant权限代码示例

system_grant(系统授权)

system_grant指的是系统授权类型,在该类型的权限许可下,应用被允许访问的数据不会涉及到用户或设备的敏感信息,应用被允许执行的操作对系统或者其他应用产生的影响可控

user_grant(用户授权)

user_grant指的是用户授权类型,在该类型的权限许可下,应用被允许访问的数据将会涉及到用户或设备的敏感信息,应用被允许执行的操作可能对系统或者其他应用产生严重的影响。

权限APL等级

根据权限对于不同等级应用有不同的开放范围,权限类型对应分为以下三个等级,等级依次提高。

APL级别说明开放范围
normal允许应用访问超出默认规则外的普通系统资源,如配置Wi-Fi信息、调用相机拍摄等。这些系统资源的开放(包括数据和功能)对用户隐私以及其他应用带来的风险低。APL等级为normal及以上的应用。
system_basic允许应用访问操作系统基础服务(系统提供或者预置的基础功能)相关的资源,如系统设置、身份认证等。这些系统资源的开放对用户隐私以及其他应用带来的风险较高。- APL等级为system_basic及以上的应用。- 部分权限对normal级别的应用受限开放,这部分权限在本指导中描述为“受限开放权限”。
system_core涉及开放操作系统核心资源的访问操作。这部分系统资源是系统最核心的底层服务,如果遭受破坏,操作系统将无法正常运行。- APL等级为system_core的应用。- 仅对系统应用开放。
  • 每次执行需要目标权限的操作时,应用都必须检查自己是否已经具有该权限。

    如需检查用户是否已向您的应用授予特定权限,可以使用checkAccessToken()函数,此方法会返回Promise<[GrantStatus]>:PERMISSION_GRANTED或PERMISSION_DENIED。

  • 每次访问受目标权限保护的接口之前,都需要使用requestPermissionsFromUser()接口请求相应的权限。

    用户可能在动态授予权限后通过系统设置来取消应用的权限,因此不能将之前授予的授权状态持久化。

  • user_grant权限授权要基于用户可知可控的原则,需要应用在运行时主动调用系统动态申请权限的接口,系统弹框由用户授权,用户结合应用运行场景的上下文,识别出应用申请相应敏感权限的合理性,从而做出正确的选择。

  • 系统不鼓励频繁弹窗打扰用户,如果用户拒绝授权,将无法再次拉起弹窗,需要应用引导用户在系统应用“设置”的界面中手动授予权限。

  • API12新增可以二次向用户申请授权requestPermissionOnSetting()

  • 系统权限弹窗不可被遮挡。

    系统权限弹窗不可被其他组件/控件遮挡,弹窗信息需要完整展示,以便用户识别并完成授权动作。

    如果系统权限弹窗与其他组件/控件同时同位置展示,系统权限弹窗将默认覆盖其他组件/控件。

代码示例:点击头像申请访问相机和相册媒体的权限,拒绝后二次点击半模态弹窗按钮向用户进行二次权限申请:
实现思路:
1.点击头像时利用checkAccessToken()检查当前是否已拥有权限,如果有则直接使用,如果没有则使用requestPermissionsFromUser()进行向用户询问权限授权
2.如果用户全部拒绝,下一次使用时则使用requestPermissionOnSetting()进行二次用户询问拉起弹窗(也可以引导用户去设置界面手动授权,在requestPermissionsFromUser()拒绝策略中即可实现)

//校验权限工具类

import { abilityAccessCtrl,Context, bundleManager, common, Permissions } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
/**
 * 校验当前是否已经授权(已封装成工具类,如使用三层架构则可放入common中)
 * @param permission
 * @returns
 */
export async function checkPermissionGrant(permission: Permissions): Promise<abilityAccessCtrl.GrantStatus> {
  let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
  let grantStatus: abilityAccessCtrl.GrantStatus = abilityAccessCtrl.GrantStatus.PERMISSION_DENIED;
  // 获取应用程序的accessTokenID
  let tokenId: number = 0;
  try {
    let bundleInfo: bundleManager.BundleInfo = await bundleManager.getBundleInfoForSelf(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
    let appInfo: bundleManager.ApplicationInfo = bundleInfo.appInfo;
    tokenId = appInfo.accessTokenId;
  } catch (error) {
    const err: BusinessError = error as BusinessError;
    console.error(`Failed to get bundle info for self. Code is ${err.code}, message is ${err.message}`);
  }
  // 校验应用是否被授予权限
  try {
    grantStatus = await atManager.checkAccessToken(tokenId, permission);
  } catch (error) {
    const err: BusinessError = error as BusinessError;
    console.error(`Failed to check access token. Code is ${err.code}, message is ${err.message}`);
  }

  return grantStatus;
}
/**
 *向用户二次询问拉起弹窗授权
 */
export function reqPermissionTwice(permissions: Array<Permissions>, context: common.UIAbilityContext): Promise<Array<abilityAccessCtrl.GrantStatus>> {
  let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
  return atManager.requestPermissionOnSetting(context, permissions)
}

//主页面(其中涉及一些工具类未提供,不影响本次示例功能实现,布局会有影响。项目代码还在开发中,之后会开源)

import {AppStorageEnum,NavigationBar,HdUser,hdHttp,cameraCapture,HdLoadingDialo,logger,authStore,reqPermissionTwice,checkPermissionGrant} from "@ss/basic"
import { fileIo, picker } from '@kit.CoreFileKit'
import { BusinessError, request } from '@kit.BasicServicesKit'
import { promptAction } from '@kit.ArkUI'
import { router } from '@kit.ArkUI'
import { abilityAccessCtrl, common, Permissions } from '@kit.AbilityKit'

@Component
export struct MineIndexView {
  pageInfos: NavPathStack = new NavPathStack()
  @StorageProp(AppStorageEnum.TOP_AVOID_HEIGHT)
  avoidTopHeight: number = 0
  @StorageProp(AppStorageEnum.LOGIN_USER)
  loginUser: HdUser = {} as HdUser
  @State isShowSheet: boolean = false
  dialog: CustomDialogController = new CustomDialogController({
    builder: HdLoadingDialog({ message: '更新中...' }),
    customStyle: true,
    alignment: DialogAlignment.Center
  })
  permissions: Array<Permissions> = ['ohos.permission.CAMERA', 'ohos.permission.WRITE_MEDIA'];
  isGrantFirst: boolean = false

  aboutToAppear(): void {
    console.log(JSON.stringify(this.loginUser))
    // this.checkPermissions()
  }

  async checkPermissions(permission: Permissions): Promise<void> {
    //通过checkAccessToken校验本次usergrant是否已授权
    let grantStatus: abilityAccessCtrl.GrantStatus = await checkPermissionGrant(permission);

    //根据返回结果处理业务
    const currentPermission: Array<Permissions> = [permission]
    if (grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) { //授权
      logger.info("已授权")
      if (currentPermission[0] == this.permissions[0]) {
        this.updateAvatarForCamera()
      } else {
        this.updateAvatarForPhotos()
      }
    } else { //未授权
      logger.info("未授权")
      if (this.isGrantFirst) { //因为二次询问授权之前必须调用一次requestPermissionsFromUser
        //二次向用户申请授权api12拥有
        reqPermissionTwice(currentPermission, getContext(this) as common.UIAbilityContext)
          .then((data: Array<abilityAccessCtrl.GrantStatus>) => {
            if (data[0] == abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
              if (currentPermission[0] == this.permissions[0]) {
                this.updateAvatarForCamera()
              } else {
                this.updateAvatarForPhotos()
              }
            }
          })
          .catch((err: BusinessError) => {
            console.error('data:' + JSON.stringify(err));
          });
      }
    }
  }

  //询问用户是否授权,只会调起一次弹窗
  reqPermissionsFromUser(permissions: Array<Permissions>, context: common.UIAbilityContext): void {
    let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
    // requestPermissionsFromUser会判断权限的授权状态来决定是否唤起弹窗
    atManager.requestPermissionsFromUser(context, permissions).then((data) => {
      let grantStatus: Array<number> = data.authResults;
      let length: number = grantStatus.length;
      let count: number = 0
      for (let i = 0; i < length; i++) {
        count = count + 1
        if (grantStatus[i] === 0) {
          // 用户授权,可以继续访问目标操作
          this.isShowSheet = true
        } else {
          // 用户拒绝授权,提示用户必须授权才能访问当前页面的功能,并引导用户到系统设置中打开相应的权限
          if(count == length)
            this.isShowSheet = false
        }
      }
      // 授权成功
    }).catch((err: BusinessError) => {
      console.error(`Failed to request permissions from user. Code is ${err.code}, message is ${err.message}`);
    })
  }

  /**
   * 唤起相机上传图片
   */
  async updateAvatarForCamera() {
    //调用相机唤起工具类
    let URI = await cameraCapture(getContext(this) as common.UIAbilityContext)
    if (URI != undefined) {
      // 2. 存储到沙箱中(fileIo在API9之后就废弃了,使用fs替换)
      const context = getContext(this)
      const fileType = 'jpg'
      const fileName = Date.now() + '.' + fileType
      const copyFilePath = context.cacheDir + '/' + fileName
      const file = fileIo.openSync(URI, fileIo.OpenMode.READ_ONLY)
      fileIo.copyFileSync(file.fd, copyFilePath)
      // 3. 准备请求配置
      const config: request.UploadConfig = {
        url: hdHttp.baseURL + 'userInfo/avatar',
        method: 'POST',
        header: {
          'Accept': '*/*',
          'Authorization': `Bearer ${this.loginUser.token}`,
          'Content-Type': 'multipart/form-data'
        },
        files: [
          {
            name: 'file', //multipart提交时,表单项目的名称,缺省为file
            uri: `internal://cache/` + fileName, //仅支持"internal"协议类型,"internal://cache/"为应用的私有目录,是必填字段
            type: fileType, //文件的内容类型,默认根据文件名或路径的后缀获取。
            filename: fileName //multipart提交时,请求头中的文件名。
          }
        ],
        data: []
      }
      // 4. 开始上传
      this.dialog.open() //自定义加载弹窗
      request.uploadFile(context, config, (err, data) => {
        if (err) {
          return logger.error('UPLOAD', err.message)
        }
        data.on('complete', () => {
          // 5. 更新头像
          hdHttp.get<HdUser>('userInfo').then(res => {
            this.loginUser.avatar = res.data.avatar
            authStore.setUser(this.loginUser)
            promptAction.showToast({ message: '更新头像成功' }) //提示弹窗
            this.dialog.close()
          })
        })
      })
    }
  }

  /**
   * 唤起相册上传图片
   */
  updateAvatarForPhotos() {
    const photoSelectOptions = new picker.PhotoSelectOptions()
    photoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE
    photoSelectOptions.maxSelectNumber = 1

    const photoViewPicker = new picker.PhotoViewPicker()
    photoViewPicker.select(photoSelectOptions).then(result => {
      // 1. 得到文件路径
      const URI = result.photoUris[0]
      // 2. 存储到沙箱中(fileIo在API9之后就废弃了,使用fs替换)
      const context = getContext(this)
      const fileType = 'jpg'
      const fileName = Date.now() + '.' + fileType
      const copyFilePath = context.cacheDir + '/' + fileName
      const file = fileIo.openSync(URI, fileIo.OpenMode.READ_ONLY)
      fileIo.copyFileSync(file.fd, copyFilePath)
      // 3. 准备请求配置
      const config: request.UploadConfig = {
        url: hdHttp.baseURL + 'userInfo/avatar',
        method: 'POST',
        header: {
          'Accept': '*/*',
          'Authorization': `Bearer ${this.loginUser.token}`,
          'Content-Type': 'multipart/form-data'
        },
        files: [
          {
            name: 'file', //multipart提交时,表单项目的名称,缺省为file
            uri: `internal://cache/` + fileName, //仅支持"internal"协议类型,"internal://cache/"为应用的私有目录,是必填字段
            type: fileType, //文件的内容类型,默认根据文件名或路径的后缀获取。
            filename: fileName //multipart提交时,请求头中的文件名。
          }
        ],
        data: []
      }
      // 4. 开始上传
      this.dialog.open() //自定义加载弹窗
      request.uploadFile(context, config, (err, data) => {
        if (err) {
          return logger.error('UPLOAD', err.message)
        }
        data.on('complete', () => {
          // 5. 更新头像
          hdHttp.get<HdUser>('userInfo').then(res => {
            this.loginUser.avatar = res.data.avatar
            authStore.setUser(this.loginUser)
            promptAction.showToast({ message: '更新头像成功' }) //提示弹窗
            this.dialog.close()
          })
        })
      })
    })
  }

  @Builder
  builderMenuBack() {
    Button({ type: ButtonType.Circle }) { //设置按钮的类型未圆形按钮
      Image($r("app.media.left"))
    }.onClick(() => {
      router.back()
    })
    .width(25)
    .height(25)
    .backgroundColor('rgba(255, 255, 255, 0)')
  }

  @Builder
  builderMenuMail() {
    Button({ type: ButtonType.Circle }) { //设置按钮的类型未圆形按钮
      Image($r("app.media.mail"))
    }.onClick(() => {
      this.pageInfos.pushPathByName("MailCenterView", false);
    })
    .width(25)
    .height(25)
    .backgroundColor('rgba(255, 255, 255, 0)')
  }

  /**
   * 半模态对话框
   */
  @Builder
  buildAvatarSheet() {
    Column() {
      Text("拍照")
        .width('100%')
        .height('50%')
        .lineHeight('45%')
        .textAlign(TextAlign.Center)
        .backgroundColor('#e5e7ed')
        .onClick(() => {
          this.isShowSheet = !this.isShowSheet
          this.checkPermissions(this.permissions[0])
        })

      Divider().color(Color.White)

      Text("从相册选择")
        .width('100%')
        .height('50%')
        .lineHeight('45%')
        .textAlign(TextAlign.Center)
        .backgroundColor('#e5e7ed')
        .onClick(() => {
        //并非多次一举,再调用reqPermissionsFromUser之前如果打开半模态化转场会阻塞主线程(原因不知)
          if(!this.isGrantFirst) {
                this.isGrantFirst = true
                this.reqPermissionsFromUser(this.permissions, getContext(this) as common.UIAbilityContext)
              }else{
                this.isShowSheet = !this.isShowSheet
              }
        })

    }
    .width('100%')
    .height(150)
  }

  build() {
    Navigation(this.pageInfos) {
      Column() {
        NavigationBar({
          buildButtonBackParam: () => {
            this.builderMenuBack()
          },
          buildMenuOneParam: () => {
            this.builderMenuMail()
          }
        })

        Flex({ alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center, direction: FlexDirection.Column }) {
          Image($rawfile('kyrie.jpg'))
            .id("mine")
            .width(90)
            .height(90)
            .clip(true)
            .borderRadius(45)
            .margin({ top: 45 })
            .geometryTransition("avatar")
            .transition(TransitionEffect.opacity(0.99))
            .onClick(() => {
              this.isShowSheet = !this.isShowSheet
              if(!this.isGrantFirst)
                this.reqPermissionsFromUser(this.permissions, getContext(this) as common.UIAbilityContext)
            })

          Text(this.loginUser.username)
            .fontWeight(700)
            .fontSize(16)
            .margin({ top: 8 })

          Button({ type: ButtonType.Capsule, stateEffect: false }) {
            Text("终身大会员")
              .fontColor("#7f8085")
              .fontSize(12)
          }
          .width(150)
          .height(32)
          .margin({ top: 8 })
          .backgroundColor("#dbdce1")
        }
        .bindSheet(this.isShowSheet, this.buildAvatarSheet(), {
          height: 150,
          dragBar: false,
          showClose: false,
          onDisappear: () => {
            this.isShowSheet = false
          }
        })
        .width("90%")
        .height(200)

      }
      .width('100%')
      .transition(TransitionEffect.asymmetric(
        TransitionEffect.opacity(0.99),
        TransitionEffect.OPACITY
      ))
      .height('100%')
    }
    .backgroundColor("#e4e5ea")

  }
}

效果图:
1.第一次点击头像
在这里插入图片描述
2.假如全部拒绝,第二次访问继续询问(具体业务权限)
在这里插入图片描述

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

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

相关文章

【大数据算法】一文掌握大数据算法之:排序链表搜索的亚线性算法。

排序链表搜索的亚线性算法 1、引言2、平面图直径问题的亚线性算法2.1 定义2.2 核心原理2.2.1 跳表2.2.2 跳跃搜索2.2.3 分块搜索 2.3 应用场景2.4 算法公式2.5 代码示例 3、总结 1、引言 小屌丝&#xff1a;鱼哥&#xff0c;这茶味道怎么样&#xff1f; 小鱼&#xff1a;嗯&am…

计算机毕业设计选题推荐-保险业务管理系统-Java/Python项目实战

✨作者主页&#xff1a;IT研究室✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Python…

[CUDA编程] --- cuda线程模型

1 核函数 先看一个cuda版本的hello world #include <stdio.h>__global__ void helloworld() {printf("hello world\n"); }int main() {helloworld()<<<1, 1>>>();cudaDeviceSynchronize();return 0; }这里helloworld()<<<1, 1>…

旅行达人必备!有道翻译和这三款神器,轻松走遍世界

在如今的全球化和科技迅猛发展的时代&#xff0c;翻译工具在我们的日常生活中发挥着越来越重要的作用。在各种格式数据的翻译当中&#xff0c;我们就可以发现各种类型的翻译工具纷纷崭露头角。今天就分享三款除了有道翻译外的好用翻译工具&#xff0c;希望可以解决大家翻译的需…

虚幻5|暴击攻击和释放技能,造成伤害

玩家数据的Actor组件制作&#xff1a;虚幻5|制作玩家血量&#xff0c;体力-CSDN博客 造成伤害时&#xff0c;显示暴击及暴击字体颜色和未暴击的字体颜色&#xff0c;还有释放技能连击 一.编辑暴击数据 1.打开之前创建的玩家数据Actor组件 创建一个浮点变量&#xff0c;命名…

从法律风险的角度来看,项目经理遇到不清楚或不明确问题时的处理

大家好&#xff0c;我是不会魔法的兔子&#xff0c;在北京从事律师工作&#xff0c;日常分享项目管理风险预防方面的内容。 序言 在项目开展过程中&#xff0c;有时候会遇到一些不清楚或不明确的状况&#xff0c;但碍于项目进度的紧迫性&#xff0c;不得不硬着头皮做决策&…

喜羊羊教你(如何应对突发的技术故障和危机?)

开发团队如何应对突发的技术故障和危机&#xff1f; 在数字化时代&#xff0c;软件服务的稳定性至关重要。、8月19日下午&#xff0c;网易云音乐疑似出现服务器故障&#xff0c;网页端出现502 Bad Gateway 报错&#xff0c;且App也无法正常使用。 怀疑了自己的电脑、自己的手…

OpenStack 常见模块(二)

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:Linux运维老纪的首页…

MFC工控项目实例之七点击下拉菜单弹出对话框

承接专栏《MFC工控项目实例之六CFile添加菜单栏》 1、在SEAL_PRESSUREDlg.h文件中添加代码 class CSEAL_PRESSUREDlg : public CDialog { ...afx_msg void OnTypeManage(); ... } 2、在SEAL_PRESSUREDlg.cpp文件中添加代码 BEGIN_MESSAGE_MAP(CSEAL_PRESSUREDlg, CDialog)//…

如何使用ssm实现基于Java的学生信息管理系统的设计与实现

TOC ssm165基于Java的学生信息管理系统的设计与实现jsp 绪论 1.1 研究背景 当前社会各行业领域竞争压力非常大&#xff0c;随着当前时代的信息化&#xff0c;科学化发展&#xff0c;让社会各行业领域都争相使用新的信息技术&#xff0c;对行业内的各种相关数据进行科学化&a…

python-随机序列(赛氪OJ)

[题目描述] 小理的作业太多了&#xff0c;怎么也做不完。 小理的数学作业由 T 张试卷组成&#xff0c;每张试卷上有 n 个数 a1..n​ &#xff0c;小理需要算出这些数的极差和方差。极差是一个整数&#xff0c;方差是一个浮点数&#xff0c;要求保留到小数点后 3 位。虽然题目很…

iPhone 手机使用技巧:iPhone 数据恢复软件

无论是由于意外删除、系统崩溃还是软件更新&#xff0c;丢失 iPhone 上的数据都是一场噩梦。从珍贵的照片到重要的工作文件&#xff0c;这种损失可能会让人感到毁灭性。值得庆幸的是&#xff0c;几个 iPhone 数据恢复软件选项可以帮助您找回丢失的文件。这些工具提供不同的功能…

大学数据库系统原理 Mysql数据库实验记录

软件版本说明&#xff1a; 1.Mysql数据库&#xff1a;sql server8.0 2.命令实现使用以及数据库可视化查看&#xff1a;Navicat 16 #不用Mysql Command Line 的原因是不喜欢那个黑框&#xff0c;也不常用&#xff0c;使用Navicat的MYSQL命令列界面是一样的 另外说明 实现相同…

Junit单元测试笔记

常用mock类框架 在软件测试和开发过程中&#xff0c;Mock框架扮演着至关重要的角色&#xff0c;它们允许开发者模拟对象的行为&#xff0c;以便在不需要实际依赖的情况下进行测试。以下是一些常用的Mock框架&#xff1a; MockitoPowerMockEasyMockJMockSpock 初始化mock/spy…

解决ONENOTE复制文字到外部为图片(Ditto)

默认情况下&#xff0c;在ONENOTE中记录的文字&#xff0c;在复制粘贴到外部时&#xff0c;会成为一张图片格式 如下图这段文字&#xff0c;粘贴到QQ中变为了图片 解决办法&#xff1a;安装Ditto Ditto下载链接 点击Download下载 双击安装.exe&#xff0c;选择安装路径后&…

JVM上篇:内存与垃圾回收篇-07-方法区

笔记来源&#xff1a;尚硅谷 JVM 全套教程&#xff0c;百万播放&#xff0c;全网巅峰&#xff08;宋红康详解 java 虚拟机&#xff09; 文章目录 7. 方法区7.1. 栈、堆、方法区的交互关系7.2. 方法区的理解7.2.1. 方法区在哪里&#xff1f;7.2.2. 方法区的基本理解7.2.3. HotSp…

编译 wolfssl 库

wolfssl github: https://github.com/wolfSSL/wolfssl 编译 .lib 或者 .dll wolfssl 很好的提供了 win32 的工程》sln 文件 这样就不用折腾 CMakeLists 文件了&#xff0c;使用 Visual Studio 打开 sln 文件后&#xff0c;设置好 Static 编译库即可&#xff0c;开箱即用 编译 .…

项目开始后,拒绝客户提出的新需求是否会违约?

大家好&#xff0c;我是不会魔法的兔子&#xff0c;在北京从事律师工作&#xff0c;日常分享项目管理风险预防方面的内容。 序言 当一个项目已经开展后&#xff0c;对于项目组而言&#xff0c;最难以忍受的可能要数需求突然发生变化了&#xff0c;尤其是在项目已经进行一半或…

【Netty】Netty时间轮实践与源码解析

目录 定时任务JDK定时任务Timer原理 ScheduledThreadPoolExecutor 时间轮算法netty时间轮架构 netty时间轮 源码解析基本使用HashedWheelTimer 初始化createWheel 创建HashedWheelBucket数组 newTimeout 添加任务执行任务时间轮的优缺点 品一品优秀设计实际的生产环境选择 定时…

css中块,行内块,行内元素转换

参考 元素作用范围 块元素 会在显示时自动换行&#xff0c;例如p标签div等 行内元素 例如span&#xff0c;可以认为是一个不换行的块&#xff0c;其他还有label等 行内块元素 例如img标签显示图片&#xff0c;但不换行 区别 块元素可以设置宽高&#xff0c;但行元素不…