OpenHarmony轻松玩转GIF数据渲染
效果演示


本文将从5个小节来带领大家使用ohos-gif-drawable这一款三方库,其中1、2、3这3个小节,主要介绍了ohos-gif-drawable的核心能力、GIF软解码和GIF绘制。4和5小节主要是扩展讨论,如何添加滤镜效果和软解码遇到的耗时问题。

1.GIF的文件格式理论基础
工欲善其事必先利其器。首先我们需要为自己打下理论基础。了解GIF的数据格式,为后续解码GIF提供理论支持。

通过学习GIF的文件格式,我们对于GIF的组成格式有了一定的了解,并且有助于理解后面GIF的解码。
在开始介绍之前,我想让大家了解一下整体的结构思路如下图:

其中gifuct-js三方库主要完成了解码的工作。
ohos-gif-drawable三方库则是在gifuct-js的三方库之上,进行了封装。并结合了OpenHarmony的Canvas绘制能力,达到了播放和控制GIF的能力。
2.GIF软解码:gifuct-js三方库介绍
GIF解码我们使用了gifuct-js这个库,它是一个纯JavaScript的GIF解码库。首先我们需要了解基础用法。
2.1 参考样例将一个文件ArrayBuffer转换为GIF解码后的帧数据数组。
//javascript
var gif = parseGIF(arraybuffer)
var frames = decompressFrames(gif, true)
2.2 由于OpenHarmony的Image生成PixelMap需要的数据是BGRA数据,而2.1生成的frames所有数组中的patch字段则是RGBA数据,所以我们需要使用
//javascript
var gif = parseGIF(arraybuffer)
var frames = decompressFrames(gif, false)
然后将frame目前还未生成的patch字段数据,通过generatePatch 函数,将RGBA的数据更换为BGRA即可,如下代码所示:
//javascript
const generatePatch = image => {
const totalPixels = image.pixels.length
const patchData = new Uint8ClampedArray(totalPixels * 4)
for (var i = 0; i < totalPixels; i++) {
const pos = i * 4
const colorIndex = image.pixels[i]
const color = image.colorTable[colorIndex] || [0, 0, 0]
patchData[pos] = color[2] // B
patchData[pos + 1] = color[1]// G
patchData[pos + 2] = color[0] // R
patchData[pos + 3] = colorIndex !== image.transparentIndex ? 255 : 0//A
}
return patchData
}
generatePatch函数,在这里会根据颜色表colorTable和基于颜色表的图像数据pixels以及透明度transparentIndex生成BGRA格式的patchData,这个数据和Canvas中getImageData获取的ImageData数据是一致的,都是Uint8ClampedArray类型,可以直接使用putImageData让canvas绘制。
最后,生成的patchData赋值给Frame的patch字段。
这里我们并没有直接使用Canvas的putImageData直接绘制。为了提升扩展性,我们使用了Image的能力来生成PixelMap,这样处理为后续滤镜效果提供了可能,也方便后续绘制流程。
好了,到这里我们就基本上把gifuct-js库的基础使用简单介绍完了。
如何使用GIF:ohos-gif-drawable三方库的介绍。
我们先来看看整个ohos-gif-drawable组件的模型图,通过模型图,我们可以看到,用户只要关注GIFComponent组件,和GIFComponent.ControllerOptions配置参数以及控制参数autoPlay和resetGif即可,非常简单!

1. 支持的功能列表如下
● 支持播放GIF图片。
● 支持控制GIF播放/暂停。
● 支持重置GIF播放动画。
● 支持调节GIF播放速率。
● 支持监听GIF所有帧显示完成后的回调。
● 支持设置显示大小。
● 支持7种不同的展示类型。
● 支持设置显示区域背景颜色。
2. 如何使用ohos-gif-drawable
首先需要使用npm下载ohos-gif-drawable三方库
npm install @ohos/ohos-gif-drawable --save
接下来我们需要配置一个worker给gifuct-js解码使用。
配置worker,在应用工程的entry/src/main/ets/pages目录下新建workers文件夹,并且创建文件 gifParseWorker.ts ,文件内容如下:
import arkWorker from '@ohos.worker';
import { handler } from '@ohos/ohos-gif-drawable/src/main/ets/components/gif/worker/GifWorker'
// handler封装了子线程逻辑,但worker目前只能在entry中进行创建arkWorker.parentPort.onmessage = handler;
然后在entry目录的build-profile.json5文件中,添加如下内容:
"buildOption": {
"sourceOption": {
"workers": [
"./src/main/ets/pages/workers/gifParseWorker.ts"
]
}
},
到这里我们worker就配置好了。
下面就到了正式使用环节,我们只要在UI界面需要的地方写上自定义控件GIFComponent,然后传入GIFComponent.ControllerOptions,gifAutoPlay,gifReset这三个参数就能控制gif动画。
import { GIFComponent, ResourceLoader } from '@ohos/ohos-gif-drawable'
// gif绘制组件用户属性设置
@State model:GIFComponent.ControllerOptions = new GIFComponent.ControllerOptions();
// 是否自动播放
@State gifAutoPlay:boolean = true;
// 重置GIF播放,每次取反都能生效
@State gifReset:boolean = true;
// 在ARKUI的其他容器组件中添加该组件
GIFComponent({model:$model, autoPlay:$gifAutoPlay, resetGif:this.gifReset})
举个简单的例子说明一下
// 创建worker
let worker = new ArkWorker.Worker('entry/ets/pages/workers/gifParseWorker.ts', {type: 'classic',name: 'loadUrlByWorker'})
// 关闭动画
this.gifAutoPlay = false;
// 销毁上一次资源
this.model.destroy();
// 新创建一个modelx,用于配置用户参数
let modelx = new GIFComponent.ControllerOptions()
modelx
// 配置回调动画结束监听,和耗时监听
.setLoopFinish((loopTime) => {
this.gifLoopCount++;
this.loopHint = '当前gif循环了' + this.gifLoopCount + '次,耗时=' + loopTime + 'ms'
})
// 设置组件大小
.setSize({ width: this.compWidth, height: this.compHeight })
// 设置图像和组件的适配类型
.setScaleType(this.scaleType)
// 设置播放速率
.setSpeedFactor(this.speedFactor)
// 设置背景
.setBackgroundColor(Color.Grey)
// 加载网络图片,getContext(this)中的this指向page页面或者组件都可以ResourceLoader.downloadDataWithContext(getContext(this), { url: 'https://pic.ibaotu.com/gif/18/17/16/51u888piCtqj.gif!fwpaa70/fw/700' }, (sucBuffer) => {
// 网络资源sucBuffer返回后处理
modelx.loadBuffer(sucBuffer, () => { console.log('网络加载解析成功回调绘制!')
// 开启自动播放
this.gifAutoPlay = true;
// 给组件数据赋新的用户配置参数,达到后续gif动画效果
this.model = modelx; }, worker)}, (err) => {
// 用户根据返回的错误信息,进行业务处理(展示一张失败占位图、再次加载一次、加载其他图片等)
})
这里ResourceLoader内置了加载网络资源GIF,本地工程资源GIF和本地路径资源GIF文件数据的能力。
如果你已经有了GIF文件的arraybuffer数据,也可以直接调用modelx.loadBuffer(buffer: ArrayBuffer, readyRender: (err?) => void, worker: any)进行GIF播放。
甚至你已经生成了GIF解析数据,比如调用了2.2中的解码代码,那么你也可以直接调用modelx.setFrames(images?: GIFFrame[])来进行gif播放。
1.控制GIF的播放与暂停:
this.gifAutoPlay = true 开启动画
this.gifAutoPlay = false 暂停动画
组件内部会监听该参数的变化,用户只要改变值即可达到控制效果
2. 重置GIF的播放
this.gifReset = !this.gifReset 每次变化都会重置gif播放。
由于重置不需要状态管理,所以组件内监听到数据变化就会重置gif播放
3. 设置GIF动画播放速度
let modelx = new GIFComponent.ControllerOptions()
modelx.setSpeedFactor(2)// 将速率提升到2倍
调用setSpeedFactor(speed: number)即可调整播放速度speed 为对比原始速率的乘积因子,比如设置0.5即为原始速率的0.5倍,设置为2即为原始速率的2倍。
4. 监听GIF动画播放回调(比如第一次动画结束)和获取动画实际播放总时长
let modelx = new GIFComponent.ControllerOptions()
modelx.setLoopFinish((loopTime?) => {
// loopTime为GIF动画一周期耗时,回调时间为GIF动画一周期结束时间节点
})
调用setLoopFinish(fn: (loopTime?) => void)可以通过回调得到GIF动画运行一周期耗时和一周期结束时间节点。
5. 显示GIF任意一帧
let modelx = new GIFComponent.ControllerOptions()
modelx.setSeekTo(5) // 直接展示该gif第5帧图像
调用setSeekTo(gifPosition: number)可以直接展示该gif的某一帧图像。
到这里ohos-gif-drawable三方库的主要能力都介绍完了,是不是很简单呢!
6. 适配组件的大小
let modelx = new GIFComponent.ControllerOptions()
modelx.setScaleType(ScaleType.FIT_CENTER) // 将图像缩放适配组件大小调用setScaleType(scaletype: ScaleType)可以将图像和组件大小进行适配。
目前支持的类型如下图所示:
GIFComponent.ScaleType

为什么要配置worker
在具体实践过程中我们会发现,当我们按下解码按钮的时候,主界面会有一点卡顿的情况。特别是大的GIF文件进行解码的时候效果更明显。这是因为我们在主线程中进行了CPU的密集型计算,这是一个耗时且占用CPU的操作。主线程中是不能执行耗时操作的。但是JavaScript只有一个线程啊?那么解码这一块操作该如何处理会比较好呢?带着疑惑,我去查阅了资料发现JavaScript虽然属于单线程环境。但是通过引入Worker的能力,引入子线程worker,可以实现JavaScript的“多线程”技术。
OpenHarmony如何在子线程中处理耗时任务
为了争取良好的用户体验,我们需要将耗时操作封装至子线程中。
这里简单描述一下worker的能力:
能够让主页面运行的JavaScript线程中加载运行另外单独的一个或者多个JavaScript线程,但是它的多线程编程能力区别于传统意义上的多线程编程。主线程和Worker线程之间,不会共享任何作用域和资源,他们的通信方式是基于事件监听机制的 message。
接下来我们参考OpenHarmony文档下的worker能力
1. OpenHarmony环境下Worker的API接口列表
2. Worker的使用简单案例
经过了解之后,我们可以把解码的耗时封装到worker中处理,避免主线程耗时操作占用CPU导致卡顿问题。提升用户体验。
这也是使用ohos-gif-drawable三方库需要配置worker的原因。
扩展部分
GIF的滤镜效果
1. 灰白滤镜
//javascript
// 重点代码更改
let avg = (color[0] + color[1] + color[2]) / 3
patchData[pos] = avg;
patchData[pos + 1] = avg;
patchData[pos + 2] = avg;
patchData[pos + 3] = colorIndex !== image.transparentIndex ? 255 : 0;
2. 反转滤镜
//javascript
// 重点代码更改
patchData[pos] = 255 - color[0];
patchData[pos + 1] = 255 - color[1];
patchData[pos + 2] = 255 - color[2];
patchData[pos + 3] = colorIndex !== image.transparentIndex ? 255 : 0;
3. 高级滤镜效果
假设我们这边已经拿到了patch: Uint8ClampedArray像素数据,这里我需要先将其变换为一张PixelMap数据,参考GIFComponent中patch数据转换为PixelMap的代码。
//typescript
import image from "@ohos.multimedia.image"
let colorBuffer = patch.buffer
let pixelmap = await image.createPixelMap(colorBuffer, {
'size': {
'height': frame.dims.height as number,
'width': frame.dims.width as number
}
})
4. 高斯模糊
然后对PixelMap像素数据进行高斯模糊, 调用 `blur(pixelmap,10,true, (outPixelMap)=>{ // 模糊后的pixelmap数据})`在回调中获取模糊后的pixelmap。以下是模糊处理的算法:
export async function blur(bitmap: any, radius: number, canReuseInBitmap: boolean, func: AsyncTransform<PixelMap>) {
if (radius < 1) {
func("error,radius must be greater than 1 ", null);
return;
}
let imageInfo = await bitmap.getImageInfo();
let size = {
width: imageInfo.size.width,
height: imageInfo.size.height
}
if (!size) {
func(new Error("fastBlur The image size does not exist."), null)
return;
}
let w = size.width;
let h = size.height;
var pixEntry: Array<PixelEntry> = new Array()
var pix: Array<number> = new Array()
let bufferData = new ArrayBuffer(bitmap.getPixelBytesNumber());
await bitmap.readPixelsToBuffer(bufferData);
let dataArray = new Uint8Array(bufferData);
for (let index = 0; index < dataArray.length; index+=4) {
const r = dataArray[index];
const g = dataArray[index+1];
const b = dataArray[index+2];
const f = dataArray[index+3];
let entry = new PixelEntry();
entry.a = 0;
entry.b = b;
entry.g = g;
entry.r = r;
entry.f = f;
entry.pixel = ColorUtils.rgb(entry.r, entry.g, entry.b);
pixEntry.push(entry);
pix.push(ColorUtils.rgb(entry.r, entry.g, entry.b));
}
let wm = w - 1;
let hm = h - 1;
let wh = w * h;
let div = radius + radius + 1;
let r = CalculatePixelUtils.createIntArray(wh);
let g = CalculatePixelUtils.createIntArray(wh);
let b = CalculatePixelUtils.createIntArray(wh);
let rsum, gsum, bsum, x, y, i, p, yp, yi, yw: number;
let vmin = CalculatePixelUtils.createIntArray(Math.max(w, h));
let divsum = (div + 1) >> 1;
divsum *= divsum;
let dv = CalculatePixelUtils.createIntArray(256 * divsum);
for (i = 0; i < 256 * divsum; i++) {
dv[i]=(i / divsum);
}
yw = yi =0;
let stack = CalculatePixelUtils.createInt2DArray(div,3);
let stackpointer, stackstart, rbs, routsum, goutsum, boutsum, rinsum, ginsum, binsum: number;
let sir: Array<number>;
let r1 = radius +1;
for(y =0; y < h; y++){
rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum =0;
for(i =-radius; i <= radius; i++){
p = pix[yi + Math.min(wm, Math.max(i,0))];
sir = stack[i + radius];
sir[0]=(p &0xff0000)>>16;
sir[1]=(p &0x00ff00)>>8;
sir[2]=(p &0x0000ff);
rbs = r1 - Math.abs(i);
rsum += sir[0]* rbs;
gsum += sir[1]* rbs;
bsum += sir[2]* rbs;
if(i >0){
rinsum += sir[0];
ginsum += sir[1];
binsum += sir[2];
}else{
routsum += sir[0];
goutsum += sir[1];
boutsum += sir[2];
}
}
stackpointer = radius;
for(x =0; x < w; x++){
r[yi]= dv[rsum];
g[yi]= dv[gsum];
b[yi]= dv[bsum];
rsum -= routsum;
gsum -= goutsum;
bsum -= boutsum;
stackstart = stackpointer - radius + div;
sir = stack[stackstart % div];
routsum -= sir[0];
goutsum -= sir[1];
boutsum -= sir[2];
if(y ==0){
vmin[x]= Math.min(x + radius +1, wm);
}
p = pix[yw + vmin[x]];
sir[0]=(p &0xff0000)>>16;
sir[1]=(p &0x00ff00)>>8;
sir[2]=(p &0x0000ff);
rinsum += sir[0];
ginsum += sir[1];
binsum += sir[2];
rsum += rinsum;
gsum += ginsum;
bsum += binsum;
stackpointer =(stackpointer +1)% div;
sir = stack[(stackpointer)% div];
routsum += sir[0];
goutsum += sir[1];
boutsum += sir[2];
rinsum -= sir[0];
ginsum -= sir[1];
binsum -= sir[2];
yi++;
}
yw += w;
}
for(x =0; x < w; x++){
rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum =0;
yp =-radius * w;
for(i =-radius; i <= radius; i++){
yi = Math.max(0, yp)+ x;
sir = stack[i + radius];
sir[0]= r[yi];
sir[1]= g[yi];
sir[2]= b[yi];
rbs = r1 - Math.abs(i);
rsum += r[yi]* rbs;
gsum += g[yi]* rbs;
bsum += b[yi]* rbs;
if(i >0){
rinsum += sir[0];
ginsum += sir[1];
binsum += sir[2];
}else{
routsum += sir[0];
goutsum += sir[1];
boutsum += sir[2];
}
if(i < hm){
yp += w;
}
}
yi = x;
stackpointer = radius;
for(y =0; y < h; y++){
// Preserve alpha channel: ( 0xff000000 & pix[yi] )
pix[yi]=(0xff000000& pix[Math.round(yi)])|(dv[Math.round(rsum)]<<16)|(dv[
Math.round(gsum)]<<8)| dv[Math.round(bsum)];
rsum -= routsum;
gsum -= goutsum;
bsum -= boutsum;
stackstart = stackpointer - radius + div;
sir = stack[stackstart % div];
routsum -= sir[0];
goutsum -= sir[1];
boutsum -= sir[2];
if(x ==0){
vmin[y]= Math.min(y + r1, hm)* w;
}
p = x + vmin[y];
sir[0]= r[p];
sir[1]= g[p];
sir[2]= b[p];
rinsum += sir[0];
ginsum += sir[1];
binsum += sir[2];
rsum += rinsum;
gsum += ginsum;
bsum += binsum;
stackpointer =(stackpointer +1)% div;
sir = stack[stackpointer];
routsum += sir[0];
goutsum += sir[1];
boutsum += sir[2];
rinsum -= sir[0];
ginsum -= sir[1];
binsum -= sir[2];
yi += w;
}
}
let bufferNewData =newArrayBuffer(bitmap.getPixelBytesNumber());
let dataNewArray =newUint8Array(bufferNewData);
let index =0;
for(let i =0; i < dataNewArray.length; i +=4){
dataNewArray[i]= ColorUtils.red(pix[index]);
dataNewArray[i+1]= ColorUtils.green(pix[index]);
dataNewArray[i+2]= ColorUtils.blue(pix[index]);
dataNewArray[i+3]= pixEntry[index].f;
index++;
}
await bitmap.writeBufferToPixels(bufferNewData);
if(func){
func("success", bitmap);
}
}
如果需要高级滤镜效果可以参考ImageKnife组件的transform部分,这里仅仅展示模糊效果。
由于滤镜效果目前ohos-gif-drawable三方库并没有开发接口提供出来,所以开发者可以根据实际需求重写自定义组件GIFComponent.,只需要在生成PixelMap的代码片段中加入滤镜代码,即可利用滤镜效果开发更多精彩的应用。
参考资料
1.《GIF文件格式解析》
https://segmentfault.com/a/1190000022866045
2.GIF解码库gifuct-js
https://github.com/matt-way/gifuct-js
3.GIF解码库底层逻辑jsBinarySchemaParser
https://github.com/matt-way/jsBinarySchemaParser
4.高级滤镜算法借鉴
https://gitee.com/openharmony-tpc/ImageKnife/tree/master/imageknife/src/main/ets/components/imageknife/transform
5.OpenHarmony环境下Worker的API接口列表
https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/reference/apis/js-apis-worker.md
6.Worker的使用简单案例
https://gitee.com/wang_zhaoyong/js_worker_module/wikis/Worker%E7%AE%80%E5%8D%95%E4%BD%BF%E7%94%A8
7.Web Worker API参考
https://developer.mozilla.org/zh-CN/docs/Web/API/Worker
8.OpenHarmony的Canvas文档
https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/reference/arkui-ts/ts-components-canvas-canvas.md
9.OpenHarmony的CanvasRenderingContext2D对象文档
https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/reference/arkui-ts/ts-canvasrenderingcontext2d.md

OpenHarmony轻松玩转GIF数据渲染的更多相关文章
- 完整版的CAD技巧!3天轻松玩转CAD,零基础也能学会
最近有很多小伙伴反应,CAD图纸学起来有点小困难,也许你还没能掌握技巧,CAD大神带你3天轻松玩转CAD,零基础也能快速学会. 一.看懂图纸是关键 CAD制图首先得让自己知道要绘制什么,如果心中对图纸 ...
- 2021 .NET Conf China 主题分享之-轻松玩转.NET大规模版本升级
去年.NET Conf China 技术大会上,我给大家分享了主题<轻松玩转.NET大规模版本升级>,今天把具体分享的内容整理成一篇博客,供大家研究参考学习. 一.先说一下技术挑战和业务背 ...
- 【日常笔记】datatables表格数据渲染
现在有很多表格渲染方式 这里只是记录怎么使用datatables渲染数据 使用datatables可以更方便的来渲染数据 [中文api]http://datatables.club/index.htm ...
- 基于AppCan MAS系统,如何轻松实现移动应用数据服务?
完成一个移动应用开发,前端提供页面展示,当它要与一些业务系统进行交互,又该如何实现呢?2016AppCan移动开发者大会上,AppCan前端开发经理杨庆,分享了AppCan轻松实现移动应用数据服务的方 ...
- json数据渲染表单元素出现的问题
解析页面表单元素 parseForm: function () { var data = {}; $(this).find('input').each(function () { switch ($( ...
- easyUI datagrid 多行多列数据渲染异常缓慢原因以及解决方法
原因 最近,在优化之前公司帮联想(外包)做的一个老的后台管理系统,由于项目是基于easy UI框架,页面是后台用jsp实现的,再加上在公司推行前后端分离的实践,大部分项目都基于vue采用前后端分离去实 ...
- 解决vue数据渲染过程中的闪动问题
关键代码 主要解决vue双大括号{{}}在数据渲染和加载过程中的闪动问题,而影响客服体验. html代码: <span class="tableTitle selftab" ...
- template.js 数据渲染引擎
template.js 数据渲染引擎 template.js是一款JavaScript模板引擎,用来渲染页面的. 原理:提前将Html代码放进编写模板 <script id="tpl& ...
- wpgcms---单片页数据渲染
单片页数据渲染,使用Twig的标签语法: <h1> {{ contentInfo.title }} </h1> {% autoescape false %} {{ conten ...
- vue2.* 目录结构分析 数据绑定 循环渲染数据 数据渲染02
一.目录 结构分析 node_modules:项目依赖文件(也可以说是模块) src:开发时所用的资源 assets:静态资源文件 App.vue:根组件(最基础的公共页面) main.js:实例化v ...
随机推荐
- unrar命令
解压提取RAR压缩文件 语法格式:unrar 参数 压缩包 常用参数 e 将文件解压缩到当前目录 o - 不要覆盖现有文件 l 显示文件列表 p 设置压缩包密码 p 将文件显示到标准输出 r 递归处理 ...
- Mysql 删除binlog日志方法
方法1 RESET MASTER; 解释: 该方法可以删除列于索引文件中的所有二进制日志,把二进制日志索引文件重新设置为空,并创建一个以.000001为后缀新的二进制日志文件. 该语法一般只用在主从环 ...
- mongo重启、远程连接
1.查看当前mongo启动进程 ps -ef | grep mongo 2.修改mongo启动远程连接配制文件 vi /etc/mongod.conf 将 bind_ip=127.0.0.1 这一行注 ...
- 【Azure Notification Hub】创建Notification Hub失败,提示 unrecognized arguments: --sku Free
问题描述 用Azure CLI命令创建 Notification Hub,报错不识别的参数 --Free SKU 问题解答 经测试发现,在创建Notification Hub前,需要创建 Notifi ...
- 【Azure 存储服务】存储在Azure Storage Table中的数据,如何按照条件进行删除呢?
问题描述 如何按条件删除 Storage Table 中的数据,如果Table中有大量的条记录需要删除,Java代码如何按条件删除 Table中的数据(Entity)? (通过Azure Storag ...
- 图查询语言 nGQL 简明教程 vol.01 快速入门
本文旨在让新手快速了解 nGQL,掌握方向,之后可以脚踩在地上借助文档写出任何心中的 NebulaGraph 图查询. 视频 本教程的视频版在B站这里. 准备工作 在正式开始 nGQL 实操之前,记得 ...
- Java 从键盘读入学生成绩 找出最高分 并输出学生等级成绩 * 成绩>=最高分-10 等级为’A‘ * 成绩>=最高分-20 等级为’B‘ * 成绩>=最高分-30 等级为'C' * 其余 等级为’D‘
1 /* 2 * 从键盘读入学生成绩 找出最高分 并输出学生等级成绩 3 * 成绩>=最高分-10 等级为'A' 4 * 成绩>=最高分-20 等级为'B' 5 * 成绩>=最高分- ...
- win10图标异常显示空白,WiFi图标消失等情况解决方案
出现WiFi图标异常不显示,但是网络却正常,以下为解决方案: Win + R 快捷键调出运行框,输入%USERPROFILE%\AppData\Local,找到IconCache.db文件并删除,之后 ...
- 十步带你用IDEA创建一个WEB项目及部署(Tomcat)
部署一个web项目首先需要安装Tomcat,还没安装的朋友们可以看一下我这个博客: https://www.cnblogs.com/deyo/p/17241878.html 第一步:打开Idea-新建 ...
- go程序在mac下的交叉编译
主页 微信公众号:密码应用技术实战 博客园首页:https://www.cnblogs.com/informatics/ 背景 go语言的一大优势就是跨平台,go语言是编译型语言,与Java.C#等语 ...