2024年1月

事件处理程序泄漏已经存在很长时间了,这是 WPF (Windows Presentation Foundation)开发人员经常要处理的最麻烦的问题之一。您可能会想:是什么让事件处理程序泄漏如此重要?事件处理程序泄漏很容易引起,只需忘记取消订阅事件即可。此外,它们很难发现,甚至更难修复。

在更新17.9预览1中引入的 Visual Studio 托管内存使用工具中添加的新见解大大简化了发现/修复这些泄漏的过程。它提供有关哪些对象正在泄漏及其订阅的事件的信息。

什么是事件处理程序泄漏?

当对象在功能上无法使用时,没有被识别为垃圾收集时,它会在堆上泄漏。这意味着它将无意中在内存中保持活动状态。事件处理程序因导致这种情况而臭名昭著。这是因为事件处理程序在对象和它订阅的事件之间创建了一个直接引用。

在本例中,我们有一个 Publisher 和 Subscriber 类。当 Subscriber 调用 Subscribe 时,MyEvent 将在堆中链接 Publisher 和 Subscriber:

这样做的问题是,如果 Subscriber 忘记取消订阅,这些引用将保留在堆上,从而泄漏 Subscriber。这里最简单的解决方案是简单地调用 Unsubscribe 方法,但是在更复杂的应用程序中,很难跟踪对象的订阅以及何时退订。这就是 Memory Analyzer 可以帮助开发人员解决这些问题的地方。

好吧,让我们看看如何解决它?

为了证明这一点,我们将对一个示例 WPF 应用程序进行演练调试,以查找事件处理程序泄漏:

在本例中,我们有一个窗口,它在打开时订阅 dispatcherTimer_Tick 事件。事件在做什么并不重要。这段代码的重要部分是,当窗口关闭时,我们忘记取消订阅事件:

这里取消订阅语句的注释是有问题的,因为它在窗口关闭时不再正确地取消订阅,并且会导致泄漏。为了找到它,让我们开始调试这个应用程序(F5)。为简单起见,让我们假设 main() 正确地导致泄漏,它打开 AdWindow,导致它订阅一个事件,然后关闭它。

首先,我们需要打开诊断工具窗口。要在调试会话中访问它,请执行 Debug ->  Windows ->  Diagnostic Tools。

当它在调试会话中打开时,它应该是这样的:

此窗口显示正在调试的应用程序的堆的总体大小和 CPU 百分比。当我们单击相机图标拍摄快照时,我们可以通过单击 Objects 下的值来查看堆并访问事件处理程序泄漏见解。

进入快照的堆视图后,导航到 Insights(见解):

我们终于到站了!在 Insights 选项卡中,我们可以看到泄漏事件处理程序的列表,并且我们可以看到泄漏窗口的显示。此外,我们可以看到该泄漏所浪费的总量。仅仅是这个简单的例子就造成了 4.93 KB 的泄漏!这是因为窗口有一个完整的子树,它引用的对象也会因为忘记取消订阅而泄漏。

此外,您可以通过点击“Show Just My code”来过滤掉所有的系统代码,只显示 AdWindow。

这很酷,但现在怎么办?

到目前为止,我们已经成功地确定了我们的应用程序中的泄漏。现在,如果我们想要修复它,我们可以点击“View Details”来查看有关问题的更多信息,更重要的是,如何修复它。

这个视图向我们展示了一些关于泄漏的关键信息。我们可以看到对象的地址,它持有的事件处理程序,最重要的是,它订阅的对象。这传达了要解决这个问题,AdWindow 必须从 DispatcherTimer 取消订阅。此外,您还可以看到 AdWindow 对象的引用图。“Referenced Objects”选项卡显示了由于 AdWindow 而泄漏的额外对象的数量。

我们还能做什么?

如果您已经读到这里了,你可能会对这种见解的其他用途感兴趣。例如,检测逻辑可以处理任何类型的事件处理程序。考虑前面的带有发布者和订阅者类的控制台应用程序,如果我们创建自己的事件处理程序,我们仍然可以检测到它。

告诉我们您的想法!

在未来的版本中,这种体验还会有很多改进。请下载最新的 Visual Studio 预览版并提供您的反馈。请在 Visual Studio 中通过“Report a Problem”或直接在开发者社区站点提出问题并提供反馈。

原文链接:https://devblogs.microsoft.com/visualstudio/unlocking-the-secrets-of-managed-memory-dive-into-event-handler-leak-insights/

兴趣是最好的老师,
HelloGitHub
让你对编程感兴趣!

简介

HelloGitHub
分享 GitHub 上有趣、入门级的开源项目。

https://github.com/521xueweihan/HelloGitHub

这里有实战项目、入门教程、黑科技、开源书籍、大厂开源项目等,涵盖多种编程语言 Python、Java、Go、C/C++、Swift...让你在短时间内感受到开源的魅力,对编程产生兴趣!


以下为本期内容|每个月
28
号更新

C 项目

1、
genann
:C 语言写的极简神经网络库。这是一个轻量、无依赖、单文件的 C 语言神经网络库,内含丰富的示例和测试。代码简洁易读,适合作为初学者学习神经网络的入门项目。来自
@ziming012
的分享

#include "genann.h"

/* Not shown, loading your training and test data. */
double **training_data_input, **training_data_output, **test_data_input;

/* New network with 2 inputs,
 * 1 hidden layer of 3 neurons each,
 * and 2 outputs. */
genann *ann = genann_init(2, 1, 3, 2);

/* Learn on the training set. */
for (i = 0; i < 300; ++i) {
    for (j = 0; j < 100; ++j)
        genann_train(ann, training_data_input[j], training_data_output[j], 0.1);
}

/* Run the network and see what it predicts. */
double const *prediction = genann_run(ann, test_data_input[0]);
printf("Output for the first test data point is: %f, %f\n", prediction[0], prediction[1]);

genann_free(ann);

C# 项目

2、
FancyScrollView
:Unity 滑动列表插件。该项目采用 Unity 引擎动画系统来定制列表滑动效果,具备非常高的灵活性,除了用作滑动列表,还可以用作导航栏。项目代码结构和风格规范,接入成本低、易于使用和定制。来自
@Wu Zheng
的分享

using UnityEngine;
using UnityEngine.UI;
using FancyScrollView;

class MyCell : FancyCell<ItemData>
{
    [SerializeField] Text message = default;

    public override void UpdateContent(ItemData itemData)
    {
        // 更新内容
        message.text = itemData.Message;
    }

    public override void UpdatePosition(float position)
    {
        // position 是一个介于 0.0 到 1.0 之间的值
        // 可以根据 position 自由控制滚动的外观
    }
}

3、
MarkovJunior
:基于马尔可夫链的图像生成器。马尔可夫链是一种数学模型,具有“无记忆”的性质,即未来状态的概率分布只依赖于当前状态,而不依赖于过去的状态。该项目利用马尔可夫链原理,通过模拟图像的状态转移来生成独特的图像,包括建筑、迷宫等。

C++ 项目

4、
abseil-cpp
:谷歌开源的 C++ 基础库。这是一个在 Google 内部被广泛应用的 C++ 公共库,它提供了一系列高质量、可靠、高效的基础模块,其中包括字符串处理、并发、时间、STL 容器、测试、日志等实用函数。来自
@张程林
的分享

5、
gpupixel
:高性能跨平台实时美颜滤镜库。这是一个用 C++11 编写的高性能图像和视频处理库,内置基于 GPU 的美颜特效滤镜,效果可以达到商业级别水平。支持磨皮、美白、瘦脸、大眼等特效,适用于 iOS、macOS 和 Android 平台。来自
@Zhaoyou Ge
的分享

6、
qtrvsim
:面向教育的 RISC-V CPU 模拟器。这是一个采用 Qt 实现的 RISC-V CPU 模拟器,由捷克理工大学计算学院开发。

Go 项目

7、
copilot-gpt4-service
:将 Github Copilot 转成 ChatGPT 的服务。该项目可以将 GitHub Copilot 转换成 ChatGPT 的服务,为什么要这么做呢?因为如果你在 GitHub 上有一个开源项目,就有机会免费使用 Copilot,除此之外学生和老师在完成认证后也可以免费使用 Copilot,一番操作下来就等于白嫖 GPT-4。一定要按照推荐的方式部署,否则有封号风险,仅限自用。来自
@exiaOgW1q
的分享

8、
goploy
:容易上手的代码发布平台。这是一个采用 Go + Vue.js 构建的 Web 部署平台,可一键部署、发布和回滚项目。支持基于角色的访问控制、监控、秒级定时任务、Xterm、LDAP 等功能,提供了完整的安装引导,即使是初学者也能轻松上手。来自
@zhenorzz
的分享

9、
listmonk
:开源的邮件列表和营销平台。这是一个开箱即用的邮件营销平台,可以帮助你管理邮件订阅者、创建和发送邮件、分析营销数据。可查看邮件阅读率、链接点击率等,支持自托管适用于个人和企业。

10、
restic
:一款强大的开源备份工具。该项目提供了简单、快速、安全的开源备份解决方案。它无需繁琐的配置,即可轻松完成备份和恢复操作。采用增量备份策略,备份数据经过加密和压缩处理,保障数据安全且节省空间,支持灵活的存储选择,包括本地磁盘和云存储。可设置自动备份时间,确保数据得到定期的备份保护。

$ restic --repo /tmp/backup backup ~/work
enter password for repository:
scan [/home/user/work]
scanned 764 directories, 1816 files in 0:00
[0:29] 100.00%  54.732 MiB/s  1.582 GiB / 1.582 GiB  2580 / 2580 items  0 errors  ETA 0:00
duration: 0:29, 54.47MiB/s
snapshot 40dc1520 saved

11、
vfox
:无忧应对多编程语言不同版本的工具。这是一款跨平台的通用版本管理工具,通过命令行快速安装、切换编程语言的不同版本,并支持自定义源地址。相比于针对每种语言的独立版本管理工具(如 nvm、fvm、gvm 等),这个项目让开发者摆脱繁琐的学习和记忆过程,只需一个工具、一条命令,轻松搞定多编程语言版本管理。来自
@Han Li
的分享

$ vfox c
node -> v20.10.0
java -> v11.0.12

Java 项目

12、
1brc
:探索 Java 处理 10 亿行文本的最快速度。这是一个有趣的 Java 编程挑战,要求开发者编写一个 Java 程序,读取包含多个气象站温度值的文件(10 亿行),然后计算每个气象站的最小、平均和最大值,最后按照站点名称排序后输出,现在最快速度为 2 秒。

13、
automq-for-kafka
:一款真正的云原生 Kafka 解决方案。该项目是基于云原生重新设计的新一代 Kafka 发行版。在保持和 Apache Kafka 100%兼容前提下,AutoMQ 可以为用户提供高达 10 倍的成本优势以及百倍的弹性优势,同时支持秒级分区迁移和流量自动重平衡,解决运维痛点。

14、
spring-startup-analyzer
:优化 Spring Boot 应用启动性能的工具。该项目利用采集 Spring 应用启动过程数据,生成交互式分析报告,为开发者提供了分析 Spring 应用启动性能的工具。其主要功能包括分析启动卡点、处理 Spring Bean 异步初始化,以及显示应用未加载的 jar 包、方法调用次数和耗时统计等详细信息。来自
@linyimin
的分享

JavaScript 项目

15、
awesome-hands-control
:用手势操控电脑程序的工具。该项目基于手势识别进行自定义操控电脑程序,采用纯前端技术栈实现。它通过训练好的模型(MediaPipe)来识别手势,然后将特定手势与电脑操控绑定,最后,用户可以指定操作的进程,从而实现手势操控电脑程序。来自
@Rylan
的分享

16、
bpmn-js
:专注于流程图的可视化和编辑组件。该项目提供了直观的拖拽式创建和编辑流程图的功能,可用于构建业务流程管理、决策流可视化和低代码平台。来自
@塔咖
的分享

const xml = '...'; // my BPMN 2.0 xml
const viewer = new BpmnJS({
  container: 'body'
});

try {
  const { warnings } = await viewer.importXML(xml);

  console.log('rendered');
} catch (err) {
  console.log('error rendering', err);
}

17、
dockge
:一个美观、易用的 Docker Compose 管理平台。该项目提供了一个 Web 界面,用于管理 docker-compose.yaml 文件。它开箱即用、界面设计精美,支持交互式编辑 compose.yaml 文件、更新 docker 镜像,以及启动、停止、重启、删除 docker 等操作。来自
@猎隼丶止戈reNo7
的分享

18、
theatre
:一个用于创建 Web 动画的 JavaScript 库。该项目是带图形用户界面的 Web 动画编辑器,能对任何 JavaScript 变量进行动画处理。它不仅支持处理 three.js 或其他 3D 库对象的动画功能,还能利用 React 等库对 HTML/SVG 进行动画处理。

19、
tiny-rdm
:一款轻量级的跨平台 Redis 桌面客户端。该项目是基于 WebView2 的 Redis 桌面客户端,拥有小巧的体积和精美的界面,同时支持中文。它提供了多种连接方式、分段加载、慢日志、转码显示等功能,可以在 Windows、Linux 和 macOS 系统上使用。

Kotlin 项目

20、
jingmo
:一款古诗词文和成语应用。它叫「京墨」是一个免费的 Android 阅读应用,内含丰富的中国传统文化内容,包括古诗、歇后语、成语故事、中国传统节日、绕口令等。安装应用第一次进入无内容,需要在设置里手动同步数据。来自
@贺丰宝
的分享

Objective-C 项目

21、
Itsycal
:可爱的 Mac 菜单栏日历。这是一个迷你的菜单栏,拥有可爱的界面和实用的功能,支持显示/添加系统日历的事件、深色模式、周数、快捷键等功能,适用于 macOS 11+ 系统。

22、
KeepingYouAwake
:防止 Mac 进入睡眠状态的工具。这一个小型的菜单栏实用工具,可以让 Mac 电脑在预设的时间内或永久不进入睡眠模式,适用于 macOS 10.13 或更高版本。

PHP 项目

23、
akaunting
:专为小型企业和个人设计的在线会计软件。该项目是基于 Laravel+Vue.js+Tailwind CSS+MySQL 构建的会计平台,为用户提供全面的会计和财务功能。其中包括费用跟踪、现金流、报告等,并且支持移动端适配和多语言。

Python 项目

24、
DouyinLiveRecorder
:一款支持多平台的直播录制工具。该项目是基于 FFmpeg 实现的多平台直播源录制工具,支持循环执行直播录制任务(循环值守)、直播状态推送、多人录制、去水印、选择画质等功能。

25、
harlequin
:一个简单、快速、美观的终端数据库客户端。这是一个带界面的命令行数据库客户端,提供了数据库和表目录、查询编辑器、显示结果、导出数据的功能,支持 DuckDB、SQLite、Postgres、MySQL 等数据库。

26、
khal
:一款简单、美观的终端日历。该项目是用 Python 写的命令行日历工具,支持快速便捷地浏览、添加和编辑事件,以及同步日历数据。

27、
pyupgrade
:一键升级 Python 代码的工具。这是一个用于自动升级 Python 代码,以适应新版本语法的工具。支持升级到不同的 Python 版本、提供预览模式即查看改动变化等功能。

28、
text_blind_watermark
:给文本加盲水印的 Python 库。通过该项目可以将一段隐秘信息嵌入到明文中,嵌入前后的明文无变化。简单说就是给文本打上隐藏水印,适合在版权保护、数据泄漏溯源、数据安全等场景使用,支持 macOS 的 Chrome 浏览器、苹果备忘录、macOS/iPhone 的微信和钉钉等应用。

from text_blind_watermark import TextBlindWatermark2

password = 'HelloGitHub'
text = '这句话中有盲水印,你能提取出来吗?'
watermark = 'HelloGitHub'

text_blind_wm = TextBlindWatermark2(password=password)

text_with_wm = text_blind_wm.embed(text=text, watermark=watermark)
print(text_with_wm)

Rust 项目

29、
cmd-wrapped
:一个 Rust 编写的命令行历史记录分析工具。这是一款命令行工具,它可以读取你的命令行操作历史记录,并生成详细的分析报告。报告包括过去任意一年的命令行活跃时段、常用命令等信息,支持 Zsh、Bash、fish 等。

其它

30、
gdb-dashboard
:GDB 可视化调试界面。这是专为 GNU 调试器(GDB)设计的文本界面,支持模块化显示调试的程序相关信息,更直观和方便地调试代码。该界面采用 Python 编写,具备轻松自定义和扩展的特性。

31、
kubernetes-network-policy-recipes
:只需复制粘贴即可解决 K8s 网络问题的配方。该项目包含了 Kubernetes 网络策略的各种用例和示例 YAML 文件,可直接复制使用。

32、
network_proxy_flutter
:支持手机端的免费抓包工具。该项目是采用 Flutter 开发的抓包工具,可用于拦截、检查和重写 HTTP(S) 流量。它支持扫码连接、域名过滤、请求重写等功能,适用于 Windows、macOS、Linux、Android 和 iOS 平台。

33、
particle-life
:粒子生命演化游戏。该项目通过定义粒子之间的相互作用力,从而生成复杂的自组织图案。源码十分简单,用户可以在线试玩,创造出各种有趣的图案。

34、
vimwiki
:Vim 中的个人 wiki。这是一个 Vim 插件,通过以 wiki 的方式连接文本,提供更好的组织笔记和想法的功能。

开源书籍

35、
game-programming-patterns
:《游戏编程模式》。该书收集了经过验证、已发布的 3A 级游戏中的经验和模式,来解决你在游戏开发中遇到的问题。

36、
PDF-Explained
:《PDF 解析》。该项目是《PDF Explained》一书的非官方中文翻译版,内容由浅入深介绍了如何构建简单的 PDF 文件,以及 PDF 运算符、书签、超链接、注释、加密等高级特性。

机器学习

37、
AnimateDiff
:让 AI 生成的图动起来。这是一款可以在 Stable Diffusion 中制作动图的库,支持将大多数开源模型转换为动画生成器。让原本通过文字生成的图片,变成 gif 图片动起来。来自
@adoin
的分享

38、
AnyText
:轻松 DIY 图片文字,定制你的创意设计。该项目提供了文字生成和文字编辑两种模式,它能够根据提示词生成图文融合的图片,并确保文字的准确性,还支持对上传图片中的文字进行编辑后,重新生成图片。支持中文、英语、日语、韩语等多语言,适用于海报设计、Logo 设计、创意涂鸦、表情包等场景。

39、
pyvideotrans
:开源的视频翻译和配音工具。该项目可以将视频从一种语言翻译成指定语言的视频,并自动生成和添加对应语言的字幕和配音。来自
@okaymyworld
的分享

40、
StreamDiffusion
:实时交互式 AIGC 图片。该项目能以惊人的速度生成 AIGC 图像,单张 RTX4090 显卡可达 100 张/秒。它通过流批处理简化数据处理,采用残差无分类器(RCFG)减少计算冗余,随机相似性过滤器提高 GPU 利用率,并通过优化 IO 队列实现并行处理。同时,利用多种模型加速工具,实现爆炸式地提升 AIGC 图像速度。

最后

感谢参与分​享开源项目的小伙伴们,欢迎更多的开源爱好者来 HelloGitHub 自荐/推荐开源项目。如果你发现了 GitHub 上有趣的项目,就
点击这里
分享给大家伙吧!

本期有你感兴趣的开源项目吗?如果有的话就留言告诉我吧~如果还没看过瘾,可以
点击阅读
往期内容。


那么,下个月
28 号
不见不散,完结撒花

本教程基于
物联网浏览器(IoTBrowser)-Web串口自定义开发
,详细的过程可以翻看之前的文章。

本篇以实现顶尖OS2系列电子秤协议对接,并集成到IoTBrowser平台。由于没有找到OS2协议的官方文档,用串口助手抓包分析了一下,简单封装了一下实现代码有点潦草。

所有的串口或者需要实时数据同步的业务都可以集成ComBase类,重量数据采集是典型的实时推送场景,由于电子秤基本只需要推送重量,需要往串口写数据,所以不需要实现发送数据接口。

代码如下:

usingDDS.IoT.Com;usingSystem;usingSystem.Collections.Generic;usingSystem.IO.Ports;usingSystem.Linq;usingSystem.Runtime.InteropServices;usingSystem.Text;usingSystem.Threading;usingSystem.Threading.Tasks;namespaceDDS.IoT.DJ
{
public classOs2Driver : ComBase
{
public override string Type => "DJ_Os2";public override string Name => "顶尖OS2";/// <summary> ///最近一次重量/// </summary> private string _lastValue = string.Empty;private object _locker = new object();public override bool Init(int port, int baudRate = 9600, string extendData = null)
{
if (SP == null)
{
this.Port =port;var portName = "COM" +port;
SP
= newSerialPort(portName);base.PortName =portName;
SP.BaudRate
=baudRate;
SP.Parity
=System.IO.Ports.Parity.None;
SP.DataBits
= 8;
SP.StopBits
=System.IO.Ports.StopBits.One;//SP.DtrEnable = true;//SP.RtsEnable = true; SP.DataReceived +=SP_DataReceived;
Console.WriteLine(
"初始化Os2驱动程序成功!");
}
return true;
}
private void SP_DataReceived(objectsender, SerialDataReceivedEventArgs e)
{
try{lock(_locker)
{
if (false == SP.IsOpen) return;int bytesRead = SP.BytesToRead;//获取接收缓冲区中数据的字节数 if (bytesRead < 16)
{
return;
}
byte[] bytesData = new byte[16];//var strData = "1,2,83,45,48,48,46,48,49,56,107,103,101,3,4,0"; SP.Read(bytesData, 0, 16);if (bytesData[0] != 0x01)
{
SP.DiscardInBuffer();
return;
}
List
<byte> buffer = new List<byte>();
buffer.AddRange(bytesData);
SP.DiscardInBuffer();
var weight =ConvertWeight(buffer);//验证数据是否有效,且与上一次数据不一致 if (weight != this.Data)
{
if (this.OnPushData != null)
{
this.OnPushData(this.Id,weight);//触发事件 this.Data =weight;
}
}
}
}
catch(Exception ex)
{
Console.WriteLine(
"串口操作失败:" +ex.Message);
}
}
/// <summary> ///Os2称重数据--读取字节为十进制数据 0x为十六进制/// </summary> /// <param name="byteFrame">帧数据(两位wn+8位数据带小数点+kg+两个固定字节回车换行)</param> private float ConvertWeight(List<byte>byteFrame)
{
float value =0f;if (byteFrame == null || byteFrame.Count == 0)
{
returnvalue;
}
if (true)
{
byte[] byteData = new byte[9];
byteFrame.CopyTo(
3, byteData, 0, 9);string weightData = Encoding.ASCII.GetString(byteData).Replace("kg", "").Replace("g", "");
value
= float.Parse(weightData);
}
returnvalue;
}
public bool Opened = false;public override eventPushData OnPushData;public override boolOpen()
{
var b = false;try{if (!Opened)
SP.Open();
b
= true;
Opened
= true;
}
catch(Exception ex)
{
string msg = string.Format("Os2串口打开失败:{0}", ex.Message);
Console.WriteLine(msg);
}
returnb;
}
public override boolClose()
{
if(SP.IsOpen)
SP.Close();
Opened
= false;
OnPushData
= null;return true;
}


}
}

同样,先需要本地测试,所以需要在main方法中编写测试代码。

        static void Main(string[] args)
{
var ok = false;do{
Os2Driver driver
= newOs2Driver();
driver.OnPushData
+=OnPushData;
Console.Write(
"请输入串口号:");var port =Convert.ToInt32(Console.ReadLine());
ok
=driver.Init(port);
ok
=driver.Open();if(ok)
Console.Write(
"打开成功,重量监听中...");
}
while (!ok);
Console.ReadLine();
}
private static void OnPushData(string id, dynamicweight)
{
Console.WriteLine(DateTime.Now.ToString()
+ ":" +weight);
}

本地编译,拷贝到Plugins/Com文件夹下,在IoTBrowser中将驱动型号改为DJ_Os2,串口号根据实际情况填写即可。

示例代码:

https://gitee.com/yizhuqing/IoTBrowser/tree/master/Plugins/DDS.IoT.DJ

利用灵活的“导入”和“导出”机制,WebAssembly与承载的JavaScript应用之间可以很便利地“互通有无”。《
与JavaScript的交互
》着重演示了如何利用函数的导入和导出实现功能的共享,接下来我们主要关注数据的传递或者共享。宗地来说,WebAssembly与宿主程序之间的数据传递主要有如下三种手段,本篇文章主要关注Memory。源代码下载:
app3
app4

  • Memory:以二进制(字节)的形式传递数据;
  • Table:传递类型化数据(目前只支持函数);
  • Global:共享全局变量;

一、导入Memory

顾名思义,一个Memory映射一块连续的内存,应用以二进制(字节)的形式对它进行读写。Memory可以利用导入功能从宿主程序传递给WebAssembly,下面的实例演示了这样的场景:作为宿主的JavaScript应用创建一个Memory对象并写入相应的内容,然后将其导入到加载的WebAssembly模块,后者将其中的内容读出来。

如下所示的代码片段是承载WebAssembly程序的app.wat文件的内容,我们利用(memory)定义了一个导入的memory,导入路径为“imports.memory”,后面指定的参数1代表初始大小,单位为Page(64K)。接下来我们定义了四个导出函数,它们会从指定的位置(参数$index表示偏移量)读取相应长度的字节内容,并将其转换成对应的类型。具体来说,这四个函数的返回类型分别为i32、i64、f32和f64,也就是WebAssembly支持的四种数据类型。具体的读取通过执行{i32|i64|f32|f64}.load指令完成,该指令将读取位置作为唯一参数,所以我们在执行该指令之前需要执行local.get 指令将代表读取位置的$index参数压入栈中。

(module
    (memory (import "imports" "memory") 1)

    (func (export "readAsInt32") (param $index i32) (result i32)
         local.get $index
         i32.load
    )

    (func (export "readAsInt64") (param $index i32) (result i64)
         local.get $index
         i64.load
    )

    (func (export "readAsSingle") (param $index i32) (result f32)
         local.get $index
         f32.load
    )

    (func (export "readAsDouble") (param $index i32) (result f64)
         local.get $index
         f64.load
    )
)

有人可能有这样的疑问,我们在执行load指令的时候为什么没有指定具体读取的Memory对象呢?这是因为目前一个WebAssembly模块只能拥有一个Memory对象,这一限制可能会在后续版本中解除,针对多
Memory的提案
在两年前已经提出。

接下来我们依然需要执行“wat2wasm app.wat –o app.wasm”命令对app.wat文件进行编译,最终生成二进制的模块文件app.wasm。该文件在index.html页面的JavaScript脚本中被加载,index.html页面的内容如下所示。如下面的代码片段所示,我们调用构造函数WebAssembly.Memory创建了一个Memory对象,并将初始大小设置为1(Page)。我们将这个Memory对象的缓冲区(对应buffer属性)映射为一个Uint32Array数组。通过设置这个数组的前两个元素的值(123),我们相应的字节写入前8个字节。

<html>
    <head></head>
    <body>
        <div id="container"></div>
        <script>
            var memory= new WebAssembly.Memory({ initial: 1 });
            const array = new Uint32Array(memory.buffer);
            array[0] = 123;
            array[1] = 123;
            WebAssembly
                .instantiateStreaming(fetch("app.wasm"), {"imports":{"memory":memory}})
                .then(results => {
                    var exports = results.instance.exports;
                        document.getElementById("container").innerHTML =
                        `<p>Int32: ${exports.readAsInt32(0)}</p>` +
                        `<p>Int64: ${exports.readAsInt64(0)}</p>` +
                        `<p>Single: ${exports.readAsSingle(0)}</p>` +
                        `<p>Double: ${exports.readAsDouble(0)}</p>` ;
                });
        </script>
    </body>
</html>

我们从WebAssembly模块实例中导出前面定义的4个函数,并将针对4种不同类型读取出来的值格式化成HTML呈现出来,下图所示的就是最终的输出结果。

image

二、导出Memory

上面演示了如何将Memory对象从宿主应用中导入到WebAssembly模块,现在我们反其道而行,将WebAssembly模块中创建的Memory导出到宿主程序,为此我们将app.wat文件修改为如下的内容。如代码片段所示,我们利用(memory (export "
memory
") 1)节点定义了一个初始大小为1Page的Memory,并以名称“memory”进行导出。随后定义的导出函数initialize会将作为参数指定的两个整数写入前8个字节。针对Memory的写入通过{i32|i64|f32|f64}.store指令完成,该指令接受两个参数,第一个代表写入的位置,第二个代表写入的值。由于我们具体调用的是i32.store指令,所以在第二次调用的时候指定的写入位置是4,而不是2。

(module
    (memory (export "memory") 1)
    (func (export "initialize") (param $first i32) (param $second i32)
         i32.const 0
         local.get $first
         i32.store

         i32.const 4
         local.get $second
         i32.store
    )
)

在如下所示的index.html中,我们在加载WebAssembly
模块“app.wasm”并得到模块实例后,调用导出的initialize函数在Memory中写入两个整数123。然后我们导出Memory对象,并将它的缓冲区映射为四种类型的数组(Uint32Array、BigUint64Array、Float32Array和Float64Array),并将第一个元
素的值读取出来,这一操作与上面定义针对四个类型定义的读取函数可谓“异曲同工”。

<html>
    <head></head>
    <body>
        <div id="container"></div>
        <script>
            WebAssembly
                .instantiateStreaming(fetch("app.wasm"))
                .then(results => {
                    var exports = results.instance.exports;
                    exports.initialize(123,123);
                    var buffer = exports.memory.buffer;
                    document.getElementById("container").innerHTML =
                        `<p>Int32: ${new Uint32Array(buffer)[0]}</p>`+
                        `<p>Int32: ${new BigUint64Array(buffer)[0]}</p>` +
                        `<p>Int32: ${new Float32Array(buffer)[0]}</p>`+
                        `<p>Int32: ${new Float64Array(buffer)[0]}</p>`;
                });
        </script>
    </body>
</html>

我们按照相同的方式将读取出来的四个值转换成HTML进行输出,所以我们在浏览器上看到相同的结果。

image

WebAssembly入门笔记[1]:与JavaScript的交互
WebAssembly入门笔记[2]:利用Memory传递字节数据

写在开头

请聊一聊Java中方法的重写和重载?

这个问题应该是各大厂面试时问的最多的话题之一了,它们几乎贯穿了我们日常的开发工作,在过往的博客中我们多多少少都提到过
重载

重写
,而今天我们就一起来详细的学习一下这二者的功能与区别!

重载与重写的定义

重写:
类实现接口或者子类继承父类时,保持方法签名相同,用不同的方法体来实现不同的功能,这也是Java三大特性之一多态的具体实现,是垂直方向的“类间行为”。

重载:
在同一个类或者父类与子类之间,保持方法名称相同,参数类型,参数数量,参数顺序不同的一种实现,是水平方向上的“类内行为”,同一个类中,或者父子类中。

方法的重写

如下代码中是一个最简单的重写的实现

public class Dog extends Animal{
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.method1();
    }
    @Override
    public void method1() {
        System.out.println("狗子爱奔跑");
    }
}
class Animal {
    public void method1(){
        System.out.println("我是动物!");
    };
}

重写的规则

1、重写发生在子类继承父类
2、参数列表必须完全与被重写方法的相同
3、重写父类方法时,修改方法的权限只能从小范围到大范围
4、返回类型与被重写方法的返回类型可以不相同,但是必须是父类返回值的子类(JDK1.5 及更早版本返回类型要一样,JDK1.7 及更高版本可以不同)
5、访问权限不能比父类中被重写的方法的访问权限更低。如:父类的方法被声明为 public,那么子类中重写该方法不能声明为 protected
6、重写方法不能抛出新的检查异常和比被重写方法申明更宽泛的异常(即只能抛出父类方法抛出异常的子类)
7、声明为 final 的方法不能被重写
8、声明为 static 的方法不能被重写
9、声明为 private 的方法不能被重写

【补充说明】
上面的9条规则中,前面6条应该没什么太多的疑问,访问权限、异常声明范围,子类小于父类,就如同包含与被包含关系,狗子(子类)是动物(父类),那它所拥有的一切自然都要在动物的范畴内!
第7条,final关键字修饰的方法,一旦初始化引用不可变,具体可参考之前发的这篇文章
面试官:小伙子来说一说Java中final关键字
第8点,static修饰的方法不能被重写,这也是一个考点,很好理解,static修饰的方法属于类方法,在调用的时候直接通过类名.方法名即可,而重写也就是多态是基于对象的,一个静态的方法不会联系到任何实例上,所以也就不存在重写了!但是很多面试官为了考验应聘着的细心,会紧接着问一句,那子类中可以有一个和父类方法签名一致的静态方法吗?

答案是肯定的,如果父类中含有一个静态方法,且在子类中也含有一个返回类型、方法名、参数列表均与之相同的静态方法,那么该子类实际上只是
将父类中的该同名方法进行了隐藏,而非重写。 换句话说,父类和子类中含有的其实是两个没有关系的方法,它们的行为也并不具有多态性。

static关键字内容,可以看这篇文章:
深入理解Java中的static关键字

第9条就更没什么好说的了,private表示方法是本类私有,其他类均不可见,更不可能重写了。

方法的重载

重载是发生在编译期间的一种静态绑定,编译器通过方法签名来定位具体重载的哪个方法,如String的valueOf就是一个方法重载的案例典范,上代码!

目前String中valueOf重载的方法已多达十几种,用以实现不同的数据类型转换为字符串的逻辑。

是不是觉得重载很简单?不就是方法名相同的情况下,传入不同参数即可调用不同的重载方法,那么我们看看下面的代码

这时若我们分别调用1、method(),2、method(1),3、method(1L),4、method(null),猜一下答案,嘿嘿

调用1时方法1和方法5均可满足条件(可变参数,参数个数为0或多个),但因为JVM重载方法时优先通过精准匹配进行选择,所以这里会选择方法1;调用2时,方法2、3、4、5均满足,同样因为精准匹配的原因,选择了方法2;调用3时,因为重载方法中没有参数为long类型的,所以会通过子类向上转型继承路线依次匹配,最终调用到了方法4;这里4的调用被注释掉了,原因是报错啦,出现了模糊匹配。

参数null可以匹配任何一个类对象,这里从满足从子类向上转型进行匹配,但在Integer和可变参数的选择上,编译器无法选择,所以编译报错。

由上述的例子其实我们不难看出,在多个重载方法均满足条件时,编译器对于重载的选择是有优先顺序的,下面进行了整理。

编译器选择顺序(从高到底)
精准匹配
基本数据类型,自动转换为更大范围的基本类型
通过装箱与拆箱进行匹配
通过子类向上转型继承路线依次匹配
通过可变参数匹配

重载的规则

1、被重载的方法参数列表(个数或类型)不一样
2、被重载的方法可以修改返回类型
3、被重载的方法可以修改访问修饰符
4、被重载的方法可以修改异常抛出
5、方法能够在同一个类中或者在一个子类中被重载
6、无法以返回值类型作为重载函数的区分标准

【补充说明】
以上6点规则中,前5点比较好理解,第6点也是很多面试官经常会追问的问题,为什么重载的方法,不能将返回值类型作为参考标准。
首先,我们看一下这段代码:

public class Dog {
    public int method1(int a,int b) {
        return a+b;
    }
    public short method1(int a,int b) {
        return (short)(a+b);
    }
}

这段代码中的两个方法,就是保持了返回值类型不同,但提示方法已经被定义,无法重载,说明仅靠返回值类型无法作为方法重载的参考标准!,仔细想想也很容易明白,并不是所有的方法都是有返回值的,难道无返回值的方法重载时,我们还要依赖返回值去判断?
​写到这里,俺又想到了一个问题,
你们觉得main()方法可以重载吗

public class OverloadingMain {
    public static void main(String[] args) {
        System.out.println("String[] args");
    }

    public static void main(String args) {
        System.out.println("String args");
    }

    public static void main() {
        System.out.println("无参");
    }
}

输出:

String[] args

显然,重载是被允许的,但JVM在运行时,只会将那个参数为String[] args的静态main方法作为程序的入口,其他方法只能通过调用去实现打印结果!

重载与重写的区别

基于以上的分析,我们可以精炼出重载与重写的重要区别:
作用范围:
重写的作用范围是父类和子类之间;重载是发生在一个类里面
参数列表:
重载必须不同;重写不能修改
返回类型:
重载可修改;重写方法返回相同类型或子类
抛出异常:
重载可修改;重写可减少或删除,一定不能抛出新的或者更广的异常
访问权限:
重载可修改;重写一定不能做更严格的限制