Android CMake 项目集成 Ccache

Ccache is a compiler cache. 能极大的提升 clean build 编译效率。

github 上有一个为 CMake 提供 Ccache 集成的项目:https://github.com/TheLartians/Ccache.cmake,可以拿过来直接用。

在 cpp 库根目录的 CMakeLists.txt 中添加Ccache.cmake,如:

src/main/cpp/CMakeLists.txt
1
2
3
4
5
6
7
8
9
10
11

cmake_minimum_required(VERSION 3.11.0)

project(myapp)

include(FetchContent)
FetchContent_Declare(Ccache.cmake
GIT_REPOSITORY https://github.com/TheLartians/Ccache.cmake.git
GIT_TAG origin/master
)
FetchContent_MakeAvailable(Ccache.cmake)

注意:FetchContent 最低要求 CMake 3.11

下载安装 Ccache: https://ccache.dev/download.html ,或者使用 Homebrew 安装:brew install ccache

安装完毕后,在 Android 项目的 build.gradle 中记得加上参数开启次模块功能:-DUSE_CCACHE=ON,并给Ccache 加上选项-DCCACHE_OPTIONS=CCACHE_CPP2=true;CCACHE_SLOPPINESS=time_macros,locale,file_stat_matches

build.gradle
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
}

android {
namespace 'com.example.myapplication'
compileSdk 33

defaultConfig {
applicationId "com.example.myapplication"
minSdk 21
targetSdk 33
versionCode 1
versionName "1.0"

externalNativeBuild {
cmake {
arguments "-DUSE_CCACHE=ON", "-DCCACHE_OPTIONS=CCACHE_CPP2=true;CCACHE_SLOPPINESS=time_macros,locale,file_stat_matches"
}
}
}

externalNativeBuild {
cmake {
path file('src/main/cpp/CMakeLists.txt')
version '3.22.1'

}
}

}

开启 ccache stats log,clean build,日志如下表示命中缓存:

# ~/MyApplication/app/src/main/cpp/native-lib.cpp
direct_cache_hit
local_storage_hit
local_storage_read_hit
local_storage_read_hit

ENJOY it.

沉迷 Midjourney

近些日子沉迷 Midjourney,用不同咒语来回调教 AI,也算体验了一把炼丹的乐趣 —— 投喂一堆奇奇怪怪的东西,得一个匪夷所思的玩意。

推荐一个 Notion 笔记,总结了非常多的风格、建筑、艺术家、摄影技法等等关键字,用来辅助调教:https://marigoldguide.notion.site/marigoldguide/52ac9968a8da4003a825039022561a30?v=057d3669790c4dc28bd8d3ddf35e3a37

比如:
Castle Claude Monet –seed 9875
Castle in the style of Claude Monet --seed 9875

另外还有一个可视化组织咒语的工具网站,也很好用:MidJourney Prompt Helper


下面是我用使用 Midjourney 画的图:

泉州蟳埔女,簪花围:

a pretty young woman smells flower on chair at sunset

/imagine prompt: https://s.mj.run/t3qtGrXY96c, a pretty young woman smells flower on chair at sunset, --q 2 --ar 9:16

蓝眼泪:

In the midnight, the ocean beach

/imagine prompt: https://s.mj.run/gGNA5xDRpCc In the midnight, the ocean beach, rippling light-blue waves, shining stars, weak lights, movie lights, HD, 4k, --ar 16:9 --no moon

宇航员月球种土豆:

astronaut planting potatoes on the moon

/imagine prompt: astronaut planting potatoes on the moon, nebula shines through glass dome, high detail, edge lighting, movie lighting, oc render, 8K --ar 5:3

如何快速定位 Flutter APP 内存泄漏

当一个 APP 随着业务发展,无用功能越堆越多,参与开发的人员队伍越发壮大,代码大爆炸式膨胀,尽管有一系列的例如人工review、代码规范、静态lint检查之类流程上的克制手段,也止不住坍缩的小宇宙往深不可测无法捉摸的黑洞演进。其中,最为令人头疼的问题莫过于“内存泄漏”。

什么是内存泄漏?很常见却也很不起眼,你我随手写段代码就能轻松让一个对象被垃圾回收器(GC)视而不见。

如下面这一段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class WidgetA extends StatefulWidget {
...
@override
_WidgetAState createState() => _WidgetAState();
}

class _WidgetAState extends State<WidgetA> {
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () async {
var r = await fetchResult();
if (mounted) {
setState(() {
this.result = r;
});
}
},
child: ...
);
}
}

你能在阅读完这段代码的同时找到那个可能被泄漏的对象吗?

没错,context 可能会短时间泄漏
但,得益于 Dart 积极高效的 GC 策略,这种泄漏已经算是内存泄漏问题中的可以忽略不计的轻症了。(扩展阅读:Flutter: Don’t Fear the Garbage Collector

又如这一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
class _WidgetAState extends State<WidgetA> {
@override
void initState() {
super.initState();
Provider.of<AuthStateBloc>(context).stream.listen(onAuthStateChanged);
}
void onAuthStateChanged(AuthState state) {
if (mounted) {
setState(() {
this.state = state;
});
}
}

有着丰富的 Flutter 开发经验的你,还是很轻松地就发现了,stream.listen 的返回对象StreamSubscription没有被正确地cancel,导致_WidgetAState 和它的 Element (context) 可能都泄漏了,直到祖节点的 Provider<AuthStateBloc> 被卸载才可能被 GC 回收。

那么回到标题,有没有工具可以程序化的发现内存泄漏呢?

首先要解决的问题是怎么找到内存泄漏点?

Flutter Raw Image Provider

Flutter 中的 Image Widget 内置支持 file、network、memory三种形式的文件。
但这几种都只支持常规的经过压缩后的图片文件或二进制数据,如jpg、png、webp文件等。并没有支持原始的rgba 二进制数据。
这里说的原始二进制数据是指图像的每个像素的色彩值所组成的字节数组。一张图有宽x高个像素点,一个像素点的色彩值用32bit来存储,分为4个通道,每个通道各占用8bit,分别为红、绿、蓝、透明度(RGBA),这个数组就是每个像素点色彩值的集合,dart 中一般用Uint8List

一般情况下,考虑网络传输效率,会采用算法来压缩这个数据,故而你会看到有各种各样的图像压缩算法和文件格式。

你可能会问什么情况下会有需要直接去加载一张图的原始rgba数据?

这里举个简单例子:分块加载图片。将图片解码后,分割成一个个矩形区域,每个矩形就有一个 raw rgba 数据,将其交给Image渲染,这样做可以降低一定的GPU 内存压力,减少出现GPU OOM 或黑屏的概率。(可以参考我的试验项目:https://github.com/yrom/image_pieces)

要支持 raw rgba ,其实很简单,在 dart:ui包下有个方法decodeImageFromPixels可以直接使用,前提是需要有原始的二进制数据、宽、高。

1
2
3
4
5
6
7
8
9
10
11
12
13
import 'dart:ui';

Future<Image> decodeRawRgba(ByteData bytes, int width, int height) {
final Completer<Image> completer = Completer<Image>();
decodeImageFromPixels(
bytes.buffer.asUint8List(),
width,
height,
PixelFormat.rgba8888,
completer.complete,
);
return completer.future;
}

有了这个 Image(dart:ui)对象就可以交给 RawImage Widget 来加载了。但RawImage太过于底层了,能不能只用 Image Widget呢?因为需要复用 LoadingBuilder这些逻辑。

当然可以。查看一下 Image Widget 的构造函数就知道,我们需要一个 ImageProvider,那么问题进一步简化到如何写一个ImageProvider 支持 raw rgba 数据。

实现一个 ImageProvider,我们需要实现 load这个关键方法。以MemoryImage为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MemoryImage extends ImageProvider<MemoryImage> {
@override
ImageStreamCompleter load(MemoryImage key, DecoderCallback decode) {
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, decode),
scale: key.scale,
debugLabel: 'MemoryImage(${describeIdentity(key.bytes)})',
);
}

Future<ui.Codec> _loadAsync(MemoryImage key, DecoderCallback decode) {
return decode(bytes);
}
}

很显然,我们需要想一个方法构造出raw rgba 数据的 Codec

瞎拍 - 顾村寻花

植树节刚过,上海也结束了细雨绵绵的天气,稍稍放晴,正好周末,前往顾村公园看看樱花开了没。

到了公园,花是没见着,人却许多。

紫叶李?

往深处走,这种长着红叶的树,簇着花,密密麻麻,独占风头。百度识图告诉说是“紫叶李”,希望没认错。

紫叶李?


粉色的摩天轮,在上面看樱花视野应该不错,想想而已。


还有这种白如凝脂的花,点缀公园,在一众光溜溜的树干里煞是好看。

瞎拍 - 留沪过年

过年前疫情闹得凶,吓得我把回家票给退了……

谁知上海还是比较给力,没有扩散,却再买不着回去的票,只好一边上班一边摸鱼用盒马囤了许多年货。

一人一猫留沪过年
就这样,一人一猫留沪过年。

年夜饭
细雨声中,捣鼓好了一个人的年夜饭。

http/2 over http tunnel proxy for Dart

Flutter 项目里用到了 http2 包 (https://pub.dev/packages/http2) 。但官方似乎没有支持 http proxy ,转念一想,似乎也对,proxy 这层不属于 http2 协议该负责实现的事。

研究了一下 http/1.1 tunnel proxy 的协议(RFC 2817)发现其实挺简单的。

这里直接放出示例了,仅供参考哦(需要登录的 proxy 就留给你自己实现了)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:http2/http2.dart';

void main() async {
var uri = Uri.parse('https://cn.bing.com/search?q=http2');
var transport = await connectTunnel('127.0.0.1', 8888, uri.host);
var headers = [
Header.ascii(':method', 'GET'),
Header.ascii(':path', uri.hasQuery ? uri.path + '?' + uri.query : uri.path),
Header.ascii(':scheme', uri.scheme),
Header.ascii(':authority', uri.host),
];
var stream = transport.makeRequest(headers, endStream: true);
var bodyStream = StreamController<List<int>>();
utf8.decoder.bind(bodyStream.stream).listen((body) {
print(body);
});
await for (var message in stream.incomingMessages) {
if (message is HeadersStreamMessage) {
for (var header in message.headers) {
var name = utf8.decode(header.name);
var value = utf8.decode(header.value);
print('Header: $name: $value');
}
} else if (message is DataStreamMessage) {
bodyStream.add(message.bytes);
if (message.endStream) {
bodyStream.close();
}
}
}
if (!bodyStream.isClosed) {
await bodyStream.close();
}
await transport.finish();
}

Future<ClientTransportConnection> connectTunnel(String proxyHost, int proxyPort, String targetHost,
[int targetPort = 443]) async {
var proxy = await Socket.connect(proxyHost, proxyPort, timeout: const Duration(seconds: 1));
const CRLF = "\r\n";
proxy.write("CONNECT $targetHost:$targetPort HTTP/1.1"); // request line
proxy.write(CRLF);
proxy.write("Host: $targetHost:$targetPort"); // header
proxy.write(CRLF);
proxy.write(CRLF);
var completer = Completer<bool>.sync();
var sub = proxy.listen((event) {
var response = ascii.decode(event);
var lines = response.split(CRLF);
// status line
var statusLine = lines.first;
if (statusLine.startsWith("HTTP/1.1 200")) {
completer.complete();
} else {
completer.completeError(statusLine);
}
}, onError: completer.completeError);
await completer.future; // established
await sub.pause();

var socket = await SecureSocket.secure(proxy, host: targetHost, supportedProtocols: const ["h2"]);
return ClientTransportConnection.viaSocket(socket);
}

用一个简单示例告诉你怎样提升 Flutter App 中的动画性能

观前提醒:本文假设你已经有一定的 Flutter 开发经验,对Flutter 的 Widget,RenderObject 等概念有所了解,并且知道如何开启 DevTools。

现有一个简单的汽泡动画需要实现,如下图:

bubble animation

直接通过 AnimationController 实现

当看到这个效果图的时候,很快啊,啪一下思路就来了。涉及到动画,有状态,用 StatefulWidget ,State 里创建一个 AnimationController,用两个 Container 对应两个圈,外圈的 Container 的宽高监听动画跟着更新就行。
代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
const double size = 56;

class BubbleAnimationByAnimationController extends StatefulWidget {
@override
_BubbleAnimationByAnimationControllerState createState() => _BubbleAnimationByAnimationControllerState();
}

class _BubbleAnimationByAnimationControllerState extends State<BubbleAnimationByAnimationController>
with SingleTickerProviderStateMixin {
AnimationController _controller;

@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 1),
vsync: this,
)..addListener(() => setState(() {}));
_controller.repeat(reverse: true);
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
// 两个 `Container` 对应两个圈
return Container(
alignment: Alignment.center,
constraints: BoxConstraints.tight(
Size.square((1 + _controller.value * 0.2) * size),
),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.blue[200],
),
child: Container(
alignment: Alignment.center,
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.blue,
),
width: size,
height: size,
child: Text(
'Hello world!',
style: TextStyle(color: Colors.white, fontSize: 12),
),
),
);
}
}

介绍一位新成员

雄赳赳气昂昂
过年立的 Flag —— 拥有一只猫主子 —— 达成。撒花 ╮( ̄▽ ̄)╭

不要抢我的玩具!
一只原主人卖不出去的雄性布偶。