2024年8月

问题描述

最近发现Flutter中引入像素较大的静态图片或者字体导致调试或者打包之后在高刷手机上帧率只有60的问题。

  • 测试设备为小米13,可在开发者选项中直接打开帧率显示,

  • 也可使用
    statsfl
    插件显示帧率

    StatsFl(
        maxFps: 120, // Support custom FPS target (default is 60)
        align: Alignment.bottomCenter, //Alignment of statsbox
        child: MyApp(),
    ),
    

解决方案

大图片问题

可以适当缩小图片分辨率以及压缩图片。

1、PS中先将图片转换为智能对象,然后调整图像大小(按像素),之后保存图片。这样做可以最大限度保留清晰度。

2、图片压缩网站推荐

字体问题

不能通过静态字体方式,可以将网络字体下载到本地,然后动态加载。

1、字体初始化工具类

import 'dart:io';
import 'package:flutter/services.dart';
import 'package:muen_edu_app/network/dio/dio_instance.dart';
import 'package:muen_edu_app/utils/file_utils.dart';

class FontUtils {
  static FontUtils? _instan;
  final Map<String, String> fontFamilies = {
    "JiangCheng": "https://xxx.ttf",
    "SegoeUI": "https://xxx.ttf",
  };
  final String fontFloder = "fonts";

  FontUtils._();

  static FontUtils get instan => _instan ??= FontUtils._();

  Future initiaFont() async {
    String jiangCheng =
        await FileUtils.ins.getLocalDocumentFile(fontFloder, "JiangCheng.ttf");
    String segoeUI =
        await FileUtils.ins.getLocalDocumentFile(fontFloder, "SegoeUI.ttf");
    await loadFont(File(jiangCheng), "JiangCheng");
    await loadFont(File(segoeUI), "SegoeUI");
  }

  /// 加载字体
  Future loadFont(File fontFile, String fontFamily) async {
    if (!fontFile.existsSync()) {
      // 没有字体,去下载
      await downloadFont(fontFamilies[fontFamily]!, fontFamily);
    }
    Future<ByteData> readFont() async {
      ByteData byteData = (await fontFile.readAsBytes()).buffer.asByteData();
      return byteData;
    }

    FontLoader loader = FontLoader(fontFamily);
    loader.addFont(readFont());
    await loader.load();
  }

  Future<String> downloadFont(String url, String fontFamily) async {
    String savePath =
        await FileUtils.ins.getLocalDocumentFile(fontFloder, '$fontFamily.ttf');
    await DioInstance.instan.download(url, savePath);
    return savePath;
  }
}

2、文件工具类

import 'dart:io';
import 'package:muen_edu_app/network/dio/dio_instance.dart';
import 'package:path_provider/path_provider.dart';

class FileUtils {
  static FileUtils? _ins;

  FileUtils._();
  static FileUtils get ins {
    return _ins ??= FileUtils._();
  }

  /// 获取文档目录文件
  Future<String> getLocalDocumentFile(String folder, String filename) async {
    final dir = await getApplicationDocumentsDirectory();
    return '${dir.path}/$folder/$filename';
  }

  /// 获取临时目录文件
  Future<String> getLocalTemporaryFile(String folder, String filename) async {
    final dir = await getTemporaryDirectory();
    return '${dir.path}/$folder/$filename';
  }

  /// 获取应用程序目录文件
  Future<String> getLocalSupportFile(String folder, String filename) async {
    final dir = await getApplicationSupportDirectory();
    return '${dir.path}/$folder/$filename';
  }
}

3、dio下载

Future<Response> download(
  String url,
  String savePath, {
  CancelToken? cancelToken,
  Options? options,
  void Function(int, int)? onReceiveProgress,
}) async {
  return await _dio.download(
    url,
    savePath,
    onReceiveProgress: onReceiveProgress,
    options: options ??
        Options(
          method: HttpMethods.get,
          responseType: ResponseType.bytes,
          receiveTimeout: _defaultTime,
          sendTimeout: _defaultTime,
        ),
  );
}

4、调用初始化方法

FontUtils.instan.initiaFont();

5、设置全局默认字体

ThemeData(fontFamily: 'JiangCheng');

地址
https://hub.docker.com/r/rainsccc/strtoimg

拉取镜像后,可以启动一个容器来运行该应用程序。以下命令会启动容器并将其端口映射到主机上:

docker run -d -p 5000:5000 rainsccc/strtoimg:latest
  • -d
    :在后台运行容器。
  • -p 5000:5000
    :将容器的 5000 端口映射到主机的 5000 端口。

使用服务

服务启动后,你可以通过 HTTP GET 请求访问它。以下是请求的 URL 模板:

http://localhost:5000/generate_image?text=YOUR_TEXT


YOUR_TEXT
替换为你希望转换的文本,并确保文本已被 URL 编码。例如:

  • javaScript
    const encodedStr = encodeURIComponent(text);
    
http://localhost:5000/generate_image?text=%E6%AD%A3%E5%9C%A8%E5%8A%A0%E8%BD%BD%2C%E8%AF%B7%E7%88%86%E5%90%8E

示例

请求示例

使用
curl
命令请求示例:

curl "http://localhost:5000/generate_image?text=Hello%20World"

响应

请求将返回一个包含文本 “Hello World” 的 PNG 图像。你可以在浏览器中查看或将其保存到本地。

常见问题

  • 图像生成失败

    如果你收到图像生成失败的消息,请检查容器日志。使用以下命令查看容器日志:

    docker logs <container_id>
    
  • 端口冲突

    如果 5000 端口已被其他服务占用,请修改
    -p
    参数中的主机端口,例如:

    docker run -d -p 8080:5000 rainsccc/strtoimg:latest
    

    然后访问
    http://localhost:8080/generate_image?text=YOUR_TEXT

使用说明

  • docker pull rainsccc/strtoimg:latest
    : 从 Docker Hub 拉取最新版本的镜像。
  • docker run -d -p 5000:5000 rainsccc/strtoimg:latest
    : 运行容器并将其 5000 端口映射到主机的 5000 端口。
  • curl "http://localhost:5000/generate_image?text=YOUR_TEXT"
    : 发送 HTTP 请求以生成图像。

一、为什么要修改 kubeadm 证书时间

Kubernetes 官方提供了 kubeadm 工具安装 kubernetes 集群,使用这个工具安装集群非常便捷,使部署和升级 Kubernetes 变得简单起来。

不过该工具有点坑的就是,使用其安装的 kubernetes 集群的大部分证书有效期只有一年,需要在证书过期前,使用更新操作更新集群,使证书的有效期再续一年。如果忘记这个操作,那么在使用过程中证书到期将导致集群不可用,应用无法访问,急急忙忙解决也需要半天时间,这个问题是致命的。

不过实际情况下,在现网环境中大部分人追求稳定,一般不会大改 Kubernetes 版本,所以解决 kubeadm 集群证书有效期只有一年的最好办法就是重新编译 kubeadm 源码,将里面的 1 年有效期修改为 10 年或者 100 年,也不会影响使用 kubeadm 后续的升级,所以修改源码能很好的规避这个证书过期风险。

二、如何查看 kubernetes 证书过期时间

在执行修改 Kubeadm 源码且重新编译之前,我们先通观察下使用的官方的 Kubeadm 工具初始化的 Kubernetes 集群,观察在默认情况下证书过期时间,执行的命令如下:

$ kubeadm alpha certs check-expiration
然后可以看到输出的过期时间如下:


CERTIFICATE                EXPIRES                  RESIDUAL TIME   CERTIFICATE AUTHORITY   EXTERNALLY MANAGED
admin.conf                 Aug 14, 2022 03:15 UTC   364d                                     no
apiserver                  Aug 14, 2022 03:15 UTC   364d             ca                      no
apiserver-etcd-client      Aug 14, 2022 03:15 UTC   364d             etcd-ca                 no
apiserver-kubelet-client   Aug 14, 2022 03:15 UTC   364d             ca                      no
controller-manager.conf    Aug 14, 2022 03:15 UTC   364d                                     no
etcd-healthcheck-client    Aug 14, 2022 03:15 UTC   364d             etcd-ca                 no
etcd-peer                  Aug 14, 2022 03:15 UTC   364d             etcd-ca                 no
etcd-server                Aug 14, 2022 03:15 UTC   364d             etcd-ca                 no
front-proxy-client         Aug 14, 2022 03:15 UTC   364d             front-proxy-ca          no
scheduler.conf             Aug 14, 2022 03:15 UTC   364d                                     no

CERTIFICATE AUTHORITY   EXPIRES                  RESIDUAL TIME   EXTERNALLY MANAGED
ca                      Aug 14, 2031 03:15 UTC   9y             no
etcd-ca                 Aug 14, 2031 03:15 UTC   9y             no
front-proxy-ca          Aug 14, 2031 03:15 UTC   9y             no

从上面可以了解到,默认情况下 ETCD 证书有效期是 10 年时间,其它证书有效期为 1 年时间,所以如果我们安装集群时没有修改证书过期时间,那么默认 1 年后可能会出现证书过期集群不可用的问题,所以接下来我们进入修改 kubeadm 源码过程。

三、修改 kubeadm 源码并重新编译

3.1 安装 Golang 等编译源码的环境包
由于 Kubeadm 是 Go 语言编写的,所以我们提前安装好编译 Kubeadm 源码的工具,操作过程按下面执行即可:

(1) 安装编译工具

$ yum install -y gcc make rsync jq

(2) 下载并配置 Golang 环境

## 下载 golang 1.15.15
$ wget https://dl.google.com/go/go1.15.15.linux-amd64.tar.gz

## 解压并放置在一个目录中
$ tar zxvf go1.15.15.linux-amd64.tar.gz  -C /usr/local

## 编辑 /etc/profile 文件,添加 Go 环境配置内容
$ vi /etc/profile

export GOROOT=/usr/local/go
export GOPATH=/usr/local/gopath
export PATH=$PATH:$GOROOT/bin

## 使配置生效
$ source /etc/profile

## 测试 Go 命令是否配置成功,成功则显示如下
$ go version

go version go1.15.15 linux/amd64

3.2 下载 kubernetes 源码
下载 Kubernetes 源码,然后切换到指定版本,操作的命令如下:

## 下的 kubernetes 源码
$ git clone https://github.com/kubernetes/kubernetes.git

## 进入 Kubernetes 目录
$ cd kubernetes

## 切换 Kubernetes 版本
$ git checkout v1.20.9

3.3 修改 kubeadm 源码中证书过期时间
接下来我们修改 Kubernetes 代码中与 kubeadm 证书有效期相关的源码,操作的命令如下:

(1) 修改 constants.go 文件,操作如下:

$ vim cmd/kubeadm/app/constants/constants.go
########### 后面追加个 * 100 (注掉部分为源代码,后面跟着的是修改后的代码)
#const duration365d = time.Hour * 24 * 365
const duration365d = time.Hour * 24 * 365 * 100

// Config contains the basic fields required for creating a certificate
type Config struct {
        CommonName   string
        Organization []string
        AltNames     AltNames
        Usages       []x509.ExtKeyUsage
}

(2) 修改 cert.go 文件,操作如下:

$ vim staging/src/k8s.io/client-go/util/cert/cert.go
########### 修改10年为100年(注掉部分为源代码,后面跟着的是修改后的代码)
#NotAfter:             now.Add(duration365d * 10).UTC(),
NotAfter:              now.Add(duration365d * 100).UTC(),
KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
BasicConstraintsValid: true,
IsCA:                  true,
......

3.4 执行 kubeadm 编译
使用 make 命令编译 kubeadm, 执行的命令如下:

$ make all WHAT=cmd/kubeadm GOFLAGS=-v

编译成功后的 kubeadm 会放到当前目录中的 ./_output/local/bin/linux/amd64/ 目录中,我们进入到该文件下,查看是否有对应的文件。

## 进入
$ cd ./_output/local/bin/linux/amd64/

## 查看文件列表
$ ls -l

-rwxr-xr-x 10:03 conversion-gen
-rwxr-xr-x 10:03 deepcopy-gen
-rwxr-xr-x 10:03 defaulter-gen
-rwxr-xr-x 10:03 go2make
-rwxr-xr-x 10:04 go-bindata
-rwxr-xr-x 10:04 kubeadm
-rwxr-xr-x 10:47 kubectl
-rwxr-xr-x 10:34 kubelet
-rwxr-xr-x 10:04 openapi-gen
-rwxr-xr-x 10:03 prerelease-lifecycle-gen

四、续签证书

4.1 备份数据

kubectl -n kube-system get cm kubeadm-config -o yaml > kubeadm-config.yaml
$ cp -rp /etc/kubernetes /root/kubernetes_$(date +%F)
$ ls /etc/kubernetes_2024-08-25/
admin.conf  controller-manager.conf  kubelet.conf  manifests  pki  scheduler.conf

4.2 续签证书

  • 查看证书到期时间
$ kubeadm alpha certs check-expiration
[check-expiration] Reading configuration from the cluster...
[check-expiration] FYI: You can look at this config file with 'kubectl -n kube-system get cm kubeadm-config -oyaml'

CERTIFICATE                EXPIRES                  RESIDUAL TIME   CERTIFICATE AUTHORITY   EXTERNALLY MANAGED
admin.conf                 Feb 17, 2025 02:26 UTC   175d                                    no      
apiserver                  Feb 17, 2025 02:26 UTC   175d            ca                      no      
apiserver-etcd-client      Feb 17, 2025 02:26 UTC   175d            etcd-ca                 no      
apiserver-kubelet-client   Feb 17, 2025 02:26 UTC   175d            ca                      no      
controller-manager.conf    Feb 17, 2025 02:26 UTC   175d                                    no      
etcd-healthcheck-client    Feb 17, 2025 02:26 UTC   175d            etcd-ca                 no      
etcd-peer                  Feb 17, 2025 02:26 UTC   175d            etcd-ca                 no      
etcd-server                Feb 17, 2025 02:26 UTC   175d            etcd-ca                 no      
front-proxy-client         Feb 17, 2025 02:26 UTC   175d            front-proxy-ca          no      
scheduler.conf             Feb 17, 2025 02:26 UTC   175d                                    no      

CERTIFICATE AUTHORITY   EXPIRES                  RESIDUAL TIME   EXTERNALLY MANAGED
ca                      Feb 06, 2033 02:38 UTC   8y              no      
etcd-ca                 Feb 06, 2033 02:38 UTC   8y              no      
front-proxy-ca          Feb 06, 2033 02:38 UTC   8y              no 
  • 续签所有证书
./kubeadm alpha  certs renew all
  • 更新kubeconfig证书
$ mv $HOME/.kube/config $HOME/.kube/config.old
$ cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
$ chown $(id -u):$(id -g) $HOME/.kube/config
  • 再次查看证书到期时间
$ kubeadm alpha certs check-expiration
[check-expiration] Reading configuration from the cluster...
[check-expiration] FYI: You can look at this config file with 'kubectl -n kube-system get cm kubeadm-config -oyaml'

CERTIFICATE                EXPIRES                  RESIDUAL TIME   CERTIFICATE AUTHORITY   EXTERNALLY MANAGED
admin.conf                 Aug 01, 2124 02:58 UTC   99y                                     no      
apiserver                  Aug 01, 2124 02:58 UTC   99y             ca                      no      
apiserver-etcd-client      Aug 01, 2124 02:58 UTC   99y             etcd-ca                 no      
apiserver-kubelet-client   Aug 01, 2124 02:58 UTC   99y             ca                      no      
controller-manager.conf    Aug 01, 2124 02:58 UTC   99y                                     no      
etcd-healthcheck-client    Aug 01, 2124 02:58 UTC   99y             etcd-ca                 no      
etcd-peer                  Aug 01, 2124 02:58 UTC   99y             etcd-ca                 no      
etcd-server                Aug 01, 2124 02:58 UTC   99y             etcd-ca                 no      
front-proxy-client         Aug 01, 2124 02:58 UTC   99y             front-proxy-ca          no      
scheduler.conf             Aug 01, 2124 02:58 UTC   99y                                     no      

CERTIFICATE AUTHORITY   EXPIRES                  RESIDUAL TIME   EXTERNALLY MANAGED
ca                      Feb 06, 2033 02:38 UTC   8y              no      
etcd-ca                 Feb 06, 2033 02:38 UTC   8y              no      
front-proxy-ca          Feb 06, 2033 02:38 UTC   8y              no  

完成后重启 kube-api server、kube-controller、kube-scheduler、etcd 这 4 个容器即可,我们可以查看 apiserver 的证书的有效期来验证是否更新成功:

$ echo | openssl s_client -showcerts -connect 127.0.0.1:6443 -servername api 2>/dev/null | openssl x509 -noout -enddate
notAfter=Aug  1 02:58:33 2124 GMT

可以看到现在的有效期是100年以后,证明已经更新成功。

  • 也可以直接获取所有证书的过期时间
$ for item in `find /etc/kubernetes/pki -maxdepth 2 -name "*.crt"`;do openssl x509 -in $item -text -noout| grep Not;echo ======================$item===============;done

            Not Before: Feb  9 02:38:42 2023 GMT
            Not After : Feb  6 02:38:42 2033 GMT
======================/etc/kubernetes/pki/front-proxy-ca.crt===============
            Not Before: Feb  9 02:38:43 2023 GMT
            Not After : Aug  1 02:58:33 2124 GMT
======================/etc/kubernetes/pki/apiserver-etcd-client.crt===============
            Not Before: Feb  9 02:38:41 2023 GMT
            Not After : Feb  6 02:38:41 2033 GMT
======================/etc/kubernetes/pki/ca.crt===============
            Not Before: Feb  9 02:38:41 2023 GMT
            Not After : Aug  1 02:58:34 2124 GMT
======================/etc/kubernetes/pki/apiserver-kubelet-client.crt===============
            Not Before: Feb  9 02:38:41 2023 GMT
            Not After : Aug  1 02:58:33 2124 GMT
======================/etc/kubernetes/pki/apiserver.crt===============
            Not Before: Feb  9 02:38:42 2023 GMT
            Not After : Aug  1 02:58:36 2124 GMT
======================/etc/kubernetes/pki/front-proxy-client.crt===============
            Not Before: Feb  9 02:38:43 2023 GMT
            Not After : Aug  1 02:58:36 2124 GMT
======================/etc/kubernetes/pki/etcd/server.crt===============
            Not Before: Feb  9 02:38:43 2023 GMT
            Not After : Aug  1 02:58:35 2124 GMT
======================/etc/kubernetes/pki/etcd/peer.crt===============
            Not Before: Feb  9 02:38:43 2023 GMT
            Not After : Feb  6 02:38:43 2033 GMT
======================/etc/kubernetes/pki/etcd/ca.crt===============
            Not Before: Feb  9 02:38:43 2023 GMT
            Not After : Aug  1 02:58:34 2124 GMT
======================/etc/kubernetes/pki/etcd/healthcheck-client.crt===============
  • 高可用k8s master节点需要将以下证书拷贝至其他master节点并重启 kube-api server、kube-controller、kube-scheduler、etcd 4 个容器。
cp  /root/etc/kubernetes/pki/ca.* /etc/kubernetes/pki/
cp  /root/etc/kubernetes/pki/sa.* /etc/kubernetes/pki/
cp  /root/etc/kubernetes/pki/front-proxy-ca.* /etc/kubernetes/pki/
cp  /root/etc/kubernetes/admin.conf /etc/kubernetes/admin.conf
cp  /etc/kubernetes/admin.conf /root/.kube/config

部分转载至
http://www.mydlq.club/article/118/

一:背景

1. 讲故事


.NET高级调试
的旅程中,我常常会与 Bitmap 短兵相接,它最大的一个危害就是会让程序抛出匪夷所思的
OutOfMemoryException
,也常常会让一些.NET开发者们陷入其中不能自拔,痛不欲生,基于此,这一篇我从dump分析的角度给大家深挖一下 Bitmap 背后的故事。

二:Bitmap 背后的故事

1. Bitmap 能吃多少内存

相信有很多朋友都知道 bitmap 吃的是非托管内存,但相信也有很多朋友不知道这玩意竟然能吃掉bitmap自身大小的几十倍,甚至上百倍。可能这么说有点抽象,举一个例子说明一下,用 chatgpt 生成的参考代码如下:


static void Main(string[] args)
{
    // 创建一个新的Bitmap对象,大小为100x100像素  
    Bitmap bitmap = new Bitmap(21000, 21000);

    // 获取Bitmap的Graphics对象,用于绘制  
    using (Graphics g = Graphics.FromImage(bitmap))
    {
        // 设置背景色为蓝色  
        g.Clear(Color.Blue);

        // 示例:在Bitmap上绘制一个红色的圆  
        // 设置画笔颜色为红色  
        using (Pen pen = new Pen(Color.Red, 10000)) // 10为画笔粗细  
        {
            // 绘制圆,圆心为(50, 50),半径为30  
            g.DrawEllipse(pen, 10000, 10000, 15000, 15000);
        }

        // 示例:在Bitmap上绘制文本  
        // 设置字体  
        using (Font font = new Font("Arial", 1600))
        {
            // 设置画刷颜色为白色  
            using (Brush brush = new SolidBrush(Color.White))
            {
                // 在Bitmap上绘制文本,位置为(10, 70)  
                g.DrawString("Hello, Bitmap!", font, brush, new PointF(100, 700));
            }
        }
    }

    // 保存Bitmap到文件  
    bitmap.Save("example.png", System.Drawing.Imaging.ImageFormat.Png);

    Console.ReadLine();

    // 释放Bitmap资源  
    bitmap.Dispose();

    Console.WriteLine("Bitmap saved as example.png");

    Debugger.Break();
    Console.ReadLine();
}


bitmap.Dispose();
之前加上一个
Console.ReadLine();
故意不销毁 bitmap 来观察下内存消耗,真是不看不知道,一看吓一跳,居然吃了高达 1.7G 的内存。

接下来按一下 Enter 观察一下 bitmap 在磁盘上的大小,居然小到无语的2M ,这差距咂舌的 1000 倍啊,截图如下:

这就是 bitmap 的恐怖之处,也是很多程序员疑惑的地方。

2. Bitmap 吃的是哪里的内存

纵然有很多朋友知道是非托管内存,但还是有必要用数据来展示一下,这个非常简单,可以用
!address -summary
观察下提交内存,用
!eeheap -gc
观察下托管堆即可。


0:006> !address -summary

--- Type Summary (for busy) ------ RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_MAPPED                              168      200`03998000 (   2.000 TB)  88.58%    1.56%
MEM_PRIVATE                              96       42`01319000 ( 264.019 GB)  11.42%    0.20%
MEM_IMAGE                               265        0`03820000 (  56.125 MB)   0.00%    0.00%

--- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_FREE                                 73     7dbd`f7b1f000 ( 125.742 TB)           98.24%
MEM_RESERVE                              83      241`94389000 (   2.256 TB)  99.92%    1.76%
MEM_COMMIT                              446        0`74148000 (   1.814 GB)   0.08%    0.00%

0:006> !eeheap -gc

========================================
Number of GC Heaps: 1
----------------------------------------
....
------------------------------
GC Allocated Heap Size:    Size: 0x1d7f8 (120824) bytes.
GC Committed Heap Size:    Size: 0x45000 (282624) bytes.

从卦中可以清晰的看到
MEM_COMMIT=1.814 GB
同时
GC Committed Heap Size=2.8M
,妥妥的非托管泄漏。

3. 能找到 Bitmap 所属的内存段吗

要想知道 bitmap 所侵占的内存段,如果用 windbg 去调试的话,可以对
KERNELBASE!VirtualAlloc
下一个 bp 断点即可,参考如下:


0:000> k 5
 # Child-SP          RetAddr               Call Site
00 00000010`5257e198 00007ffb`c2ec7662     KERNELBASE!VirtualAlloc
01 00000010`5257e1a0 00007ffb`c2ec684b     gdiplus!GpMemoryBitmap::AllocBitmapData+0xc6
02 00000010`5257e1e0 00007ffb`c2e8a355     gdiplus!GpMemoryBitmap::AllocBitmapMemory+0x3f
03 00000010`5257e220 00007ffb`c2e8a47a     gdiplus!GpMemoryBitmap::InitNewBitmap+0x49
04 00000010`5257e260 00007ffb`c2e8a2cb     gdiplus!CopyOnWriteBitmap::CopyOnWriteBitmap+0x8a
...

但可惜的是你拿到的是 dump 文件,无法使用 bp 下断点,那怎么办呢?只要这辈子积攒的福报够多,自然不会有绝人之路,首先从托管类 Bitmap 上挖起。


0:000> !DumpObj /d 000001ef0b809648
Name:        System.Drawing.Bitmap
MethodTable: 00007ffa86f0cf90
EEClass:     00007ffa86f34760
Tracked Type: false
Size:        40(0x28) bytes
File:        D:\code\MyCode\ConsoleApplication1\bin\x64\Debug\net8.0\System.Drawing.Common.dll
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
00007ffa86e370a0  400019c       18        System.IntPtr  1 instance 000001EF08B222F0 _nativeImage
00007ffa86d85fa8  400019d        8        System.Object  0 instance 0000000000000000 _userData
00007ffa86fc01a8  400019e       10        System.Byte[]  0 instance 0000000000000000 _rawData
00007ffa86f0cee8  4000014       10 System.Drawing.Color  1   static 0000000000000000 s_defaultTransparentColor

从 Bitmap 的字段布局来是用 _nativeImage 字段来持有着对原生 bitmap 的引用,下面的截图也可以佐证。

说了这么多,其实我想表达的是什么呢?虽然我不知道 gdiplus 的底层源码,但有一点可以确认的是,VirtualAlloc 返回的 ptr 和 这里的 _nativeImage 肯定是有偏移关系的,有可能是一级关系,有可能是 二级关系,在我的内存地址视察下,总结如下:

  • 在 Windows10 x64 环境下偏移为
    +0x570
  • 在 Windows10 x86 环境下偏移为
    +0x2e8

接下来就可以在 windbg 中轻松做验证,先拦截 VirtualAlloc 找到大的地址段。


0:000> bp KERNELBASE!VirtualAlloc ".if (@rdx>=0x200000) {  .printf  \"============ %lu bytes  ================\\n\",@rdx; k } .else {gc}"
breakpoint 0 redefined

0:000> g
============ 1764000000 bytes  ================
 # Child-SP          RetAddr               Call Site
00 00000060`d9f7e7b8 00007ffb`c2ec7662     KERNELBASE!VirtualAlloc
01 00000060`d9f7e7c0 00007ffb`c2ec684b     gdiplus!GpMemoryBitmap::AllocBitmapData+0xc6
02 00000060`d9f7e800 00007ffb`c2e8a355     gdiplus!GpMemoryBitmap::AllocBitmapMemory+0x3f
03 00000060`d9f7e840 00007ffb`c2e8a47a     gdiplus!GpMemoryBitmap::InitNewBitmap+0x49
04 00000060`d9f7e880 00007ffb`c2e8a2cb     gdiplus!CopyOnWriteBitmap::CopyOnWriteBitmap+0x8a
05 00000060`d9f7e8c0 00007ffb`c2e8a1b4     gdiplus!GpBitmap::GpBitmap+0x6b
06 00000060`d9f7e900 00007ffa`86e91f95     gdiplus!GdipCreateBitmapFromScan0+0xc4

0:000> pt
KERNELBASE!VirtualAlloc+0x5a:
00007ffb`c25df28a c3              ret

0:000> r
rax=0000020759db0000 rbx=0000000000014820 rcx=00007ffbc4acd3c4
rdx=0000000000000000 rsi=000000000026200a rdi=000001c6c4bb2d20
rip=00007ffbc25df28a rsp=00000060d9f7e7b8 rbp=0000000000005208
 r8=00000060d9f7e778  r9=0000000000005208 r10=0000000000000000
r11=0000000000000246 r12=0000000000005208 r13=0000000000000004
r14=0000000000005208 r15=0000000069248100
iopl=0         nv up ei pl nz na po nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
KERNELBASE!VirtualAlloc+0x5a:
00007ffb`c25df28a c3              ret

0:000> !address 0000020759db0000

Usage:                  <unknown>
Base Address:           00000207`59db0000
End Address:            00000207`c2ff9000
Region Size:            00000000`69249000 (   1.643 GB)
State:                  00001000          MEM_COMMIT
Protect:                00000004          PAGE_READWRITE
Type:                   00020000          MEM_PRIVATE
Allocation Base:        00000207`59db0000
Allocation Protect:     00000004          PAGE_READWRITE


Content source: 1 (target), length: 69249000

从卦中可以看到分配的地址段的首地址为
0000020759db0000
,解析来到
Bitmap._nativeImage+0x570
处做个验证即可,可以看到遥相呼应,输出如下:


0:000> !DumpObj /d 000001c6c7409648
Name:        System.Drawing.Bitmap
MethodTable: 00007ffa86f4cf90
EEClass:     00007ffa86f74760
Tracked Type: false
Size:        40(0x28) bytes
File:        D:\code\MyCode\ConsoleApplication1\bin\x64\Debug\net8.0\System.Drawing.Common.dll
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
00007ffa86e770a0  400019c       18        System.IntPtr  1 instance 000001C6C4BB25B0 _nativeImage
00007ffa86dc5fa8  400019d        8        System.Object  0 instance 0000000000000000 _userData
00007ffa870001a8  400019e       10        System.Byte[]  0 instance 0000000000000000 _rawData
00007ffa86f4cee8  4000014       10 System.Drawing.Color  1   static 0000000000000000 s_defaultTransparentColor

0:000> dp 000001C6C4BB25B0+0x570 L2
000001c6`c4bb2b20  00000207`59db0000 00000000`00000003

三:总结

Bitmap使用不当危害巨大,所以一定要谨记
尽早释放
的原则,如果真的不幸被吃了很多内存,也一定要明白那些未知的大内存段是不是被 Bitmap 所关联,从而尽早的找到真正的祸根。
图片名称

一、简介

在开发中可能会遇到这样一类场景,业务复杂度不算太高,技术难度不算太深,但是做起来就很容易把人整破防,伤害很高侮辱性很强的:绘图。

绘图最怕有人挑刺:这里变形,那里不对,全图失真。

最近在处理这样一个场景,使用Java的Graphics2D类,绘制业务需要的图形模板,然后在具体流程中填充数据,并且将图形存储起来,逻辑并不复杂,由于涉及ToC和ToB两端交互,必须用点雕花的态度。

二、字体安装

在绘制具体图形时,需要先处理好本地字体,使用设计师提供的字体,才可能在图片上复制出想要的效果;安装完相关的字体包,使用Java读取验证后再直接使用。

public class Typeface {
    public static void main(String[] args) {
        List<String> fontNames = new java.util.ArrayList<>();
        Font[] fonts = GraphicsEnvironment.getLocalGraphicsEnvironment().getAllFonts();
        for (Font font : fonts){
            fontNames.add(font.getName());
        }
        fontNames.forEach(System.out::println);
    }
}

三、绘制图形

在制图中,会涉及一些简单的图形样式,比如线条、矩形、圆弧线等,这些都可以使用
Graphics2D
的语法直接生成,下面的程序创建一张500x500的图片,然后在其中绘制一些简单的图形样式,最后保存到本地。

public class DrawDraft {
  public static void main(String[] args) throws Exception {
    // 1、创建图片绘图
    BufferedImage image = new BufferedImage(500, 500, BufferedImage.TYPE_4BYTE_ABGR);
    Graphics2D graphics = image.createGraphics();
    graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
    graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);

    // 2、填充背景色
    graphics.setColor(Color.white);
    graphics.fillRect(0, 0, 500, 500);

    // 3、绘制线条
    graphics.setStroke(new BasicStroke(3));
    graphics.setColor(Color.red);
    graphics.drawLine(50, 50, 280, 50);
    graphics.setColor(Color.blue);
    graphics.drawLine(50, 50, 165, 200);
    graphics.setColor(Color.green);
    graphics.drawLine(280, 50, 165, 200);

    // 4、绘制图形
    graphics.setStroke(new BasicStroke(2));
    graphics.setColor(Color.pink);
    graphics.drawRect(200, 200, 80, 50);// 矩形

    graphics.setColor(Color.green);
    graphics.drawArc(280, 280, 100, 100, 0, 180);//圆弧线
    graphics.drawArc(300, 300, 100, 50, 0, -270);//圆弧线弧度

    graphics.setColor(Color.orange);
    graphics.drawArc(350, 350, 100, 100, 0, 180);//圆弧线
    graphics.fillArc(350, 350, 100, 100, 0, -270);//填充四分之三的圆形

    // 5、写到图片
    ImageIO.write(image, "png", new File("src/main/draw-draft.png"));

    image.flush();
    graphics.dispose();
  }
}

四、绘制文本

在常规的业务场景中,一般是先绘制模版图形,然后在模板的图形上填充数据,也可以直接使用设计师提供的模板文件,这样可以避免数据填充时出现排版问题,如果有大量的动态数据内容,可以使用模板引擎,这在以前的内容中有写个类似的案例。

下面这个案例,使用上面的模板,在此模版上进行文本添加,绘制文本主要就是一些动态对齐和排版等问题,最后制图生效时添加签章即可。

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.File;
import javax.imageio.ImageIO;

public class DrawImage {

  public static void main(String[] args) throws Exception {
    // 1、基础样式
    Font yhFont = new Font("Microsoft Yahei UI", Font.PLAIN, 15);
    Font yhBoldFont = new Font("Microsoft Yahei UI Bold", Font.BOLD, 25);
    Font tailFont = new Font("Microsoft Yahei UI Bold", Font.PLAIN, 12);

    // 2、基于底图绘制
    BufferedImage backImg = ImageIO.read(new File("src/main/draw-draft.png"));
    int canvasWidth = backImg.getWidth();
    int canvasHeight = backImg.getHeight();

    // 3、创建画笔
    Graphics2D graphics = backImg.createGraphics();
    graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
    graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);

    // 4、绘制居中标题
    graphics.setFont(yhBoldFont);
    graphics.setColor(Color.BLACK);
    String title = "2D绘图";
    int titleWidth = graphics.getFontMetrics().stringWidth(title);
    int titleX = canvasWidth/2-titleWidth/2;
    int titleY = 50;
    graphics.drawString(title, titleX, titleY);

    // 5、绘制长文本,左对齐和换行
    graphics.setFont(yhFont);
    graphics.setColor(Color.BLACK);
    String blackText = "\u3000组织需要重新审视项目的核心价值主张,以便更好地与利益相关者对齐目标,协同共创。";
    String[] textWord = blackText.split("");
    // 文本最大宽度和行高
    int textMaxWidth = 200;
    int textLineHeight = 18;
    // 文本字符输出起始坐标
    int textWordX = 20;
    int textWordY = 350;
    // 通过计算控制单行文本长度
    StringBuilder textLine = new StringBuilder();
    for (String word : textWord){
      graphics.drawString(word, textWordX, textWordY);
      if (graphics.getFontMetrics().stringWidth(textLine + word) <= textMaxWidth) {
        // 不需要换行,记录单行内容,移动X坐标
        textLine.append(word);
        textWordX = textWordX + graphics.getFontMetrics().stringWidth(word);
      } else {
        // 需要换行,重置当行文本内容,移动X坐标和Y坐标
        textLine.setLength(0);
        textWordX = 20 ;
        textWordY = textWordY+textLineHeight;
      }
    }

    // 6、绘制短文本,右对齐
    graphics.setFont(tailFont);
    graphics.setColor(Color.BLUE);
    String author = "制图方:白天睡不着";
    int authorWidth = canvasWidth-30-graphics.getFontMetrics().stringWidth(author);
    graphics.drawString(author, authorWidth, 180);
    String drawDate = "时间:2024年8月28日";
    int drawDateWidth = canvasWidth-30-graphics.getFontMetrics().stringWidth(drawDate);
    graphics.drawString(drawDate, drawDateWidth, 200);

    // 7、添加水印图片
    BufferedImage watermarkImg = ImageIO.read(new File("src/main/watermark.png"));
    graphics.drawImage(watermarkImg, 350, 120,120,120, null);

    // 8、写到图片
    ImageIO.write(backImg, "png", new File("src/main/draw-img.png"));
    backImg.flush();
    watermarkImg.flush();
    graphics.dispose();
  }
}

彩蛋:这里
blackText
文本是让大模型随机写的,就冲这个输出和味道,大家猜猜出自哪个国产大模型,(放水提示词:国产)。最后关于文件管理就不赘述了,哪个文件服务器方便,就随地存着。

五、源码参考

文档仓库:
https://gitee.com/cicadasmile/butte-java-note

源码仓库:
https://gitee.com/cicadasmile/butte-spring-parent