HarmonyOS开发实战( Beta5版)优化实践/合理使用缓存提升性能

news2024/9/27 17:21:56

简介

随着应用功能的日益丰富与复杂化,数据加载效率成为了衡量应用性能的重要指标。不合理的加载策略往往导致用户面临长时间的等待,这不仅损害了用户体验,还可能引发用户流失。因此,合理运用缓存技术变得尤为重要。 系统提供了Preferences、数据库、文件、AppStorage等缓存方式,开发者可以对应用数据先进行缓存,再次加载数据时优先展示缓存数据,减少加载时间,从而提升用户体验。本文将介绍如何通过缓存技术优化应用性能:针对网络数据和地址数据等内容,采用缓存策略,加速数据访问速度,提升应用性能。

问题场景

在应用启动流程中,开发者往往会遇到冷启动完成时延长的问题。这是由于大部分应用的首页数据依赖于网络请求或定位服务等方式来获取相应数据。如果网络、位置服务等信号差,就会导致应用请求网络和位置数据耗时变长,从而在页面冷启动过程中出现较长时间的白屏或白块现象。例如,在需要冷启动时即时加载首页地址数据的场景下,如果应用每次冷启动都通过getCurrentLocation获取位置数据,特别是在信号较弱的区域,这可能导致显著的延迟,迫使用户等待较长时间才能获取到所需的位置信息,从而极大地影响了应用的冷启动体验。针对上述问题,下面将通过使用缓存减少首次数据加载展示时间,优化应用启动性能,为开发者优化应用性能提供参考。

优化示例

在介绍示例前,先给开发者简单介绍一些冷启动首页中常用的缓存使用流程。

图1 三种常用的缓存使用流程

reasonable_using_cache_improve_performance_flow_chart

图1是三种常用的缓存使用流程。常用流程1的详细过程如下:

1.应用冷启动时,读取缓存。

2.判断是否有缓存数据。

3.如果本地没有缓存数据,则需要通过网络、位置服务等方式请求相应数据,然后把数据刷新到首页,同时异步更新缓存数据。

4.如果本地有缓存数据,则把缓存数据先刷新到应用首页,然后异步请求数据进行页面二刷,并更新缓存数据。

常用流程2和1的过程类似,只是常用流程2中省略了异步请求数据进行页面二刷并更新缓存的步骤。而常用流程3和2相比,常用流程3只是在本地有缓存数据时,增加了对缓存数据是否失效的处理。如果缓存数据没有失效,则把缓存数据刷新到应用首页。如果缓存数据已经失效,则需要重新请求数据,然后刷新到首页并更新缓存。

上述缓存使用流程仅为开发者提供参考,实际开发中需结合具体业务场景与需求进行灵活的调整与优化。下面将介绍缓存网络数据和缓存地址数据两个场景示例,并进行性能分析对比。

场景1缓存网络数据

图2 使用本地缓存首页数据流程图

reasonable_using_cache_improve_performance_network_flow_chart

图2是使用本地缓存首页数据的流程图。使用本地缓存优先展示冷启动首页数据,可以减少首帧展示完成时延,减少用户可见白屏或白块时间,提升用户的冷启动体验。

说明:

应用需根据自身对于数据的时效性要求,来决定是否使用缓存数据。例如时效性要求为一天时,一天前保存的缓存数据就不适合进行展示,需从网络获取新数据进行展示,并更新本地缓存数据。

下面是一个缓存网络数据的场景示例。示例中应用首页需展示一张从网站获取的图片信息,在aboutToAppear()中发起网络请求,待数据返回解析后展示在首页上。之后将图片信息缓存至本地应用沙箱内,再次冷启动时首先从沙箱内获取图片信息。若存在,即可解析并展示,在网络请求返回时再次更新图片信息。

以下为关键示例代码,源码参考:

import { http } from '@kit.NetworkKit';
import { image } from '@kit.ImageKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { abilityAccessCtrl, common, Permissions } from '@kit.AbilityKit';
import { fileIo as fs } from '@kit.CoreFileKit';

const PERMISSIONS: Array<Permissions> = [
  'ohos.permission.READ_MEDIA',
  'ohos.permission.WRITE_MEDIA'
];
AppStorage.link('net_picture');
PersistentStorage.persistProp('net_picture', '');

@Entry
@Component
struct Index {
  @State image: PixelMap | undefined = undefined;
  @State imageBuffer: ArrayBuffer | undefined = undefined; // 图片ArrayBuffer

  /**
   * 通过http的request方法从网络下载图片资源
   */
  async getPicture() {
    http.createHttp()
      .request('https://www.example1.com/POST?e=f&g=h',
        (error: BusinessError, data: http.HttpResponse) => {
          if (error) {
            return;
          }
          // 判断网络获取到的资源是否为ArrayBuffer类型
          if (data.result instanceof ArrayBuffer) {
            this.imageBuffer = data.result as ArrayBuffer;
          }
          this.transcodePixelMap(data);
        }
      )
  }

  /**
   * 使用createPixelMap将ArrayBuffer类型的图片装换为PixelMap类型
   * @param data:网络获取到的资源
   */
  transcodePixelMap(data: http.HttpResponse) {
    if (http.ResponseCode.OK === data.responseCode) {
      const imageData: ArrayBuffer = data.result as ArrayBuffer;
      // 通过ArrayBuffer创建图片源实例。
      const imageSource: image.ImageSource = image.createImageSource(imageData);
      const options: image.InitializationOptions = {
        'alphaType': 0, // 透明度
        'editable': false, // 是否可编辑
        'pixelFormat': 3, // 像素格式
        'scaleMode': 1, // 缩略值
        'size': { height: 100, width: 100 }
      }; // 创建图片大小

      // 通过属性创建PixelMap
      imageSource.createPixelMap(options).then((pixelMap: PixelMap) => {
        this.image = pixelMap;
        setTimeout(() => {
          if (this.imageBuffer !== undefined) {
            this.saveImage(this.imageBuffer);
          }
        }, 0)
      });
    }
  }

  /**
   * 保存ArrayBuffer到沙箱路径
   * @param buffer:图片ArrayBuffer
   * @returns
   */
  async saveImage(buffer: ArrayBuffer | string): Promise<void> {
    const context = getContext(this) as common.UIAbilityContext;
    const filePath: string = context.cacheDir + '/test.jpg';
    AppStorage.set('net_picture', filePath);
    const file = await fs.open(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
    await fs.write(file.fd, buffer);
    await fs.close(file.fd);
  }

  async useCachePic(): Promise<void> {
    if (AppStorage.get('net_picture') !== '') {
      // 获取图片的ArrayBuffer
      const imageSource: image.ImageSource = image.createImageSource(AppStorage.get('net_picture'));
      const options: image.InitializationOptions = {
        'alphaType': 0, // 透明度
        'editable': false, // 是否可编辑
        'pixelFormat': 3, // 像素格式
        'scaleMode': 1, // 缩略值
        'size': { height: 100, width: 100 }
      };
      imageSource.createPixelMap(options).then((pixelMap: PixelMap) => {
        this.image = pixelMap;
      });
    }
  }

  async aboutToAppear(): Promise<void> {
    const context = getContext(this) as common.UIAbilityContext;
    const atManager = abilityAccessCtrl.createAtManager();
    await atManager.requestPermissionsFromUser(context, PERMISSIONS);
    this.useCachePic(); // 从本地缓存获取数据
    this.getPicture(); // 从网络端获取数据
  }

  build() {
    Column() {
      Image(this.image)
        .objectFit(ImageFit.Contain)
        .width('50%')
        .height('50%')
    }
  }
}

效果对比

下面对优化前后启动性能进行对比分析。分析阶段的起点为启动Ability(即H:void OHOS::AppExecFwk::MainThread::HandleLaunchAbility的开始点),阶段终点为应用首次解析Pixelmap(即H:Napi execute, name:CreatePixelMap, traceid:0x0)后的第一个vsync(即H:ReceiveVsync dataCount: 24bytes now:timestamp expectedEnd:timestamp vsyncId:int的开始点)。

图3 优化前未使用本地缓存 

reasonable_using_cache_improve_performance_network_use_api

图4 优化后使用本地缓存 

reasonable_using_cache_improve_performance_network_use_cache

图3是优化前未使用本地缓存(从网络端获取数据)的耗时,图4是优化后使用本地缓存的耗时,对比数据如下(性能耗时数据因设备版本环境而异,以实测为准):

方案阶段时长(毫秒)
(优化前)未使用本地缓存641.8
(优化后)使用本地缓存68.9

可以看到在使用本地缓存后,应用冷启动时从Ability启动到图片显示的阶段耗时明显减少。

场景2缓存地址数据

下面是一个使用PersistentStorage(持久化存储UI状态)缓存地址数据的场景示例,源码参考。主要步骤如下:

1.通过persistProp初始化PersistentStorage。

2.创建状态变量@StorageLink(MYLOCATION) myLocation,和AppStorage中MYLOCATION双向绑定。

3.应用冷启动时,先判断缓存AppStorage里MYLOCATION值是否为空(UI和业务逻辑不直接访问PersistentStorage中的属性,所有属性访问都是对AppStorage的访问)。

4.如果缓存为空,则从getCurrentLocation获取地址数据,并加载到页面,同时保存到缓存。如果缓存不为空,则直接从缓存获取地址数据,并加载到页面。

说明:

为了方便对比性能差异,本例中未做缓存数据是否失效和页面二刷的业务处理。实际业务开发中冷启动时虽然是优先从缓存获取地址数据进行刷新,但是后面还需要再使用getCurrentLocation获取最新地址数据进行页面二刷,以确保地址数据的准确性。

import { abilityAccessCtrl, common, Permissions } from '@kit.AbilityKit'; // 程序访问控制管理模块
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog, hiTraceMeter } from '@kit.PerformanceAnalysisKit'; // 性能打点模块
import { geoLocationManager } from '@kit.LocationKit'; // 位置服务模块。需要在module.json5中配置ohos.permission.APPROXIMATELY_LOCATION权限。

// 写入与读取缓存位置数据的key值
const MYLOCATION = 'myLocation';
// 定义获取模糊位置的权限
const PERMISSIONS: Array<Permissions> = ['ohos.permission.APPROXIMATELY_LOCATION'];
// 获取上下文信息
const context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
// 初始化PersistentStorage。PersistentStorage用于持久化存储选定的AppStorage属性
PersistentStorage.persistProp(MYLOCATION, '');

@Entry
@Component
struct Index {
  // 创建状态变量@StorageLink(MYLOCATION) myLocation,和AppStorage中MYLOCATION双向绑定
  @StorageLink(MYLOCATION) myLocation: string = '';

  aboutToAppear() {
    // ApiDataTime表示从getCurrentLocation接口获取位置信息的性能打点起始位置。
    hiTraceMeter.startTrace("ApiDataTime", 1);
    // CacheDataTime表示从AppStorage缓存中获取位置信息的性能打点起始位置。
    hiTraceMeter.startTrace("CacheDataTime", 1);
    // 从AppStorage缓存中获取位置信息
    let cacheData = AppStorage.get<string>(MYLOCATION);
    // 缓存中如果有位置信息,则直接从缓存获取位置信息。如果没有,则从getCurrentLocation接口获取位置信息。
    if (cacheData !== '') {
      // 缓存中有位置信息,则从缓存中直接获取位置信息,并结束性能打点
      hiTraceMeter.finishTrace("CacheDataTime", 1);
      AlertDialog.show({
        message: 'AppStorage:' + cacheData,
        alignment: DialogAlignment.Center
      });
    } else {
      // 缓存中没有位置信息,则从接口获取位置信息
      this.apiGetLocation(PERMISSIONS, context);
    }
  }

  /**
   * 从getCurrentLocation接口获取位置信息。用户需要先授权。
   */
  apiGetLocation(permissions: Array<Permissions>, context: common.UIAbilityContext): void {
    // 获取访问控制模块对象
    let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
    // 拉起弹框请求用户授权。requestPermissionsFromUser会判断权限的授权状态来决定是否唤起弹窗
    atManager.requestPermissionsFromUser(context, permissions).then((data) => {
      // 获取相应请求权限的结果。 0表示已授权,否则表示未授权
      let grantStatus: Array<number> = data.authResults;
      let length: number = grantStatus.length;
      for (let i = 0; i < length; i++) {
        // 如果用户已授权模糊位置的权限,则调用getCurrentLocation获取位置信息,并保存到AppStorage
        if (data.permissions[i] === 'ohos.permission.APPROXIMATELY_LOCATION' && grantStatus[i] === 0) {
          // 设置位置请求参数
          let requestInfo: geoLocationManager.CurrentLocationRequest = {
            'priority': geoLocationManager.LocationRequestPriority.FIRST_FIX, // 设置优先级信息。FIRST_FIX表示快速获取位置优先,如果应用希望快速拿到一个位置,可以将优先级设置为该字段。
            'scenario': geoLocationManager.LocationRequestScenario.UNSET // 设置场景信息。UNSET表示未设置场景信息。当scenario取值为UNSET时,priority参数生效,否则priority参数不生效;
          };
          try {
            // 获取当前位置
            geoLocationManager.getCurrentLocation(requestInfo).then((result) => {
              // 获取位置信息后,结束性能打点
              hiTraceMeter.finishTrace("ApiDataTime", 1);
              let locationData = JSON.stringify(result);
              // 保存到本地缓存
              AppStorage.setOrCreate(MYLOCATION, JSON.stringify(locationData));
              AlertDialog.show({
                message: 'getCurrentLocation:' + locationData,
                alignment: DialogAlignment.Center
              });
            })
              .catch((error: BusinessError) => {
                hilog.error(0x0000, "UseCacheInsteadAddressInquiry", `getCurrentLocation: error= ${error}`);
              });
          } catch (err) {
            hilog.error(0x0000, "UseCacheInsteadAddressInquiry", `err: ${err}`);
          }
        } else {
          // 如果用户未授权,提示用户授权。
          AlertDialog.show({
            message: '用户未授权,请到系统设置中打开应用的位置权限后再试。',
            alignment: DialogAlignment.Center
          });
          return;
        }
      }
    }).catch((err: BusinessError) => {
      hilog.error(0x0000, "UseCacheInsteadAddressInquiry", `failed to request permissions from user. Code is ${err.code} , message is ${err.message}`);
    })
  }

  build() {
    Column() {
      Button('clear cache').onClick(() => {
        // 清除AppStorage缓存中的位置信息
        this.myLocation = '';
        AlertDialog.show({
          message: 'cache cleared',
          alignment: DialogAlignment.Center
        });
      })
    }
    .height('100%')
    .width('100%')
  }
}

效果对比

下面使用DevEco Studio内置的Profiler中的启动分析工具Launch,对使用getCurrentLocation获取地址数据及使用缓存获取地址数据的冷启动性能进行对比分析。本例中通过在aboutToAppear进行起始位置的性能打点,然后在使用本地缓存和使用getCurrentLocation获取到地址数据的位置分别进行结束位置的性能打点来分析两者的性能差异。对比性能前,需要先打开一次应用页面,在弹出位置信息授权弹窗时选择允许授权的选项。

优化前未使用本地缓存(通过getCurrentLocation获取地址数据)的测试步骤:先打开示例页面,点击'clear cache'按钮(清除本地位置信息的缓存)后退出应用,再使用Launch抓取性能数据。

图5 优化前未使用本地缓存

reasonable_using_cache_improve_performance_use_api

优化后使用本地缓存(通过PersistentStorage获取地址数据)的测试步骤:在使用getCurrentLocation获取地址数据后退出应用(本例中在getCurrentLocation获取地址数据数据后会保存到本地缓存),再使用Launch工具抓取性能数据。

图6 优化后使用本地缓存

reasonable_using_cache_improve_performance_use_cache

图5是优化前未使用本地缓存(从getCurrentLocation获取地址数据)的耗时,图6是优化后使用本地缓存(从PersistentStorage获取地址数据)的耗时,对比数据如下(性能耗时数据因设备版本环境而异,以实测为准):

方案阶段时长
(优化前)未使用本地缓存46ms
(优化后)使用本地缓存19μs

由此可见,在冷启动首页需要加载地址数据的场景中,先采用本地缓存策略获取地址数据相比调用getCurrentLocation接口,能显著缩短地址数据的获取时间,减少用户等待,提升冷启动完成时延性能与用户体验。

总结

应用性能优化中,合理使用缓存是提升体验的关键。通过将频繁请求的网络数据或位置信息等缓存起来,可以在下次启动时优先加载缓存数据,避免网络延迟或位置服务信号差导致的白屏或白块现象。这样不仅能减少页面数据加载时间,提升应用性能,还能显著改善用户体验。

最后

小编在之前的鸿蒙系统扫盲中,有很多朋友给我留言,不同的角度的问了一些问题,我明显感觉到一点,那就是许多人参与鸿蒙开发,但是又不知道从哪里下手,因为资料太多,太杂,教授的人也多,无从选择。有很多小伙伴不知道学习哪些鸿蒙开发技术?不知道需要重点掌握哪些鸿蒙应用开发知识点?而且学习时频繁踩坑,最终浪费大量时间。所以有一份实用的鸿蒙(HarmonyOS NEXT)文档用来跟着学习是非常有必要的。 

为了确保高效学习,建议规划清晰的学习路线,涵盖以下关键阶段:


 鸿蒙(HarmonyOS NEXT)最新学习路线

该路线图包含基础技能、就业必备技能、多媒体技术、六大电商APP、进阶高级技能、实战就业级设备开发,不仅补充了华为官网未涉及的解决方案

路线图适合人群:

IT开发人员:想要拓展职业边界
零基础小白:鸿蒙爱好者,希望从0到1学习,增加一项技能。
技术提升/进阶跳槽:发展瓶颈期,提升职场竞争力,快速掌握鸿蒙技术

2.视频学习教程+学习PDF文档

HarmonyOS Next 最新全套视频教程

  纯血版鸿蒙全套学习文档(面试、文档、全套视频等)       

​​

总结

参与鸿蒙开发,你要先认清适合你的方向,如果是想从事鸿蒙应用开发方向的话,可以参考本文的学习路径,简单来说就是:为了确保高效学习,建议规划清晰的学习路线

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

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

相关文章

uniapp组件用法

一. 什么是组件,有什么好处? 在uni-app中&#xff0c;组件是构成应用的基本单位&#xff0c;它们是用来定义用户界面的一部分&#xff0c;并且通常包含了视图和逻辑。组件的设计使得开发者能够以声明式的方式构建应用界面&#xff0c;并且通过组件化的开发方式来提高代码的复…

损失函数、成本函数cost 、最大似然估计

一、损失函数 什么是损失函数&#xff1f; 【深度学习】一文读懂机器学习常用损失函数&#xff08;Loss Function&#xff09;-腾讯云开发者社区-腾讯云 损失函数&#xff08;loss function&#xff09;是用来估量模型的预测值f(x)与真实值Y的不一致程度&#xff0c;它是一个…

Python 新手必看:如何用 unittest 写出高质量代码?

文末赠免费精品编程资料~~ 在 Python中 &#xff0c;unittest 模块是进行单元测试的强大工具。无论你是初学者还是有经验的开发者&#xff0c;单元测试都是确保代码质量的重要一环。而 unittest 模块就是让这一过程变得简单、快捷的利器。 什么是单元测试&#xff1f; 在进入…

浩瀚麦克风怎么样?西圣、罗德、神牛领夹麦克风全网巅峰PK测评

​一台优质专业的无线领夹麦克风能够清晰、稳定地收录声音&#xff0c;提升音频录制质量。而劣质的无线领夹麦克风则可能出现声音不清晰、信号不稳定、续航短等各种问题。作为一名资深的数码测评师&#xff0c;我已经测评过了好几十款无线领夹麦克风&#xff0c;今天将从麦克风…

Python应用指南:获取高德地铁站点数据(单城市版)

书接上文&#xff0c;上篇文章是一次性下载全国所以城市的地铁站点数据&#xff0c;但是可视化的过程需要手动把换乘站给一个个复制出来分配到其他各个经过的线路&#xff0c;还需要核对站点顺序不能出错&#xff0c;如果只需要单个城市的数据呢&#xff1f;另外能不能直接生成…

【复杂系统系列(初级)】自动调节动态平衡模型——生物体的稳态机制

【通俗理解】自动调节动态平衡模型——生物体的稳态机制 关键词提炼 #自动调节 #动态平衡 #生物体稳态 #反馈机制 #体温调节 #微分方程模型 第一节&#xff1a;自动调节动态平衡模型的类比与核心概念 1.1 自动调节动态平衡模型的类比 自动调节动态平衡模型可以被视为生物体…

grpc-spring 通信(监控视频传输)

先看效果 这是微软相机&#xff0c;22ms延迟 &#xff08;不走网络存粹寄存器和内存的通信&#xff09;这是程序抓取摄像头然后传给client&#xff0c;client的java窗口展示的&#xff0c;延时也是22ms&#xff08;对了localhost好像也不走网络吧&#xff09; 几个点 1.openc…

大功率舞台灯调光调色方案 | 支持深度调光,多路输出调光 36V/48V/60V FP7126

在舞台演出中&#xff0c;灯光扮演着非常重要的角色&#xff0c;它不仅可以烘托氛围&#xff0c;营造氛围&#xff0c;更能够为表演者增添光彩&#xff0c;塑造形象。在博物馆场所中&#xff0c;突出展品细节。根据灯光用途和适用类型&#xff0c;舞台灯可以细分为聚光灯、泛光…

foundation model

目录 多模态预训练模型 BLIP LLM GPT3 InstructGPT FLAN chain of thought ToolFormer QWEN Llama3 VLM QWen-VL VideoChat Video-ChatGPT 应用 DriveVLM PlanAgent 多模态预训练模型 CLIP BLIP 《Bootstrapping Language-Image Pre-training for Unified Visi…

突发!Runway 从 HuggingFace 及 GitHub 上删库跑路,背后有何隐情?

突发&#xff01;2024年8月29日Runway 从 HuggingFace 及 GitHub 上删库跑路&#xff0c;背后有何隐情&#xff1f; &#x1f9d0; 今天我们来聊一聊科技圈一则爆炸性消息&#xff1a;Runway ML 从 HuggingFace 和 GitHub 上删库跑路&#xff0c;毫无预警&#xff01;这个举动…

基于asp.net的在线考试系统源码分享

这是一个基于asp.net的开发的在线考试系统&#xff0c;需要的小伙伴自己参考源码&#xff0c;下载链接我放在后面了。 1.主要功能 主要功能包含用户注册登陆、学生登录、老师登陆、试卷管理、分数管理、 填空题选择、选择题管理、题库管理、分数管理、试卷随机生成等等模块。…

【精选】基于Django的智能水果销售系统设计与实现

博主介绍&#xff1a; ✌我是阿龙&#xff0c;一名专注于Java技术领域的程序员&#xff0c;全网拥有10W粉丝。作为CSDN特邀作者、博客专家、新星计划导师&#xff0c;我在计算机毕业设计开发方面积累了丰富的经验。同时&#xff0c;我也是掘金、华为云、阿里云、InfoQ等平台…

面试必问的 Linux 命令帮你整理好啦...

01.背景 作为一名测试工程师&#xff0c;Linux 是必须要掌握的一项技能。 因为大部分的互联网公司的项目都是搭建和部署在 Linux 服务器上的&#xff0c;所以测试就算不去负责搭建和维护测试环境&#xff0c;也需要在测试过程中去服务器那边定位测试中的问题。 所以&#xf…

PTA L1-027 出租

L1-027 出租&#xff08;20分&#xff09; 下面是新浪微博上曾经很火的一张图&#xff1a; 一时间网上一片求救声&#xff0c;急问这个怎么破。其实这段代码很简单&#xff0c;index数组就是arr数组的下标&#xff0c;index[0]2 对应 arr[2]1&#xff0c;index[1]0 对应 arr[0…

这四种人不能合作做生意

合伙创业千万不要和这四种人合伙&#xff0c;不然公司做大了都不是你的&#xff01; 一、不愿出钱的人&#xff0c;不愿出钱就不会有决心。公司一旦有风吹草动&#xff0c;最先跑路的都是没有出钱的。 二、不愿付出时间的人&#xff0c;想用业余时间参与&#xff0c;不愿全身心…

Unity编辑器扩展之Hierarchy面板扩展

内容将会持续更新&#xff0c;有错误的地方欢迎指正&#xff0c;谢谢! Unity编辑器扩展之Hierarchy面板扩展 TechX 坚持将创新的科技带给世界&#xff01; 拥有更好的学习体验 —— 不断努力&#xff0c;不断进步&#xff0c;不断探索 TechX —— 心探索、心进取&#xff…

css-functions-图形函数

概述 本文会讨论和图形函数有关的 5 个函数,分别是:inset、circle、ellipse、polygon和path。这个 5 个图形函数也对应着相应的<basic-shape>。 <basic-shape>是一种表现基础图形的 CSS 数据类型,作用于clip-path或shape-outside属性中;而其值是由图形函数获…

sfud移植

sfud移植 首先看readme文档 文件结构 inc文件夹&#xff1a;各种头文件&#xff0c;注意flash_def和cfg头文件 port文件夹&#xff1a;接口文件 src文件夹&#xff1a;代码源文件 移植 基础&#xff1a;你的SPI没问题&#xff0c;用普通工程可以正常操作Flash 首先打开f…

cloudways相关

服务器优惠链接&#xff1a; https://www.cloudways.com/en/?id1081165 cloudways 后台清缓存位置&#xff1a; 网站迁移到cloudways&#xff08;用cloudways的自动迁移插件&#xff09;&#xff1a; 不管原网站是不是用的cloudways主机&#xff0c;都可以用这个方法。 1…

【React】react项目安装tailwindcss

创建React项目 首先,如果您还没有React项目,可以使用以下命令创建一个新项目: npx create-react-app my-tailwind-app cd my-tailwind-app安装Tailwind CSS 接下来,按照以下步骤安装Tailwind CSS: 安装必要的依赖: npm install -D tailwindcss postcss autoprefixer初始化…