0%

家用宽带公网 ipv6 踩坑指南

1. 申请宽带 ipv6 支持

一般移动设备流量都支持 ipv6,家用宽带可能要申请打开,并且桌面端网卡的 ipv6 支持也要打开。

例:可以查询 windows 网卡如何打开 ipv6 支持,放行 ipv6 流量。

获取公网 ipv6 地址

一般家庭网络终端的拓扑结构是这样的:

1
ISP -> 调制解调器(光猫) -> 路由器 -> 终端

因为 ipv6 数量足够,所以路由器可能不会做 ipv6 的端口转发(不过可以通过 socat 等实现,就是麻烦)

我这里选择公网 ip6 + 防火墙方式。想要终端获取到公网 ipv6 地址,可以有以下几种方式:

光猫拨号

  1. 光猫从它的 WAN 口(光纤口)通过 DHCPv6-PD 获取到了一个前缀,需要从这个前缀中挑一个子前缀,让光猫向 LAN 口通过 DHCPv6-PD 下发这个子前缀。所以可以选择直连光猫 LAN 口。

据我了解,大部分光猫应该不支持一级路由 PD 下发 ipv6 给 2 级路由器,需要另起 dhcpv6 pd server。

  1. 把路由器改成 AP (有线中继)模式,也就是做一个二层设备(无线交换机)。这样路由器下面的设备就相当于是直连光猫 LAN 口了。
  2. 路由器设置 ipv6 转发(pass through)模式,这样路由器通过 ND Proxy 使 LAN 和 WAN 处于同一个子网,相当于直连光猫 LAN 口了。

路由器拨号 光猫桥接

一劳永逸,路由器会分配有效的 ipv6 地址给终端,比如以电信 240e 开头,移动 2409 开头的地址。

ipv6 ddns

原因:

  1. 家用宽带 ipv6 地址前缀一般都是不固定的,可能重新拨号就会改变。不过后缀可以固定,windows 中把 temp ipv6 取消掉就行。
  2. ipv6 地址 128 位,不方便记忆。

因为我懒得定制路由器,并且并不是随都时需要 ddns(笑

我选择把 ddns-go 部署到需要放开的主机上随主机启动,再到域名服务商上分配一个 ipv6 AAAA 记录就好。

Swift 函数的派发方法

static func 会变成直接派发 direct dispatch

重点探讨 dynamic & @objc

1.swift

1
2
3
class PigeonError {
dynamic func wrapResult() {}
}

生成对应 sil 中间代码并且替换 shadow name

1
$ swiftc 1.swift -emit-silgen | xcrun swift-demangle > 1.out

dynamic func compare with @objc func

几个关键区别,dynamic 多了 dynamically_replacable,说明其动态可替换

1
2
3
4
5
6
7
class PigeonError {
dynamic func wrapResult() {}
}
// PigeonError.wrapResult()
+++ sil hidden [dynamically_replacable] [ossa] @main.PigeonError.wrapResult() -> () : $@convention(method) (@guaranteed PigeonError) -> () {
...
}

@objc 多了 objc_method,一个暴露给 objc runtime 的壳方法,copy_value & borrow PigeonError实例,再 call 源函数,再 return。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class PigeonError {
@objc func wrapResult()
@objc deinit
init()
}

// @objc PigeonError.wrapResult()
sil private [thunk] [ossa] @@objc main.PigeonError.wrapResult() -> () : $@convention(objc_method) (PigeonError) -> () {
// %0 // user: %1
bb0(%0 : @unowned $PigeonError):
%1 = copy_value %0 : $PigeonError // users: %6, %2
%2 = begin_borrow %1 : $PigeonError // users: %5, %4
// function_ref PigeonError.wrapResult()
%3 = function_ref @main.PigeonError.wrapResult() -> () : $@convention(method) (@guaranteed PigeonError) -> () // user: %4
%4 = apply %3(%2) : $@convention(method) (@guaranteed PigeonError) -> () // user: %7
end_borrow %2 : $PigeonError // id: %5
destroy_value %1 : $PigeonError // id: %6
return %4 : $() // id: %7
} // end sil function '@objc main.PigeonError.wrapResult() -> ()'

两者皆是 vtable dispatch 方法,@objc 并未使用消息派发机制

1
2
3
4
5
sil_vtable PigeonError {
#PigeonError.wrapResult: (PigeonError) -> () -> () : @main.PigeonError.wrapResult() -> () // PigeonError.wrapResult()
#PigeonError.init!allocator: (PigeonError.Type) -> () -> PigeonError : @main.PigeonError.__allocating_init() -> main.PigeonError // PigeonError.__allocating_init()
#PigeonError.deinit!deallocator: @main.PigeonError.__deallocating_deinit // PigeonError.__deallocating_deinit
}

@objc dynamic func

与 objc 唯一区别就是,func wrapResult 从 vtable中去掉了,说明转为objc 消息发送了

1
2
3
4
sil_vtable PigeonError {
#PigeonError.init!allocator: (PigeonError.Type) -> () -> PigeonError : @main.PigeonError.__allocating_init() -> main.PigeonError // PigeonError.__allocating_init()
#PigeonError.deinit!deallocator: @main.PigeonError.__deallocating_deinit // PigeonError.__deallocating_deinit
}

objc.out

dynamic.out

objc_dynamic.out

static.out

需求:

  1. 客户端表现,写完即销毁的 Notes
  2. 发送到特定的另一方查阅

大致实现:

  1. 用户名称双方绑定,储存本地
  2. 笔记本地暂存,提交后销毁
  3. 仅拉取另一方的信息

私以为这样一个仅双方交流的笔记软件用不着专门提供后端(其实是因为没有现有服务器),自行解决办法可以跨越物理距离面对面快传,甚至走 p2p 形式。最终选择使用某些公有服务。

客户端方面一开始打算使用iOS原生,cloudkit+sharing提供后端,双方共享储存。但由于 apple 系原生软件不允许私人使用(自签要给每个人手机开开发者,还只有7天),除了企业认证、tf 内测等歪门邪道,以上还需持有每年688的 appledev 会员。

于是考虑使用 flutter + web 代替,尽量完整 apple 系原生体验,后端使用 github issues api,参考评论区系统 https://jw1.dev/2022/10/23/a01/


Update

以上大部分作废,github oauth app还是需要回传 token,静态 pages 无法实现。

于是借用了一位爹的服务器打算自己造一下储存,典中典之 gin+gorm 后端。

之后又想了下用狗狗公共服务懒得自己搭数据库,使用 firebase database + messaging + google oauth + 爹的服务器上的pushNotification服务作为后端,flutter 作 web 前端。

问题记录:

Firebase SDK 中的FirebaseMessaging.instance.requestPermission方法,一开始总是喜提 Denied,也没见授权弹窗。更离谱是概率事件,有时候能有授权弹窗。

找了一下午代码,messaging web sdk 通过调用 Notification: requestPermission() 方法完成授权,其中重要的一句如下。

https://developer.mozilla.org/en-US/docs/Web/API/Notification/requestPermission_static

Note: In the above example we spawn notifications in response to a user gesture (clicking a button). This is not only best practice — you should not be spamming users with notifications they didn’t agree to — but going forward browsers will explicitly disallow notifications not triggered in response to a user gesture. Firefox is already doing this from version 72, for example.

而 Firebase 并未指明该方法需要在手势后调用。这下知道概率触发的原因了,大概是因为 dart 代码加载慢,先给了手势再执行的 requestPermission 代码。

最近在做一个相关的需求,需要判断当前的输入法是否处于拼写态,以及当前的输入法的布局。

拼写态即用户正在拼写还未形成最终的字、词的状态,例如在中文全键上 typing 拼音,输入法上还待选中文但输入框显示字母;又比如英文全键输入字母还未成单词时,候选框待选。 这个词感觉很难用中文解释,用中文关键字 google 几乎不会得到结果。

键盘布局(KeyboardLayout),例中文全键、中文九宫格、英文全键、emoji等。

Read more »

某Flutter应用发热解决过程

某版本改动过后,发热量徒增(尤其是iOS的弱散热平台),遂开始溯源。

先CPU Profiler开profile看实时cpu消耗,注意到键盘升起时报Low Memory Warning,同理键盘收取也是。并且CPU Usage飙升,并且只要键盘存在CPU Usage就维持在较高水平。

在活跃约1分钟后Thermal State变为Fair,一分半变为Serious。发热确实很严重。

初步判断flutter Textfield在iOS平台有问题,去flutter issue寻找,果真找到了#128197

于是将flutter Textfield替换成iOS原生UITextView的实现,发热解决了。

但是CPU高占用和Low Memory Warning还是未解决。

Low Memory Warning可能是大量新创建的对象Object,即可能是大量的Widget被重新创建导致的build。

rebuild范围大主要原因就是监听了不必要的dependency,比如MediaQuery.of会监听所有MediaQueryData,context.watch会监听所有来自ChangeNotifier的变化。所以应该改成如MediaQuery.paddingOf和context.select的形式,并且以抽Widget代替function的方式,可以有效减小rebuild范围。

Dart里的Mixin

一开始以为只是简单的代码混入,感觉还蛮好用。

但是多个Mixin如果有相同的函数或变量混入了如何解决?

Mixins in Dart work by creating a new class that layers the implementation of
the mixin on top of a superclass to create a new class - it is not “on the side “ but “on top” of the superclass, so there is no ambiguity in how to resolve lookups.

- Lasse R. H. Nielsen on StackOverflow.

以下的代码,实际上继承链为Disposable->A->B->AB。

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
abstract class Disposable {
void dispose() {
print('Disposable');
}
}

mixin A on Disposable {
@override
void dispose() {
super.dispose();
print('A');
}
}

mixin B on Disposable {
@override
void dispose() {
super.dispose();
print('B');
}
}

final class AB extends Disposable with A,B {
@override
void dispose() {
super.dispose();
print('AB');
}
}


void main() {
AB().dispose();
}

实现ModalBottomSheet

定义ModalBottomSheet可滑动关闭底部弹出Route,并且可以定义上一Route的Transition动画,这个动画是跟手的。

iOS13中新出现了一种弹出Route,是堆叠形式的底部弹出Route,暂且将其命名为StackModalPopup

所以StackModalPopupModalBottomSheet的子集,只是自定义了动画。

Read more »

不要靠近Flutter的CupertinoContextMenu

已存在的issue

跟原生完全没得比,child大小适配没有做https://github.com/flutter/flutter/issues/58880

child大小总是占屏幕的一半

actions弹出位置总是固定,一旦action多了就会超出屏幕,没有办法点击,也没有缩小或者滑动的策略让用户点击https://github.com/flutter/flutter/issues/55025

我使用过程发现的issue

#1

flutter在弹出ctxMenu的策略是先放置overlay,在推入路由。

一旦这个overlay里面有跟其他组件交互的方法(在Widgets树中向上查找其他组件),而WidgetApp中Overlay处于较上层的位置,所以他所能找到的组件是有限的。

举个例子,Overlay在WidgetApp中是位于Navigator上层的,也就是你用Overlay下的context不可能调用Navigator.push等方法,因为在Overlay下context下找不到Navigator,此时会抛出

Navigator operation requested with a context that does not include a Navigator. The context used to push or pop routes from the Navigator must be that of a widget that is a descendant of a Navigator widget.

所以如果想制作某个依赖上层dependency的overlay,最好做法是传context进去。

#2 CupertinoContextMenu中可触发多手势

如果在长按触发CupertinoContextMenu过程中点击其他可响应事件的地方,这时候两个手势会在分别的GestureArena中不互相竞争,导致两个都可能触发。

比如在长按一张照片1时点击另一张照片2,这时候不仅会出现照片1的ContextMenuRoute,还会出现照片2的的照片详情页。但是在iOS中原生里,点击其他地方会取消打开这个照片1的ContextMenuRoute,而不是去响应这个点击手势。

#3 [CupertinoContextMenu] Potential unremoved overlay entry

Details

According to https://github.com/flutter/flutter/blob/0ff68b8c610d54dd88585d0f97531208988f80b3/packages/flutter/lib/src/cupertino/context_menu.dart#L560C1-L583C1

_lastOverlayEntry is only removed when animation is dismissed or completed.

What if the overlayEntry is inserted, and then the CupertinoContextMenu is disposed and removed from widgets tree? So the entry will never be removed.

Temporary solution is add _lastOverlayEntry?.remove() to

https://github.com/flutter/flutter/blob/0ff68b8c610d54dd88585d0f97531208988f80b3/packages/flutter/lib/src/cupertino/context_menu.dart#L683C1-L687C4

But this is not enough, because there will be no animation, and it will be abrupt for it to suddenly disappear.

Steps to reproduce

  1. Add a CupertinoContextMenu to any visible widget.
  2. Press it to insert the overlayEntry (the _DecoyChild) , while remove the CupertinoContextMenu from the widgets tree.
  3. The entry never dismissed.

issue在https://github.com/flutter/flutter/issues/131471