2023年4月

deflat 脚本学习【去除OLLVM混淆】

deflat脚本链接:
GitHub - cq674350529/deflat: use angr to deobfuscation

deflat 脚本测试

这里以
代码混淆与反混淆学习-第一弹
中的OLLVM 混淆样本为例进行去除。【LLVM-4.0】

控制流平坦前 控制流平坦后
image image

python deflat.py --file main-bcf --addr 0x401180

image

image

deflat.py 成功去除后效果:

image image

去混淆后,效果还算可以,能分析程序流程了。

deflat 脚本分析【angr】

利用符号执行去除控制流平坦化 - 博客 - 腾讯安全应急响应中心 (tencent.com)

angr documentation

利用angr符号执行去除控制流平坦化 - 0x401RevTrain-Tools (bluesadi.github.io)

image

  • 序言:函数的第一个执行的基本块
  • 主(子)分发器:控制程序跳转到下一个待执行的基本块
  • retn块:函数出口
  • 真实块:混淆前的基本块,程序真正执行工作的版块
  • 预处理器:跳转到主分发器

如第一弹中分析:OLLVM 的控制流平坦化是将程序的一般逻辑划分为很多个真实执行的块,然后通过分发器进行链接。其实就是一个Switch结构,每次执行完真实块后,进行预处理,再跳转到主分发器,继续分发,最终达到平坦化的效果。

显然,
去控制流平坦化
就是要找到真实块间的跳转逻辑,打破Switch结构束缚。

具体来说,有如下步骤:

  1. 静态分析CFG得到序言/入口块、主分发器、子分发器/无用块、真实块、预分发器和返回块。
  2. 利用符号执行恢复真实块的前后关系,重建控制流
  3. 根据第二步重建的控制流Patch程序,输出恢复后的可执行文件

静态分析

首先明确:
【以下结论针对OLLVM项目,其他大佬加料的OLLVM混淆还需要单独分析】

  1. 函数的开始地址为序言的地址
  2. 序言的后继为主分发器
  3. 后继为主分发器的块为预处理器
  4. 后继为预处理器的块为真实块
  5. 无后继的块为retn块
  6. 剩下的为无用块

angr 获取类似Ida的 CFG

image

获取真实块、主分发器、预处理器、序言、retn块和无用块

image

获取真实块的细节

image

angr 恢复真实块执行逻辑,重建控制流

利用angr 强大的符号执行功能,找到各真实块的连接逻辑。

image

这里对于两个分支的模拟执行,只需关注
cmov
指令,就可以分别对应得到eax、ecx,然后获得后续真实块。【局限性很大】

image

符号执行
symbolic_execution()
函数,返回后继真实块。

image

Patch程序恢复执行逻辑

image

如此便完成了 deflat脚本的简单处理分析。

小结

分析下来,其实就是定位到所有真实块,然后
利用angr符号执行将真实块间的执行逻辑进行串联
。最后进行patch程序,重建控制流。

但显然存在一些问题,我们默认了如下规则:

  1. 函数的开始地址为序言的地址
  2. 序言的后继为主分发器
  3. 后继为主分发器的块为预处理器
  4. 后继为预处理器的块为真实块
  5. 无后继的块为retn块
  6. 剩下的为无用块

但是在实际去除控制流平坦化过程中,上面的默认思路已经被加混淆的开发者做了处理。

例如:

  • 后继为预处理器的块不一定是真实块;
  • 预处理器不一定存在;
  • 存在分支的真实块跳转的判断逻辑,不一定是
    cmov
    指令;
  • deflat脚本默认模拟执行最多两个分支,但真实情况可能不只两个分支;
  • 可能存在一个
    向前更新的数组,依据程序运行进行更新
    ,决定当前真实块的跳转【这导致
    angr对于该块的模拟执行
    得不到正确的跳转】
  • 程序在加混淆前,已经被添加了花指令或其他处理,
    程序CFG图
    已经被打破;
  • 某个块存在死循环,会使angr符号执行卡死……

这也导致了,这个
deflat脚本的普适性较低
,除了能够处理OLLVM官方项目做的混淆,对加了其他PASS或者处理的混淆,基本用不了。

所以对于去除不了的OLLVM混淆,我们需要
根据程序的实际混淆效果,对deflat脚本进行修改
,再进行去混淆。

【这也要求对deflat 脚本比较熟悉,可以更快上手】

失败的花指令控制流平坦化尝试

使用
代码混淆与反混淆学习-第一弹
中加了花指令的程序,进行OLLVM控制流平坦化混淆,看看效果。

源代码如下:

image

# clang 执行内联汇编加 -fasm-blocks 或者 -fms-extensions 或者 -masm=intel

clang -mllvm -fla -mllvm -split -mllvm -split_num=3 main-call-加花.cpp -lm -fasm-blocks -o main-call-加花

# 需要对源代码作一些修改

存在较大的问题,我的OLLVM 环境是在Ubuntu上搭建的,对于上述内联汇编加的花指令无法编译通过!

【或许可以在
Windows 上移植OLLVM,进行编译(好像挺难的)

image

可以看到,花指令用到的标签、$ 出现报错。

【最终也没解决编译问题,或许本就不可以,ollvm 不具备这样的处理能力,也可能是我代码的问题,
如果博客前的你有任何想法,欢迎与我交流

TSCTF-J 2022-upx_revenge实战分析

TSCTF-J 2022 WP


upx_revenge
题目进行分析。

image

首先直接使用deflat 脚本。

python deflat.py --file upx_revenge_test --addr 0x4016D0

image

发现没有找到retn 块。

处理多个retn块

回到ida 查看cfg 图发现原因:存在其他的退出块。

image

这里需要改进deflat 脚本,使其存在很多retn块。

# 其他位置的retn_node,对应改为list处理
if supergraph.out_degree(node) == 0:
            retn_node.append(node)

image

成功运行,但是去除效果不行。

去除后CFG图
image

多个comv的处理

很明显看出,程序的真实块间的逻辑串联失败,也就是重建控制流失败。

image

产生原因

image
image

显然,这里存在2个分支,因为有两个
cmov
【相同判断】,并且call 函数,对分支跳转是有作用的,这里var_CC是顺序执行,动态更新的。

【deflat 脚本只处理了执行有一个
cmov
指令的情况,且hook了call函数】

【由于var_CC是顺序执行,动态更新也可以看出,deflat 脚本的模拟执行思路已经无法对真实块的后继进行确定了】

但这里做个测试,不hook call 看是什么效果。

image

可以知道,取消hook call 对真实块后继的查找毫无影响,这是因为deflat中的模拟执行,只是基于
comv
处的模拟。对前文并无任何关联。

显然,该deflat脚本的无法处理了。【】

总结

angr


upx_revenge
这道题而言,

deflat 脚本中angr 对局部的模拟执行显然无法获取真实块间的执行顺序,重建控制流显然也无从谈起。当然静态查找各个控制流平坦化的功能块效果还是可以的。

那么如何通过angr,有序的、联系上文地进行模拟执行,获取真实块的执行逻辑,显然是关键点!

【??? 后续学习了,有思路再更新】

unicorn

[
原创]ARM64 OLLVM反混淆-Android安全-看雪论坛-安全社区|安全招聘|bbs.pediy.com (kanxue.com)

Unicorn反混淆:恢复被OLLVM保护的程序(一) - 简书 (jianshu.com)

使用unicorn 模拟执行框架获取真实块间的执行顺序,重建控制流。

【还没学过 unicorn 使用,,,】

ida

使用IDA microcode去除ollvm混淆(上) - 先知社区 (aliyun.com)

GitHub - PShocker/de-ollvm: IDA Python Script for anti ollvm

利用ida 现成的CFG 图,以及idc 脚本,动态运行程序,获取真实块的执行顺序,从而恢复控制流。

前言

这个任务调度模块的实现是形成于毕设项目中的,用在
STM32
中,断断续续跨度2个月实现了一些基本功能,可能后面再做其他项目时会一点点完善起来,也会多学习相关知识来强化模块的实用性和高效性,毕竟用自己自主实现出来的功能还是蛮舒心的。

任务调度模式结构

整体上的结构属于
线性结构
,结合
链表

定时器
来实现,我使用的是
sysTick
这个滴答时钟,
1ms
的频率,功能比较简单,容易理解。

分片

分片的模式,主要体现在
函数分片

时间分片
在我之前就有使用在函数中,主要的思路是,把函数功能
切片
,分为几个小部分,每次执行时
按次序执行小部分
,对于没有
时序
要求的函数来说,可以把一个占用
CPU
大的功能分摊开来实现,从而避免有些地方耗时长的问题。对于
时间分片
,其实就是定时器的一种应用,实际上,
函数分片
在执行的时候已经是一种
时间分片
了,不过现在加上人为的控制在里面了。
下面是
函数分片
的一般结构:

void func(char *fos,...){
    static char step=0;//顺序控制变量,自由度比较高,可乱序,可循环,可延迟执行
    switch(step){
        case 0:{
            //...
            step++;
            break;
        }
        case 1:{
            //...
            step++;
            break;
        }
        //...
        default:{
            //step++;//可以借助default实现延时的效果,即跳过几次空白step
            break;
        }

    }
    return;
}

其中添加的参数变量
*fos

必要的
,因为就是通过传入每个任务的这个标志位来判断是否运行结束,而其他的参数,就得基于具体任务做不一样的处理了。

轮询

  • 运行框图

可以看到这个框图是一个
头尾相连

闭环结构
,从头节点依次运行到尾节点后再从头循环往复执行下去。

  • 轮询函数
void loop_task(void){
	static Task_Obj *tasknode;
	
	tasknode=task_curnode->next;//repoint the curnode to the next
	if(tasknode==NULL){//tasknode is null,only the headnode have the attr
		return;//express the task space is none
	}
	else if(tasknode->task_type==TYPE_HEAD){//tasknode is headnode
		task_curnode=tasknode;
		return;
	}
	else{
		if(tasknode->run_type == RUN_WAIT){
            //等待型任务,通过ready标志来确定是否执行,否则就跳过
			if(!tasknode->ready){
				if(task_curnode->next !=NULL){
					task_curnode=task_curnode->next;
					return;
				}
			}
		}
		if(tasknode->task_status==STATUS_INIT){

			tasknode->tickstart=HAL_GetTick();//获取tick
			tasknode->task_status=STATUS_RUN;

		}
		else if(tasknode->task_status==STATUS_RUN){
			if((HAL_GetTick() - tasknode->tickstart) > (uint32_t)tasknode->task_tick){
				tasknode->task_name(&(tasknode->task_fos));//run the step task,transfer the fos
				tasknode->tickstart+=(uint32_t)tasknode->task_tick;//update the tickstart
			}
		}
		
	}
	if(tasknode->task_fos==FOS_FLAG){
		
		tasknode->ready=0;
		if(tasknode->waittask!=NULL){
            //置位该任务绑定的等待的任务准备运行标志位,标识可以准备运行了
			tasknode->waittask->ready=1;
		}
        //运行结束就删掉该任务
		delete_task(tasknode);
	}
	else if(tasknode->task_fos==FOC_FLAG){
        //循环运行该任务
		tasknode->task_status=STATUS_INIT;//continue running from start
		tasknode->task_fos=0;//RESET fos
		
	}
	if(task_curnode->next !=NULL){
		if(task_curnode->next->run_type==RUN_FORCE) return;//force-type's task
		
		else task_curnode=task_curnode->next;
		
	}
	

}

其中有几个运行态和标志位

#define FOS_FLAG 99//运行结束标志
#define FOC_FLAG 100//运行结束后再次执行,相当于循环运行
#define TYPE_NOMAL 0//标识一般任务类型
#define TYPE_HEAD 1//标识头任务类型
#define TYPE_END 2//标识尾任务类型
#define RUN_NORMAL 0//一般轮询模式
#define RUN_FORCE 1//强制运行该任务,运行结束才继续下一个任务
#define RUN_WAIT 2//等待指定的任务结束,才可以被运行
#define STATUS_INIT 0//任务的准备阶段,用于获取起始时间
#define STATUS_RUN 1//任务运行阶段
#define STATUS_UNVAILED 2//无效状态

运行时对时间间隔
tick
的把握还有点问题,这个等待后面有机会优化下。

调度实现

  • 任务链表结构
typedef struct TASK_CLASS{
	void (*task_name)(char *taskfos,...);//任务函数
	int task_tick;//任务的时间分片间隔
	uint32_t tickstart;//起始时间点,每次执行完须加上一个tick
	char task_fos;//运行结束标志
	char task_type;//任务类型变量
	char task_status;//任务状态
	char run_type;//运行状态
	char ready;//准备运行标志位
	struct TASK_CLASS *next;//下一任务
	struct TASK_CLASS *waittask;//等待执行的任务
} Task_Obj;
  • 添加任务


    • add_task

    void add_task(void (*taskname)(char *,...),int tasktick,int runtype){//可变参,这里未做处理
    Task_Obj *tasknode,*tmpnode;
    char i;
    
    tasknode = (Task_Obj*)malloc(sizeof(Task_Obj));
    
    tasknode->task_name=taskname;
    tasknode->task_tick=tasktick;
    tasknode->task_fos=0;
    tasknode->task_status=STATUS_INIT;//initial status
    tasknode->task_type=TYPE_END; //set the new node to endnode
    tasknode->run_type=runtype;
    tasknode->next=&task_headnode;//the endnode point to the headnode
    
    tmpnode=&task_headnode;
    if(task_num==0){
    	tmpnode->next=tasknode;
    	task_num++;
    	return;
    }
    for(i=0;i<task_num;i++){
    	tmpnode=tmpnode->next;//reach the endnode
    }
    tmpnode->task_type=TYPE_NOMAL;//turn the last endnode to the normal node
    tmpnode->next=tasknode;
    task_num++;
    }
    

    • add_wait_task

    void add_wait_task(void (*taskname)(char *),void (*waitname)(char *),int tasktick){
    Task_Obj *tmpnode,*tasknode;
    char i,pos;
    
    tmpnode=&task_headnode;
    for(i=0;i<task_num;i++){
    	tmpnode=tmpnode->next;//reach the endnode
    	if(tmpnode->task_name==taskname){
    		pos=i;//获取要等待任务的位置
    		break;
    	}
    }
    
    tasknode = (Task_Obj*)malloc(sizeof(Task_Obj));
    
    tasknode->task_name=waitname;
    tasknode->task_tick=tasktick;
    tasknode->task_fos=0;
    tasknode->task_status=STATUS_INIT;//initial status
    tasknode->task_type=TYPE_END; //set the new node to endnode
    tasknode->run_type=RUN_WAIT;//任务为等待运行
    tasknode->ready=0;
    tasknode->next=&task_headnode;//the endnode point to the headnode
    
    tmpnode->waittask=tasknode;//获取新建的等待执行的任务地址,在运行结束后把等待执行的任务的准备运行标志位置1
    
    tmpnode=&task_headnode;
    if(task_num==0){
    	tmpnode->next=tasknode;
    	task_num++;
    	return;
    }
    for(i=0;i<task_num;i++){
    	tmpnode=tmpnode->next;//reach the endnode
    }
    tmpnode->task_type=TYPE_NOMAL;//turn the last endnode to the normal node
    tmpnode->next=tasknode;
    task_num++;
    
    }
    
  • 删除任务


    • delete_task(局限性大,只针对当前运行的任务而言)

    void delete_task(Task_Obj *taskobj){
    if(task_curnode->task_type==TYPE_HEAD && task_num < 2){//if curnode is headnode,and tasknum=1
    	task_curnode->next=NULL;
    }
    else{
    	task_curnode->next=taskobj->next;//repoint the curnode next
    }
    free(taskobj);//free the space of where the taskobj pointed
    
    task_num--;
    
    }
    

    • delete_task_withname(删除指定任务名的任务)

    void delete_task_withname(void (*taskname)(char *)){
    Task_Obj *tmpnode,*tmpnode2;
    char i,pos;
    
    tmpnode=&task_headnode;
    for(i=0;i<task_num;i++){
    	tmpnode=tmpnode->next;//reach the endnode
    	if(tmpnode->task_name==taskname){
    		pos=i;
    		break;
    	}
    }
    if(i==task_num) return;
    tmpnode=&task_headnode;
    for(i=0;i<pos+1;i++){
    	tmpnode2=tmpnode;
    	tmpnode=tmpnode->next;
    }
    if(tmpnode->next==NULL){//if tmpnode is endnode
    	tmpnode2->next=&task_headnode;
    }
    else{
    	tmpnode2->next=tmpnode->next;//repoint the curnode next
    }
    task_num--;
    free(tmpnode);
    }
    
  • 初始化任务空间

void non_task(char *taskfos){
	return;
}

void init_taskspace(void){
	task_headnode.task_name=non_task;
	task_headnode.task_type=TYPE_HEAD;
	task_headnode.task_status=STATUS_UNVAILED;
	task_headnode.next=NULL;
	task_curnode=&task_headnode;//头节点是没有任务需要执行的
	task_num=0;
}
  • 调用实例
add_task(task1,500,RUN_NORMAL);//500ms执行一次task1任务
add_wait_task(task1,task2,500);//task2等待task1结束才会执行,运行的时间间隔为500ms
delete_task_withname(task1);//删除task1任务

while(1){
    //...
    loop_task();//任务轮询
}

结语

整体实现说难不难,说简单不简单,但也是我第一次尝试这种偏向系统级应用的代码,而且都没有参照任何其他的资料和代码,完全以自己的对任务的理解和具体项目的需求来一点点实现,希望后面会把这个调度的代码进一步完善成一个通用型的调度方式,也方便后面项目的使用了。

几天前,Python 开源社区又出了一个不小的新闻:HTTPX 和 Starlette 在同一天将在用的代码分析工具(flake8、autoflake 和 isort)统一替换成了 Ruff。

Ruff 作者的 Twitter

HTTPX
是一个支持异步的 HTTP 客户端,
Starlette
是一个轻量级的 ASGI 框架,它们都是 Python 社区里的明星项目,目前加起来有近 20K star。它们都选择了使用 Ruff,再次扩大了 Ruff 的应用版图。

Ruff 是个诞生仅仅 8 个月的新兴项目,但已呈现出一种席卷 Python 社区的趋势!很多知名的开源项目已采纳 Ruff,比如 Transformers、Pandas、FastAPI、Airflow、SciPy、Bokeh、Jupyter、LangChain、PaddlePaddle、Sphinx、Pydantic、LlamaIndex……

Ruff
是什么?为什么它能吸引大量的开源项目使用?相比于其它代码分析工具,它有哪些突出之处,是否还有一些局限性?现在是否值得将项目在用的工具都替换成它呢?

带着这些问题,本文将带你全方位了解这个火爆的项目。

Ruff 加速 Rust 与 Python 的融合

Ruff 诞生于 2022 年 8 月,它是一个用 Rust 语言编写的高性能的 Python 静态代码分析工具,比其它分析工具快几个数量级(10-100 倍),而且功能也很全面。

从头检测 CPython 代码库的结果对比

代码分析工具
即 Linter,用于检查代码中的语法错误、编码规范问题、潜在的逻辑问题和代码质量问题等,可以提供实时反馈和自动修复建议。

在 Ruff 出现之前,社区里的代码分析工具呈现出百花齐放之势,比如有 Pylint、Flake8、Autoflake、Pyflakes、Pycodestyle 等等,它们的共同点是都使用 Python 编写而成。

Ruff 异军突起,在性能方面立于不败之地,主要得益于 Rust 天然的速度优势。Ruff 的出现,就像基于大语言模型的 ChatGPT 横空出世,所有竞争对手瞬间就黯淡失色了。

两个月前,我翻译了一篇《
Python 2023 年的 3 个趋势
》,它预测的第一个趋势就是 Rust 将加快融入到 Python 相关的项目和工具中,举出的例子就有 Ruff。

我现在可以补充一个观察了:
用 Rust 开发的新工具将淘汰用其它语言开发的工具,而且新工具的普及速度可能比你的预想快得多!

Ruff 项目的成功,将刺激出更多 Python+Rust 的项目。它的作者
Charlie Marsh
立志于给 Python 构建高性能的开发工具,巧合的是我曾翻译过他写的《
Using Mypy in production at Spring
》,这篇文章恰好发布于 Ruff 诞生的 2022 年 8 月!

因此,我有理由推测:在 Ruff 项目成熟后,他将用 Rust 开发高性能的 Python 类型检查工具,到时候,目前流行的 Mypy、Pytype、Pyright 和 Pyre 等工具将迎来一大劲敌。(题外话:Python 社区纷乱繁多的虚拟环境管理工具和依赖包管理工具,也有望迎来变革了吧!)

他的目标是让Python生态更加高效

这里还必须介绍两个 Rust 项目,因为 Ruff 的成功离不开它们:

  • RustPython
    :用 Rust 写成的 Python 解释器。Ruff 利用了它高性能的 AST 解析器,以此实现了自己的 AST 遍历、访问器抽象和代码质量检测逻辑
  • Maturin
    :用 Rust 写成的打包工具,可以将 Rust 项目打包成 Python 可用的包,从而可以被我们“pip install”后使用,且不需要配置 Rust 环境

Ruff 的优点与局限性

介绍完最关键的特性后(速度极快、支持 pip),我们接下来看看 Ruff 的其它方面。

总体而言,它具有这些特点:

  • 支持
    pyproject.toml
  • 兼容 Python 3.11
  • 超过
    500 条内置规则
    ,与 Flake8 内置的规则集近乎对等
  • 重新实现了数十个 Flake8 插件,如 flake8-bugbear、flake8-comprehensions 等
  • 支持自动修复,可自动纠正错误(例如,删除未使用的导入)
  • 内置缓存,可避免重复分析未更改的文件
  • 支持 VS Code、Pycharm、Neovim、Sublime Text、Emacs 等编辑器
  • 对 monorepo 友好,具有分层和级联配置

首先最值得介绍的是它支持的规则。Ruff 借鉴了流行的工具如 Flake8、autoflake、isort、pyupgrade、yesqa 等等,然后用 Rust 重新实现了超过 500 条规则。它本身不支持插件,但是吸收了数十个常用的 Flake8 插件的设计,使得已囊括的规则范围比其它任何工具都大。

实现了的部分flake8插件

Ruff 的作者还非常熟悉其它语言的分析工具,比如 Rust 的 Clippy 和 JavaScript 的 ESLint,并从这些项目上得到了设计上的启发。

Ruff 站在了多个工具/插件的肩膀上,重新实现了它们验证过的规则,也借鉴了它们的 API 和实现细节,这使得它扮演了一种“集大成”的角色,很方便使用者们作工具的顺滑迁移。

Ruff 第二个值得介绍的特点是,它没有局限于 Linter 的定位,而是借鉴 Rome、Prettier 和 Black 这些代码格式化工具(Formatter),也实现了代码格式化的功能。借鉴了 Autoflake、ESLint、Fixit 等工具,实现了代码自动纠错的功能。另外,它还借鉴了使用很广泛的 isort,支持对 import 作快速排序。

这些表明作者的目标并不只是开发一款优秀的代码分析工具,而是在静态代码分析的核心功能外,要创造出更多的可能性。此举是开发者的福音啊,以后一个工具就能满足多种诉求,再也不必纠结于不同工具的选型、协作与维护了!

Ruff 还有其它的优点,例如支持
pyproject.toml
、支持 Python 3.11、支持只分析变更的文件,等等。另外,它也有着一些局限性:

  • 支持的 lint 规则还有不够
  • 不支持使用插件,扩展性不强
  • 用 Rust 开发的,因此不便于在出错时 debug,也不便于 Python 开发者给它贡献代码

关于第一点,毕竟 Ruff 只是 8 个月大的新生项目,支持更多的规则,只是时间问题。至于插件带来的扩展性和编程语言的开发者生态,原因也是 Rust,属于“有得必有失”了。

Ruff 的使用

介绍完 Ruff 的整体情况后,我们接着看看该如何使用它吧。

首先是安装,可以用 Conda 和其它包管理工具,也可以直接用 pip:

pip install ruff

可以通过以下命令运行:

ruff check .                        # 分析当前及子目录内的所有文件
ruff check path/to/code/            # 分析指定目录及子目录内的所有文件
ruff check path/to/code/*.py        # 分析指定目录内的所有py文件
ruff check path/to/code/to/file.py  # 分析 file.py

可以用作预提交的钩子:

- repo: https://github.com/charliermarsh/ruff-pre-commit
  # Ruff version.
  rev: 'v0.0.261'
  hooks:
    - id: ruff

可以通过
pyproject.toml

ruff.toml

.ruff.toml
文件进行配置,默认配置已能满足基本使用,详细配置可以参见文档的
Configuration

Ruff 提供了官方的 VS Code 插件,可以快速上手:

Ruff 的 VS Code 插件

Ruff 官方没有提供 Pycharm 的插件,社区中有人发布了一个 Ruff 插件。

另外,它还提供了
ruff-lsp
,可以被集成到任何支持 Language Server Protocol 的编辑器中,例如 Neovim、Sublime Text、Emacs 等等。

小结

本文从 HTTPX 和 Starlette 采纳 Ruff 的新闻开始,向读者介绍了这个仅诞生 8 个月却俘获了一大批知名开源项目。它最突出的特点是使用 Rust 开发,因此在性能方面远远超越同类工具,此外,它借鉴了众多工具和插件的设计,不仅静态代码分析的规则全面,而且还具备代码格式化、代码自动纠错和 import 排序等非其它 linter 所拥有的功能。

Ruff 的成功为 Python 社区提供了一个鲜活的榜样,可以预见,我们将迎来一波用 Rust 开发的高性能工具。Ruff 的成功,与最近火爆的 ChatGPT 一样,它们传递出了一个“
这事儿能成
”的信号,从而会引爆一场使用新技术的变革!(非常巧合的是:Rust 1.0 在 2015 年 5 月发布,而 OpenAI 在 2015 年 12 月成立。)

总体而言,Ruff 非常强大,凭实力而风靡 Python 社区,绝对推荐使用!它的
使用文档
很友好,如果你想了解更多细节,可以去翻查。

​今天不举例子了,问一句你开心吗?不开心也要记得把开心的事情放到快乐源泉小瓶子里,偶尔拿出来一一遍历看看。

Map在我们Java程序员高频使用的一种数据结构,Map的遍历方式也有很多种,那那种方式比较高效呢,今天就带大家一起验证下。

先说一下阿里巴巴Java开发手册的建议:

【推荐】使用entrySet遍历Map类集合K/V,而不是用keySet方式遍历。

说明:keySet其实遍历了2次,一次是转换为Iterator对象,另一次是从hashMap种取出key对应的value。如果是JDK8,使用Map.forEash方法。

1:通过for和map.entrySet()方式遍历。

//Map初始化
 private  static Map<String,Integer> initMap(intcount){
AlternativeJdkIdGenerator alternativeJdkIdGenerator
= newAlternativeJdkIdGenerator();
Map
<String,Integer> map = new HashMap<>();for (int i = 0; i < count; i++) {
map.put(alternativeJdkIdGenerator.generateId().toString(),i) ;
}
returnmap ;
}
int count = 1000000;
Map
<String,Integer> map =initMap(count) ;//为了计算平均值,分别循环三次进行遍历 for (int i = 0; i < 3; i++) {
Instant start;
Instant end;
start
=Instant.now();for (Map.Entry<String, Integer>entry : map.entrySet()) {//一般遍历map就是获取key和value String result="key为:"+entry.getKey()+",value为:"+entry.getValue();
}
end
=Instant.now();
System.out.println(
"遍历循环" + count + "次耗时:" + Duration.between(start, end).toMillis() + "ms");
}

运行三次的结果如下:(平均值:368.33ms)

​2、通过 for, Iterator 和 map.entrySet() 来遍历

  int count = 1000000;
Map
<String,Integer> map =initMap(count) ;for (int i = 0; i < 3; i++) {
Instant start;
Instant end;
start
=Instant.now();for (Iterator<Map.Entry<String,Integer>> entries =map.entrySet().iterator(); entries.hasNext(); ) {
Map.Entry
<String,Integer> entry =entries.next();
String result
="key为:"+entry.getKey()+",value为:"+entry.getValue();
}
end
=Instant.now();
System.out.println(
"遍历循环" + count + "次耗时:" + Duration.between(start, end).toMillis() + "ms");
}

​运行三次的结果如下:(平均值:339.66ms)

​3、通过 for 和 map.keySet() 来遍历

 int count = 1000000;
Map
<String,Integer> map =initMap(count) ;for (int i = 0; i < 3; i++) {
Instant start;
Instant end;
start
=Instant.now();for(String key : map.keySet()) {
String result
="key为:"+key+",value为:"+map.get(key);
}
end
=Instant.now();
System.out.println(
"遍历循环" + count + "次耗时:" + Duration.between(start, end).toMillis() + "ms");
}

运行三次的结果如下:(平均值:379.66ms)

​4、通过 for,Iterator 和 map.keySet() 来遍历

  int count = 1000000;
Map
<String,Integer> map =initMap(count) ;for (int i = 0; i < 3; i++) {
Instant start;
Instant end;
start
=Instant.now();for (Iterator<String> key =map.keySet().iterator(); key.hasNext(); ) {
String k
=key.next();
String result
="key为:"+k+",value为:"+map.get(k);
}
end
=Instant.now();
System.out.println(
"遍历循环" + count + "次耗时:" + Duration.between(start, end).toMillis() + "ms");
}

运行三次的结果如下:(平均值330.33ms)

5、通过 map.forEach() 来遍历

int count = 1000000;
Map
<String,Integer> map =initMap(count) ;for (int i = 0; i < 3; i++) {
Instant start;
Instant end;
start
=Instant.now();
map.forEach((key, value)
->{
String result
="key为:"+key+",value为:"+map.get(value);
});
end
=Instant.now();
System.out.println(
"遍历循环" + count + "次耗时:" + Duration.between(start, end).toMillis() + "ms");
}

​运行三次的结果如下:(平均值506.33ms)

​经过上面的验证(在大批量数据的时候,数据量小的时候没测试,感觉数据量小的化耗时太小更不好比较)最好不要用map.forEach()来遍历Map。在普通的遍历方法中 entrySet() 的方法要比使用 keySet() 的方法好。【PS虽然keySet() 的平均值有时候比较小,但它的波动性比较大,所以还是考虑阿里巴巴Java开发手册的建议使用entrySet()遍历Map】。

看过 HashMap 源码的同学应该会发现,这个遍历方式【entrySet(】在源码中也有使用,如下图所示。

​Spring的源码也有很多的Map,大神们遍历的方式也都基本使用entrySet()遍历如下:

所以开发中也建议使用entrySet()来遍历Map。

这是一篇系列博文。请关注我,学习更多.NET MAUI开发知识!

在之前的博文中提到这个项目,它是为音乐播放器专门开发的基于手势控制的UI界面。

此UI界面可以让用户在不看屏幕的情况下,通过手势来控制音乐播放器的各种操作,如播放、暂停、下一首、上一首。

在这里插入图片描述
手势来控制的交互方式适合不方便看手机屏幕时简单的音乐播放需求,在驾车、运动等场景下有较好的用户体验。

在这里插入图片描述

架构

跨平台

使用
.NET MAU
实现跨平台支持,本项目可运行于Android、iOS平台。

在这里插入图片描述
在这里插入图片描述

播放内核

播放内核使用MatoMusic.Core,查看此博文
[MAUI 项目实战] 音乐播放器(二):播放内核

此项目重点关注的是手势交互UI,播放内核的实现将不再赘述。

手势原理

在播放界面的8个方向,分别放置控制区域,通过拖拽圆形专辑(pan)到控制区域(pit),实现对应的控制操作。此示例实现了快进,快退,下一曲,上一曲,播放/暂停操作,pan和pit将在下一章节介绍。

拖拽平移和控制区域的关系,可以抽象成四个状态,分别是Out,In,Over,Start。

手势状态类型PanType定义如下:

  • In:pan进入pit时触发,
  • Out:pan离开pit时触发,
  • Over:释放pan时触发,
  • Start:pan开始拖拽时触发
public enum PanType
{
    Out, In, Over, Start
}

对拖拽手势的处理,由手势容器控件PanContainer封装,实现方式将在下一章节介绍。

一次有效的控制,经过Start -> In -> Out -> Over的状态变化,并且手指释放位置是在pit的范围内。

当整个控制触发完成后,控件将触发OnfinishedChoise事件。

基本控制

在页面中订阅这个事件,在事件方法中实现控制逻辑。

如上一曲操作,订阅事件后,实现如下逻辑:

private void DefaultPanContainer_OnOnfinishedChoise(object sender, PitGrid e)
{
    CurrentPitView = e;
    switch (CurrentPitView.PitName)
    {
        case "LeftPit":
        MusicRelatedViewModel.PreAction(null);
        break;

        ...
    }
}

控件会将当前触发的pit传递给事件方法,通过pit的名称,可以判断当前触发的是哪个控制区域。

控件在经过pit时会启用广播事件,使用CommunityToolkit库的 WeakReferenceMessenger实现了消息机制,订阅此事件消息可以接收到控件运动的细节,在事件方法中实现自己的逻辑,如界面元素样式的改变。

public NowPlayingPage()
{
    InitializeComponent();
    WeakReferenceMessenger.Default.Register<PanActionArgs, string>(this, TokenHelper.PanAction, PanActionHandler);
    ...
}

如在拖拽开始时,显示控制区域的提示信息,拖拽结束时,隐藏提示信息。

private async void PanActionHandler(object recipient, PanActionArgs args)
{
    var parentAnimation = new Animation();

    Animation scaleUpAnimation1;
    Animation scaleUpAnimation2;
    switch (args.PanType)
    {
        case PanType.Over:

            scaleUpAnimation1 = new Animation(v => this.PitContentLayout.Opacity = v, PitContentLayout.Opacity, 0, Easing.CubicOut);
            scaleUpAnimation2 = new Animation(v => this.TitleLayout.Opacity = v, TitleLayout.Opacity, 1, Easing.CubicOut);

            parentAnimation.Add(0, 1, scaleUpAnimation1);
            parentAnimation.Add(0, 1, scaleUpAnimation2);

            parentAnimation.Commit(this, "RestoreAnimation", 16, 250);
         
...

快进/快退

拖拽停留在左右控制区域超过一定时间,将触发“快进”或“快退”

播放界面拥有一个定时器,用于拖拽快进、快退功能

private IDispatcherTimer _dispatcherTimer;

拖拽进入控制区域时,启动定时器,停留在左右控制区域大于2s时将触发定时器Tick事件,执行快进或快退操作。

private async void PanActionHandler(object recipient, PanActionArgs args)
{

    switch (args.PanType)
    {
        ...
        case PanType.In:

            switch (args.CurrentPit?.PitName)
            {
                case "LeftPit":

                    _dispatcherTimer =Dispatcher.CreateTimer();
                    _dispatcherTimer.Interval=new TimeSpan(0, 0, 2);

                    _dispatcherTimer.Tick+=   async (o, e) =>
                    {
                        this.TipLabel.Text = FaIcons.IconFastBackward;
                        this.TipTextLabel.Text = "快退";
                        _runCount++;
                        await MusicRelatedViewModel.StartFastSeeking(-2);


                    };
                    _dispatcherTimer.Start();
                    this.TipTextLabel.Text = "上一曲";

                    break;
    ...

_runCount是个全局变量,记录是否已经执行过快进或快退操作,当退出控制区域时,如果已经执行过快进或快退操作,将停止快进或快退操作,并将计时器停止。

 case PanType.Out:
    this.PitTipLayout.Children.Clear();
    if (this._runCount > 0)
    {
        MusicRelatedViewModel.EndFastSeeking();
    }
    if (_dispatcherTimer!=null)
    {
        _dispatcherTimer.Stop();

    }
    _runCount = 0;
    this.TipTextLabel.Text = string.Empty;


    break;

同理,在松手时,应该停止快进或快退操作,并将计时器停止。

case PanType.Over:

    ...
    MusicRelatedViewModel.EndFastSeeking();
    if (_dispatcherTimer!=null)
    {
        _dispatcherTimer.Stop();

    }
    _runCount = 0;

沉浸模式

_dispatcherTimer2是控制界面进入“沉浸模式”的定时器,当界面无操作5s之后界面将进入沉浸模式,隐藏标题栏。

private void SetupFullScreenMode(int delay = 5)
{
    _dispatcherTimer2 =Dispatcher.CreateTimer();
    _dispatcherTimer2.Interval=new TimeSpan(0, 0, delay);

    _dispatcherTimer2.Tick+=   (o, e) =>
    {

        this.MainCircleSlider.BorderWidth = 3;
        this.TitleLayout.FadeTo(0);

    };

    _dispatcherTimer2.Start();
}

有操作进行时,恢复到正常模式。

case PanType.Start:

    ...

    if (_dispatcherTimer2.IsRunning)
    {
        _dispatcherTimer2.Stop();
    }
    SetupNormalMode();

    break;

下一章将逐步展开手势控制的实现细节。

项目地址

Github:maui-samples