注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

“杀死” App 上的疑难崩溃!

iOS
问题与背景在移动应用性能方面,崩溃带来的影响是最为严重的,程序崩溃可以打断用户正在进行的操作体验,造成关键业务中断、用户留存率下降、品牌口碑变差、生命周期价值下降等影响。很多公司将崩溃率作为优先级最高的技术指标,因此程序崩溃的监控与收集就成为了一项必不可少的工...
继续阅读 »

问题与背景

在移动应用性能方面,崩溃带来的影响是最为严重的,程序崩溃可以打断用户正在进行的操作体验,造成关键业务中断、用户留存率下降、品牌口碑变差、生命周期价值下降等影响。很多公司将崩溃率作为优先级最高的技术指标,因此程序崩溃的监控与收集就成为了一项必不可少的工作, 目前58同城App使用腾讯Bugly作为发布环境下App异常数据的收集工具。

我们的崩溃率一直在优化,每个版本都有专门负责监控线上崩溃以及解决问题的同学,经过我们不断的优化,目前 58同城iOS App的崩溃率维持在一个比较优秀的水准, Bugly上收集的崩溃大部分都是野指针崩溃和疑难崩溃。但是遗留的疑难崩溃优化手段比较有限,一个主要的原因是Bugly上的崩溃不能正常解析,定位不到真正原因。我们拿一个简单的例子来说明一下。

RN的HOOK函数问题

0 CoreFoundation  0x00000001804f504c 	0x000000018045c000 + 626764
1 Foundation 0x0000000181dae6cc 0x0000000181c7e000 + 1246924
2 UIKit 0x0000000198e5cf30 0x0000000198e57000 + 24368
3 AppName 0xe622388106d79fcc RCTFBQuickPerformanceLoggerConfigureHooks + 16244
4 CoreTelephony 0x0000000198e5e628 0x0000000198e57000 + 30248
5 CoreTelephony 0x0000000108f68fe4 0x0000000198e57000 + 78455260
6 CoreTelephony 0x00000001061ed870 0x0000000198e57000 + 30763624
7 CoreTelephony 0x0000000108f657ec 0x0000000198e57000 + 78440932
8 AppName 0x0000000108f67024 _ZN6tflite19AcquireFlexDelegateEv + 78447132
9 Foundation 0x0000000108f67024 _NSGetUsingKeyValueGetter + 88

在近几个版本中,我们发现Bugly上有大量的崩溃日志都会携带一个来自RN的函数调用栈: RCTFBQuickPerformanceLoggerConfigureHooks,这是一个RNHOOK函数。多条崩溃日志的堆栈都指向这个函数,且这个函数是一个空函数,没有任何实现,这让我们比较困扰。用过Bugly的同学都知道,Bugly每条崩溃日志都有个跟踪数据,记录着这个崩溃发生之前页面的跟踪日志,通过页面的跟踪日志我们发现这些崩溃中用户浏览的页面大多数都不涉及RN业务,与RN没有任何关系。而且每条崩溃的页面跟踪日志也不相同。既然程序崩溃之前浏览的业务不涉及RN但Bugly上的堆栈确指向RN,因此我们怀疑这种崩溃不是崩溃在RNHOOK函数上并且它们是不同错误导致的崩溃。带着这种疑问,我们开始验证这个猜想,来看一看我们的怀疑是否准确。

如何验证Bugly解析错误

因为Bugly无法拿到应用崩溃后所产生的ips文件,无法利用symbolicatecrash等工具符号化日志。因此我们采用atos命令来验证我们的怀疑是否正确。

1. atos验证

atos工具会输出崩溃的代码语句和它所在的文件以及行数,前置条件是需要拿到dSYM文件,确定手机架构是arm64还是armv7,还需要拿到atos需要的load-addressaddress,根据这些信息就能够找到问题所在。aots命令格式如下:

atos -o yourAppName.app.dSYM/Contents/Resources/DWARF/yourAppName -arch arm64/armv7 -l <load-address> <address>

怎么获取dSYM文件与架构这里就不做详细介绍了,我们来看一下怎么在Bugly的崩溃日志中拿到load-addressaddress

一般以app命名的地方就是崩溃的位置,例如:正常的一个崩溃日志格式为:

0x0000000103ef6970 0x0000000102728000 + 30252

其中0x0000000103ef6970为运行地址,就是atos需要的address0x0000000102728000为运行起始地址,就是atos需要的load address302522为偏移量,一般来说,偏移量 + 运行起始地址 = 运行地址。

介绍完atos需要的load address(运行起始地址)与address(运行地址)之后,再来看一下RCTFBQuickPerformanceLoggerConfigureHooks这个函数的崩溃,根据图中示例我们看到这个崩溃的运行地址为0xe622388106d79fcc,但是这个崩溃地址是错误的,一般地址小于0xFFFFFFFFFF,示例中明显大很多。因此我需要将高位地址清洗,清洗后此地址为0x106d79fcc。因此address0x106d79fcc

接下来我们打开Bugly其他信息一栏,看到App base addr(基地址):0x0000000102604000,这个就是atos需要需要的load address。 image20211116110000581.png 通过上述信息,我们以RCTFBQuickPerformanceLoggerConfigureHooks这个函数为例验证一下Bugly的解析结果是否正确

➜ atos -o AppName.app.dSYM/Contents/Resources/DWARF/AppName -arch arm64 -l 0x0000000102604000 0x0000000106d79fcc
-[NSMutableDictionary(YJKit) yjKit_setObject:forKey:] (in AppName) (YJKit.m:432)

结果发现atos符号化后的结果与Bugly给我们的结果确实不一致。再根据Bugly的页面跟踪数据我们确认atos符号化后的结果是正确的,这与我们的怀疑是一致的。

既然Bugly的堆栈错误的指向了这个RN的空函数,那么我们就来看一看源码中RCTFBQuickPerformanceLoggerConfigureHooks是怎样的存在。

**2. 源码中的RCTFBQuickPerformanceLoggerConfigureHooks**函数

RCTFBQuickPerformanceLoggerConfigureHooks函数在源码中的声明如下:

image-20211116110100992.png 源码中,这个函数没有任何实现,完全是一个空函数。将RCT__EXTERN 展开后为__attribute__((visibility("default"))),其作用为将RCTFBQuickPerformanceLoggerConfigureHooks向外界暴露,如果外界存在同名函数,那么RCTFBQuickPerformanceLoggerConfigureHooks会报符号冲突的错误。这里利用__attribute__((weak))RCTFBQuickPerformanceLoggerConfigureHooks声明为弱符号,当外界有同名函数时,SDK内部调用外届的函数,否则调用内部空函数,这个弱符号在RN里起到了HOOK的作用 ,接下来我们就详细的了解一下弱符号。

3. 弱符号__attribute__ ((weak))

在一个程序中,无论是变量名,还是函数名,在编译器的眼里,就是一个符号而已,符号可以分为强符号和弱符号。对于C/C++来说,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量是弱符号,强符号和弱符号在程序编译连接过程中一般遵循下面三个规则:

  1. 不允许强符号被多次定义。如果有多个强符号,会报符号重定义错误

  2. 如果有一个强符号,其他定义都是弱符号,则选择强符号

  3. 如果一个符号在所有文件中都是弱符号,则选择其中一个占用空间最大的

强弱符号规则定义摘选自:强符号和弱符号,强引用和弱引用

duplicate symbol '_OBJC_CLASS_$_XXX'这个错误大家应该都比较熟悉,通过错误的描述我们很容易就可以知道这是因为在链接的时候有重复的符号。在编译时,编译器向汇编器输出每个全局符号,若两个或两个以上全局符号(函数或变量名)名字一样,且都是强符号就会出现符号重定义错误,如果有一个是弱符号(weak symbol),则不会出现问题。

一个程序内同时存在强符号与弱符号时,链接器会忽略弱符号,去使用普通的全局符号来解析所有对这些符号的引用,但当普通的全局符号不可用时,链接器会使用弱符号。当有函数或变量名可能被用户覆盖时,该函数或变量名可以声明为一个弱符号。可以通过__attribute__((weak))来定义弱符号。

4. 弱符号的使用

在开发中,假如我们不确定外部模块是否提供一个函数func,但是我们不得不用这个函数,即自己模块的代码必须用到func函数:

extern int func(void);
...int a = func;
...

我们不知道func函数是否被定义了,这会导致2个结果:

  1. 外部存在这个函数func,那么在我自己的模块使用这个函数func,正确。
  2. 外部如果不存在这个函数,那么我们使用func,程序直接崩溃。

所以这个时候,__attribute__((weak)) 派上了用场,在自己的模块中定义:

int __attribute__((weak)) func(......)
{
return 0;
}

将本模块的func转成弱符号类型,如果遇到强符号类型(即外部模块定义了func),那么我们在本模块执行的func将会是外部模块定义的func。如果外部模块没有定义,那么,将会调用这个弱符号。

我们发现Bugly对某些没有解析正确的崩溃,堆栈都会定位到项目中的弱符号上,同时我们还发现在58同城App中,Bugly不单单定位到RCTFBQuickPerformanceLoggerConfigureHooks这一个弱符号上,还有大量的崩溃定位到了其他的弱符号上。

上面我们通过atos还原了正确的日志,并定位到了是弱符号的问题,下面我们结合符号表来看一下日志符号化的原理。

如何处理bugly解析异常的数据

Crash 日志在被符号化之前是不可读的,所谓符号化就是把堆栈信息解释成源码里可读的函数名或方法名,也就是所谓的符号。只有符号化成功后,Crash 日志才能更好的帮助开发者定位问题。日志的解析需要用到dSYM文件,dSYM指的是 Debug Symbols, 也就是调试符号。

DWARF是一种被众多编译器和调试器使用的用于支持源码级别调试的调试文件格式,该格式是一个固定的数据格式,dSYM就是按照DWARF格式保存调试信息的文件,我们常常称为符号表文件。

日志的符号化有很多种方式,例如xcode分析、symbolicatecrashatosdwarfdump等,本质其实就是查找崩溃指令在符号表哪个函数的指令区间。今天我们主要讲一下Bugly解析不准的日志怎么在符号表里查找出正确的堆栈。

1. Bugly还原正确堆栈的原理

以弱符号RCTFBQuickPerformanceLoggerConfigureHooks函数为例,还原一下日志的解析原理。

0 CoreFoundation  0x00000001804f504c 	0x000000018045c000 + 626764
1 Foundation 0x0000000181dae6cc 0x0000000181c7e000 + 1246924
2 UIKit 0x0000000198e5cf30 0x0000000198e57000 + 24368
3 AppName 0xe622388106d79fcc RCTFBQuickPerformanceLoggerConfigureHooks + 16244
4 CoreTelephony 0x0000000198e5e628 0x0000000198e57000 + 30248
5 CoreTelephony 0x0000000108f68fe4 0x0000000198e57000 + 78455260
6 CoreTelephony 0x00000001061ed870 0x0000000198e57000 + 30763624
7 CoreTelephony 0x0000000108f657ec 0x0000000198e57000 + 78440932
8 AppName 0x0000000108f67024 _ZN6tflite19AcquireFlexDelegateEv + 78447132
9 Foundation 0x0000000108f67024 _NSGetUsingKeyValueGetter + 88

  • 上图中我们看到 RCTFBQuickPerformanceLoggerConfigureHooks 这行调用栈的虚拟内存地址存在异常,一般地址地址小于0xFFFFFFFFFF ,示例中明显大很多。我们将高位地址清洗后来保证堆栈正常。调整后,地址为 0x106d79fcc,但当然不是每个Bugly解析错误的日志虚拟内存地址都异常,如果是正常的,则不用改变
  • 查看其他信息,找到基地址App base addr,此处为 0x102604000。如果崩溃发生在其他动态库,那么查找下方对应动态库的地址。
  • 经过第一步和第二步,我们获取到了 0x106d79fcc 和 0x102604000
  • 指令偏移地址为:0x4775FCC = (步骤1)0x106d79fcc - (步骤2)0x102604000
  • 找到此次打包对应的Bugly符号表,并以文本的方式打开
  • 查找0x4775FCC在哪一行符号区间内
  • 最终查找到其在 0x4775fb4 ≤ 0x4775FCC < 0x4775fd0,即3997407行的符号,符号区间遵循前闭后开原则

image20211112210415260.png 通过以上步骤我们找到了RCTFBQuickPerformanceLoggerConfigureHooks函数的实际崩溃位置,并且与我们用atos工具验证后的结果一致,说明这个结果是正确的。

上面我们在符号表里查找到Bugly解析错误的日志的正确堆栈,那如果没有符号表怎么呢,这就涉及到了提取符号表。

2. 如何提取符号表

如果符号表丢失了,但是代码没有改动,那么可以尝试在相同的环境下重新编译和提取符号表,这个步骤有两个前提 1. 代码要与之前保持一致 2. 编译和链接环境都相同,防止由于Debug/Relase对最终包有影响。如果是Debug包,可以用过dsymutil xxx.app/xxx -o xxx.dSYM 来提取符号表

有了以上两个前提就可以通过dSYM文件来提取符号表了,目前我们实现了Bugly轻量符号表的提取,并且文件体积相对于Bugly符号表体积减少到60%。推动ICI(58项目管理平台)按照一定规则输出符号表,目前可以做到根据崩溃日志的UUID直接下载对应的符号表,日志解析和问题排查效率极大提高。

3. 无符号表符号化日志

如果既找不到符号表(dSYM文件或symbol文件),也无法恢复到原先的代码重新生成符号表,那么可以考虑借助无符号表符号化工具 WBBlades 来还原日志:github.com/wuba/WBBlad…

WBBlades是基于Mach-O文件解析的工具集,包括未使用代码检测(支持ObjCSwift)、应用程序大小分析、不需要dSYM文件的日志恢复。

由于方案自身的限制,目前还不能解析除了OC方法以外的崩溃日志,如:block的崩溃、自定义C函数的崩溃。后续需要考虑如何将block的崩溃日志进行符号化。

优化成果与收益

现在我们知道了当Bugly解析不准的时候,我们可以利用Bugly给我们提供的其他信息在符号表里找到正确的答案。通过以上研究,我们通过自研解析工具重新对Bugly的日志进行符号化,通过工具我们在集团内部解决了除RNHOOK函数问题以外还解决了多个遗留已久的历史版本崩溃问题,这里简单的介绍几个比较有代表性的。

1. 拿不到基地址的问题

通过RCTFBQuickPerformanceLoggerConfigureHooks函数的崩溃的介绍,我们可以在Bugly的其他信息里获取到日志的基地址,通过这个地址我们不论是用atos验证还是手动在符号表里查找都可以还原正确的堆栈,但是如果Bugly的其他信息里没有基地址怎么办,我们来看一下下面的这种崩溃日志。

0 CoreFoundation 0x00000001835891b8 0x0000000183459000 + 1245624
5 UIKit 0x000000018963a660 0x000000018942f000 + 2143840
6 AppName 0x00000001075c9904 str_to_integral_8ExpectedIT_NS_14Conversion + 1950052
7 AppName 0x000000010627f94c RCTFBQuickPerformanceLoggerConfigureHooks + 3098344
8 AppName 0x00000001062015a0 RCTFBQuickPerformanceLoggerConfigureHooks + 2581308
9 AppName 0x00000001061fe498 RCTFBQuickPerformanceLoggerConfigureHooks + 2568756
10 AppName 0x00000001061fed38 RCTFBQuickPerformanceLoggerConfigureHooks + 2570964
11 AppName 0x00000001061ed900 RCTFBQuickPerformanceLoggerConfigureHooks + 2500252
12 AppName 0x0000000105231bd8 _ZZGetAppIdTableEvE12arAppIdTable + 57325128
13 libdispatch.dylib 0x00000001824121fc 0x0000000182411000 + 4604
21 UIKit 0x00000001894a4534 UIApplicationMain + 208
22 AppName 0x00000001085b73e8 _ZN15CTXAppidConvert13GetAppIdTableEv + 8521236
23 libdyld.dylib 0x00000001824455b8 0x0000000182441000 + 17848

通过上面的堆栈信息我们看到崩溃的调用栈也停留在了弱符号RCTFBQuickPerformanceLoggerConfigureHooks上,但与我们上面举的例子的不同点是这个崩溃在Bugly上的其他信息一栏里是空的,也就是拿不到基地址,因此我们使用atos命令是不可行的,所以只能在符号表里查找,但是我们要首先要拿到基地址。下面我们来看一下遇到这种情况该怎样拿到基地址。

  1. 首先我们看到 22 AppName 0x00000001085b73e8 str_to_integral_8ExpectedIT_NS_14Conversion + 8521236 这一行信息,熟悉Bugly与crash 日志的同学一定知道, 这一行大概率是main函数,那么我们就在这里找到突破口。
  2. 我们看到main函数的调用栈符号是_ZN15CTXAppidConvert13GetAppIdTableEv,这个函数运行地址是0x00000001085b73e8
  3. 那这个函数的运行起始地址为 0x00000001085b73e8 - 0x8521236 = 0x107D96DD4
  4. 打开符号表,找到_ZN15CTXAppidConvert13GetAppIdTableEv这个符号的偏移地址为 0x7d86dd4
  5. App base addr(基地址): 0x107D96DD4 - 0x7d86dd4 = 0x100010000

这样我们就拿到了这个日志的基地址,然后利用上面的方式在符号表里找到正确的堆栈,因此也就能将这个日志正确的解析了。

2. 百度地图SDK的崩溃问题

除了RNHOOK函数问题,我们还发现有大量的崩溃日志调用栈都指向了百度地图SDK。 image20211116110252571.png 首先我们通过Bugly显示的堆栈信息以为是百度地图SDK的崩溃,这个崩溃在某几个版本中占58同城App总崩溃率的40%左右,是58App内崩溃率最高的一个模块,在更换了新的SDK后崩溃率也并没有下降,而这么高的崩溃率,我们在开发与测试中却从未遇到过。通过Bugly上的跟踪数据我们看到最后的页面记录停留在了金融业务内,而金融业务与百度地图没有任何关系。因此这个崩溃应该与上面描述的一样,解析错误。拿到基地址与运行地址,通过我们自研的工具拿到了正确的堆栈。结果为金融业务使用的一个人脸识别SDK的崩溃,文件名称与Bugly上的跟踪日志也相同。

3. 安居客IM登录问题

我们编写了脚本文件,利用脚本文件定位了一个安居客存在很久的问题。在Bugly上排名比较靠前,崩溃占比很高,Bugly上的堆栈显示异常,因此这个崩溃之前并没有定位到具体原因。脚本协助安居客定位是IMSDK的原因。 image20211116110315205.png

以上是几个比较具有代表性的Bugly解析错误的日志,我们通过研究分析将这些错误的堆栈还原正确并解决了问题。

目前我们支持按版本自动排查出Bugly上前200名崩溃中解析异常的日志,并且可以将异常日志自动符号化成正确的日志。整个过程在符号表已经提前下载并解析好的前提下,只有10秒左右,大大提升了我们日常研发以及解决问题的效率。通过对Bugly上的疑难崩溃的治理,目前为止我们修复了Bugly上70%左右的疑难崩溃,大大降低了58 App的崩溃率。 image20211125150625544.png 除了上述我们研究的Bugly解析异常的日志可以正确解析外,58同城还支持其他异常日志的解析。 例如App内存在段迁移发生崩溃后的日志,段迁移崩溃日志中的库名变成了异常字符、丢失了进程的起始地址,获取到错误的偏移地址,这种情况下我们可以进行自动修正并解析出正确的堆栈信息。

总结与展望

文本首先介绍了我们使用Bugly遇到的RNHOOK函数问题,通过这个问题我们提出Bugly可能解析存在错误的疑问,后续用atos命令以及符号表排查找到了正确的答案,过程中又发现了弱符号的问题。按照这个研究方向我们在集团内做了一系列工具并解决了多个版本的历史遗留问题,大大的降低了58同城iOS App的崩溃率,也提高了日常工作研发效率。

App的性能优化对用户的体验十分重要,而崩溃作为其中最重要的一个环节需要我们持续的钻研与探索。后续我们将持续优化App的性能给用户带来最好的体验。

首发自CSDN:“杀死” App 上的疑难崩溃!


作者:ZYJ
链接:https://juejin.cn/post/7037308047382806565

收起阅读 »

Flutter定制一个ScrollView嵌套webview滚动的效果

场景描述 业务需要在一个滚动布局中嵌入一个webview,但是在Android平台上有一个关于webview高度的bug: 当webview高度过大时会导致Crash甚至手机重启。所以我想到了这样一种布局:最外层是一个ScrollView,内部含有一个定高的可...
继续阅读 »

场景描述


业务需要在一个滚动布局中嵌入一个webview,但是在Android平台上有一个关于webview高度的bug: 当webview高度过大时会导致Crash甚至手机重启。所以我想到了这样一种布局:最外层是一个ScrollView,内部含有一个定高的可以滚动的webview。这里有两个问题:



  1. webview怎么滚动

  2. webview的滚动怎么和外部的ScrollView联动


解决方案


第一个问题可以通过设置gestureRecognizers解决:
gestureRecognizers: [Factory(() => EagerGestureRecognizer())].toSet(),


但是这种方法会导致webview在手势竞争中获胜,外部的ScrollView根本无法获得滚动事件,从而导致webview滚动完全独立于外部ScrollView的滚动,这也是这种布局很少出现的原因。


于是我想到了使用NestedScrollView的方案,但是很明显我需要重新定义,因为我最终想要的效果是这样子的:



OutScrollView 滑动或者Fling时InnerScrollView完全静止。


在滚动InnerScrollView时OutScrollView完全不会滑动,只有在InnerScrollView滑动到边界时才能滑动OutScrollView。如果InnerScrollView Fling, OutScrollView不会Fling,同样的在InnerScrollView边界Fling则会触发OutScrollView的Fling。


下面就是具体方案:
NestedScrollView介入滚动是靠自定义ScrollActivityDelegate开始的,scrollable.dart源码中展示了滚动手势的传递过程:


Scrollable->GestureRecorgnizer->Drag(ScrollDragController)->ScrollActivityDelegate


当用户手指拖动ScrollView时会调用:


ScrollDragController:
@override
void update(DragUpdateDetails details) {
//other codes
delegate.applyUserOffset(offset);
}

当拖动结束时调用:


@override
void end(DragEndDetails details) {
///other codes, goBallistic代表Fling
delegate.goBallistic(velocity);
}

所以自定义ScrollActivityDelegate就是Hook滚动的开始,在NestedScrollView中这个类是_NestedScrollCoordinator, 所以我的思路就是自己定义一个Delegate。下面是魔改的过程:


需要判断InnerScrollView是否在滚动


我强制InnerScrollView必须被我的自定义Widget包裹:


class _NestedInnerScrollChildState extends State<NestedInnerScrollChild> {
@override
Widget build(BuildContext context) {
return Listener(
child: NotificationListener<ScrollEndNotification>(
child: widget.child,
onNotification: (end) {
widget.coordinator._innerTouchingKey = null;
//继续向上冒泡
return false;
},
),
onPointerDown: _startScrollInner,
);
}

void _startScrollInner(_) {
widget.coordinator._innerTouchingKey = widget.scrollKey;
}
}

我使用了Listener onPointerDown 方法来判断用户触摸了inner view, 但是并没有使用onPointerUp或者onPointerCancel来判断滚动结束,原因就是Fling的存在,Fling效果下手指已经离开屏幕但是view可能还在滑动,因此使用ScrollEndNotification这个标记更靠谱。


OutScrollView滑动时完全禁止InnerScrollView的滑动



  1. applyUserOffset的hook


  @override
void applyUserOffset(double delta) {
if (!innerScroll) {
_outerPosition.applyFullDragUpdate(delta);
}
}


  1. Fling


首先会调用Coordinator的goBallistic方法,然后触发beginActivity方法,我们直接在beginActivity中拦截即可:


///_innerPositions并不是所有innerView的集合,这个后面会讲到
if (innerScroll) {
for (final _NestedScrollPosition position in _innerPositions) {
final ScrollActivity newInnerActivity = innerActivityGetter(position);
position.beginActivity(newInnerActivity);
scrolling = newInnerActivity.isScrolling;
}
}

InnerScrollView和OutScrollView嵌套滑动



  1. applyUserOffset


借鉴NestedScrollView即可


@override
void applyUserOffset(double delta) {
double remainDelta = innerPositionList.first.applyClampedDragUpdate(delta);
if (remainDelta != 0.0) {
_outerPosition.applyFullDragUpdate(remainDelta);
}
}


  1. Fling


innerView触发Fling手势的调用链:ScrollDragController会调用ScrollActivityDelegate的goBallistic方法->触发ScrollPosition的beginActivity方法并创建BallisticScrollActivity实例->BallisticScrollActivity实例结合Simulation不断计算滚动距离。


BallisticScrollActivity有个方法:


 /// Move the position to the given location.
///
/// If the new position was fully applied, returns true. If there was any
/// overflow, returns false.
///
/// The default implementation calls [ScrollActivityDelegate.setPixels]
/// and returns true if the overflow was zero.
@protected
bool applyMoveTo(double value) {
return delegate.setPixels(value) == 0.0;
}

当这个方法返回false时就会立刻停止滚动,正好NestedScrollView有创建自定义OutBallisticScrollActivity方法,所以我在applyMove那里判断如果是innerView 正在滚动就返回false


  @override
bool applyMoveTo(double value) {
if (coordinator.innerScroll) {
return false;
}
// other codes
}

当然,这里也可以加个优化:比如innerView如果在边界触发了Fling就可以放开。


支持多个inner scroll view


outview只能有一个,但是innerView理论上可以有多个,我这里贴下参考的文章链接[:]("Flutter 扩展NestedScrollView (二)列表滚动同步解决 - 掘金 (juejin.cn)")。核心就是在ScrollController attach detach时实现position和ScrollView的绑定。


实现webview的滚动


这里我也是借鉴的大神的思路[:](大道至简:Flutter嵌套滑动冲突解决之路 - V大师在一号线 (vimerzhao.top))


Flutter中所有的滚动View最终都是用Scrollable+Viewport来实现的,Scrollable负责获取滚动手势,距离计算等,而绘制则交给Viewport来实现。翻看viewport.dart相关源码,我贴下paint的方法:


@override
void paint(PaintingContext context, Offset offset) {
if (firstChild == null)
return;
if (hasVisualOverflow && clipBehavior != Clip.none) {
_clipRectLayer.layer = context.pushClipRect(
needsCompositing,
offset,
Offset.zero & size,
_paintContents,
clipBehavior: clipBehavior,
oldLayer: _clipRectLayer.layer,
);
} else {
_clipRectLayer.layer = null;
_paintContents(context, offset);
}
}

void _paintContents(PaintingContext context, Offset offset) {
for (final RenderSliver child in childrenInPaintOrder) {
if (child.geometry!.visible)
context.paintChild(child, offset + paintOffsetOf(child));
}
}

paintOffsetOf(child)就可以简化为滚动导致的绘制偏差。举个栗子:一个viewport高500,内容高度1000,默认绘制[0-500]的内容,当用户向上滑动了100,则绘制[100,600]的内容,这里的100就是paintOffset。


所以我最后创建了一个自定义Viewport,但是Flutter端绘制时paintOffset始终传0,我把真正的offset传递给webview,然后调用window.scrollTo(0,offset)即可实现webview内容的滑动了。简而言之,传统的ScrollView是内容不动,画布在动,而我的方案就是画布不动,但是内容在动。参考代码:[]("inner_scroll_webview.dart (github.com)")


作者:芭比Q达人
链接:https://juejin.cn/post/7041064106094231560
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

iOS 实现类似探探、陌陌的卡片左滑右滑效果

iOS
本文章分析怎么实现这种卡片效果以及都有哪些功能,基于这些功能是否可以完善,让框架更加灵活,可拓展等。现已封装成通用框架。效果图如下: 代码地址 1、功能分析 不管是探探还是陌陌的点点匹配模块,都是对卡片的左滑右滑进行的操作,那么以陌陌的点点匹配模块分析,所涉...
继续阅读 »

本文章分析怎么实现这种卡片效果以及都有哪些功能,基于这些功能是否可以完善,让框架更加灵活,可拓展等。现已封装成通用框架。效果图如下:


效果图


代码地址


1、功能分析


不管是探探还是陌陌的点点匹配模块,都是对卡片的左滑右滑进行的操作,那么以陌陌的点点匹配模块分析,所涉及的功能有:



  • 卡片复用机制

  • 拖拽卡片时的动画

    • 卡片拖拽左上方、左下方、右上方、右下方都会有相应的旋转动画



  • 喜欢功能

    • 喜欢功能又分为拖拽卡片向右侧滑出、点击下方的喜欢按钮控制卡片滑出。



  • 不喜欢功能

    • 不喜欢功能也是和喜欢功能逻辑一样,只是方向不同。



  • 超级喜欢功能

    • 超级喜欢功能跟喜欢功能一样,只不过是针对的vip用户,普通用户需要充值vip才能使用,vip用户触发超级喜欢,会有一个炫丽的动画特效,并进行其它业务处理



  • 回退功能

    • 回退功能也是针对vip用户设计的,vip用户单次只能回退一张卡片。我们实现回退多张功能,让外界控制是否可以回退功能,更加灵活。



  • 预加载功能

    • 卡片可以无限的滑走,那么数据源获取就得支持加载更多数据。



  • 无数据的处理功能

    • 卡片数据操作完了,就得处理无数据的情况,显示占位图等等。



  • 触发卡片喜欢功能时,需要检测是否允许此次操作

    • 这个功能也是根据业务去做,探探里面,好像对喜欢的操作是有限制的,如果超出了这个限制,再次触发喜欢功能就会提示充值vip同时拖拽的卡片也会恢复原位。






2、功能实现


完成了上面的功能分析之后,接下来就可以一个一个的去实现了。




  • 卡片复用机制


    这里用了4张卡片,最上面的卡片划走之后,会被放在最下面一层,达到复用。




  • 拖拽卡片时的动画


    卡片拖拽左上方、左下方、右上方、右下方都会有相应的旋转动画。我们可以确定用户手势触发的点的位置,根据方位进行设置相关的旋转角度,左右是相反的。




  • 喜欢、不喜欢功能


    1、通过拖拽手势划走卡片
    给每个卡片添加一个拖拽手势,当拖拽卡片的时候,根据拖拽的距离和卡片原始的中心点X值进行判断卡片是向左还是向右,拖拽结束的时候,通过改变卡片的位置并加上动画,达到卡片划走的效果。


    2、通过按钮触发划走卡片
    按钮触发的时候,指定卡片的x位置。然后内部统一走手势结束的方法。




  • 超级喜欢功能
    超级喜欢功能,其实也是喜欢的一种,通过按钮触发喜欢操作,之后加上自己的炫丽动画以及业务逻辑.




  • 回退功能


    卡片回退的实现,将最底下的卡片放到最上面的卡片上面并加上入场动画,同时更新对应的索引数据。
    陌陌vip用户只支持回退一张,我们可以设计支持多张,有多少张不喜欢卡片,默认回退多少张,如果想要实现一张也可以,外界可以控制。相关的方法如下:

    这里回退操作的场景很多,比如:左滑10次,然后右滑4次,回退5次,在一次次的滑动,那么怎么保证卡片是按照正常的顺序显示的呢。这里用了2个数组处理的,第一个数组保存左滑的数据index,第二个数组保存回退的index。具体的思路看下截图:




  • 预加载功能
    每次划走一个卡片,都会代理回调对应的数据源Index供上层更新底部的卡片显示内容, 卡片划走的时候,也会做校验,看看当前的index相对于数据源总数是否小于一个值,这个值我们称为阀值。小于这个阀值会触发加载更多的代理回调。




  • 无数据的处理功能
    每次划走一个卡片,都会更新底部卡片显示内容,如果内部卡片的数据index超出了外界的数据源总数,则将卡片内容隐藏,也会做无数据的检测。




  • 触发卡片喜欢功能时,需要检测是否允许此次操作
    可以在拖拽手势结束的时候,通过代理去询问是否允许滑走,如果不允许则内部更改拖动的距离x值,走复位逻辑。






3、总结


卡片交互的细节很多,很多控制的地方也很多,封装的框架现已支持上面的所有功能, 使用的时候,可以自定义卡片cell实现自己的样式。提供的有示例demo. 欢迎预览。


作者:大大的太阳
链接:https://juejin.cn/post/7036769362652430372
收起阅读 »

现代配置指南——YAML 比 JSON 高级在哪?

一直以来,前端工程中的配置大多都是 .js 文件或者 .json 文件,最常见的比如:package.jsonbabel.config.jswebpack.config.js这些配置对前端非常友好,因为都是我们熟悉的 JS 对象结构。一般静态化的配置会选择 j...
继续阅读 »

一直以来,前端工程中的配置大多都是 .js 文件或者 .json 文件,最常见的比如:

  • package.json

  • babel.config.js

  • webpack.config.js

这些配置对前端非常友好,因为都是我们熟悉的 JS 对象结构。一般静态化的配置会选择 json 文件,而动态化的配置,涉及到引入其他模块,因此会选择 js 文件。

还有现在许多新工具同时支持多种配置,比如 Eslint,两种格式的配置任你选择:

  • .eslintrc.json

  • .eslintrc.js

后来不知道什么时候,突然出现了一种以 .yaml.yml 为后缀的配置文件。一开始以为是某个程序的专有配置,后来发现这个后缀的文件出现的频率越来越高,甚至 Eslint 也支持了第三种格式的配置 .eslintrc.yml

既然遇到了,那就探索它!

下面我们从 YAML 的出现背景使用场景具体用法高级操作四个方面,看一下这个流行的现代化配置的神秘之处。

出现背景

一个新工具的出现避免不了有两个原因:

  1. 旧工具在某些场景表现吃力,需要更优的替代方案

  2. 旧工具也没什么不好,只是新工具出现,比较而言显得它不太好

YAML 这种新工具就属于后者。其实在 yaml 出现之前 js+json 用的也不错,也没什么特别难以处理的问题;但是 yaml 出现以后,开始觉得它好乱呀什么东西,后来了解它后,越用越喜欢,一个字就是优雅。

很多文章说选择 yaml 是因为 json 的各种问题,json 不适合做配置文件,这我觉得有些言过其实了。我更愿意将 yaml 看做是 json 的升级,因为 yaml 在格式简化和体验上表现确实不错,这个得承认。

下面我们对比 YAML 和 JSON,从两方面分析:

精简了什么?

JSON 比较繁琐的地方是它严格的格式要求。比如这个对象:

{
 name: 'ruims'
}

在 JSON 中以下写法通通都是错的:

// key 没引号不行
{
 name: 'ruims'
}
// key 不是 "" 号不行
{
 'name': 'ruims'
}
// value 不是 "" 号不行
{
 "name": 'ruims'
}

字符串的值必须 k->v 都是 "" 才行:

// 只能这样
{
 "name": "ruims"
}

虽然是统一格式,但是使用上确实有不便利的地方。比如我在浏览器上测出了接口错误。然后把参数拷贝到 Postman 里调试,这时就我要手动给每个属性和值加 "" 号,非常繁琐。

YAML 则是另辟蹊径,直接把字符串符号干掉了。上面对象的同等 yaml 配置如下:

name: ruims

没错,就这么简单!

除了 "" 号,yaml 觉得 {}[] 这种符号也是多余的,不如一起干掉。

于是呢,以这个对象数组为例:

{
 "names": [{ "name": "ruims" }, { "name": "ruidoc" }]
}

转换成 yaml 是这样的:

names:
- name: ruims
- name: ruidoc

对比一下这个精简程度,有什么理由不爱它?

增加了什么?

说起增加的部分,最值得一提的,是 YAML 支持了 注释

用 JSON 写配置是不能有注释的,这就意味着我们的配置不会有备注,配置多了会非常凌乱,这是最不人性化的地方。

现在 yaml 支持了备注,以后配置可以是这样的:

# 应用名称
name: my_app
# 应用端口
port: 8080

把这种配置丢给新同事,还怕他看不懂配了啥吗?

除注释外,还支持配置复用的相关功能,这个后面说。

使用场景

我接触的第一个 yaml 配置是 Flutter 项目的包管理文件 pubspec.yaml,这个文件的作用和前端项目中的 package.json 一样,用于存放一些全局配置和应用依赖的包和版本。

看一下它的基本结构:

name: flutter_demo
description: A new Flutter project.

publish_to: 'none'
version: 1.0.0

dependencies:
cupertino_icons: ^1.0.2

dev_dependencies:
flutter_lints: ^1.0.0

你看这个结构和 package.json 是不是基本一致?dependencies 下列出应用依赖和版本,dev_dependencies 下的则是开发依赖。

后来在做 CI/CD 自动化部署的时候,我们用到了 GitHub Action。它需要多个 yaml 文件来定义不同的工作流,这个配置可比 flutter 复杂的多。

其实不光 GitHub Action,其他流行的类似的构建工具如 GitLab CI/CDcircleci,全部都是齐刷刷的 yaml 配置,因此如果你的项目要做 CI/CD 持续集成,不懂 yaml 语法肯定是不行的。

还有,接触过 Docker 的同学肯定知道 Docker Compose,它是 Docker 官方的单机编排工具,其配置文件 docker-compose.yml 也是妥妥的 yaml 格式。现在 Docker 正是如日中天的时候,使用 Docker 必然免不了编排,因此 yaml 语法早晚也要攻克。

上面说的这 3 个案例,几乎都是现代最新最流行的框架/工具。从它们身上可以看出来,yaml 必然是下一代配置文件的标准,并且是前端-后端-运维的通用标准。

说了这么多,你跃跃欲试了吗?下面我们详细介绍 yaml 语法。

YAML 语法

介绍 yaml 语法会对比 json 解释,以便我们快速理解。

先看一下 yaml 的几个特点:

  • 大小写敏感

  • 使用缩进表示层级关系

  • 缩进空格数不强制,但相同层级要对齐

  • # 表示注释

相比于 JSON 来说,最大的区别是用 缩进 来表示层级,这个和 Python 非常接近。还有强化的一点是支持了注释,JSON 默认是不支持的(虽然 TS 支持),这也对配置文件非常重要。

YAML 支持以下几种数据结构:

  • 对象:json 中的对象

  • 数组:json 中的数组

  • 纯量:json 中的简单类型(字符串,数值,布尔等)

对象

先看对象,上一个 json 例子:

{
 "id": 1,
 "name": "杨成功",
 "isman": true
}

转换成 yaml:

id: 1
name: 杨成功
isman: true

对象是最核心的结构,key 值的表示方法是 [key]:,注意这里冒号后面有个空格,一定不能少。value 的值就是一个纯量,且默认不需要引号。

数组

数组和对象的结构差不多,区别是在 key 前用一个 - 符号标识这个是数组项。注意这里也有一个空格,同样也不能少。

- hello
- world

转换成 JSON 格式如下:

["hello", "world"]

了解了基本的对象和数组,我们再来看一个复杂的结构。

众所周知,在实际项目配置中很少有简单的对象或数组,大多都是对象和数组相互嵌套而成。在 js 中我们称之为对象数组,而在 yaml 中我们叫 复合结构

比如这样一个稍复杂的 JSON:

{
 "name": "杨成功",
 "isman": true,
 "age": 25,
 "tag": ["阳光", "帅气"],
 "address": [
  { "c": "北京", "a": "海淀区" },
  { "c": "天津", "a": "滨海新区" }
]
}

转换成复合结构的 YAML:

name: 杨成功
isman: true
age: 25
tag:
- 阳光
- 帅气
address:
- c: 北京
  a: 海淀区
- c: 天津
  a: 滨海新区

若你想尝试更复杂结构的转换,可以在 这个 网页中在线实践。

纯量

纯量比较简单,对应的就是 js 的基本数据类型,支持如下:

  • 字符串

  • 布尔

  • 数值

  • null

  • 时间

比较特殊的两个,null 用 ~ 符号表示,时间大多用 2021-12-21 这种格式表示,如:

who: ~
date: 2019-09-10

转换成 JS 后:

{
 who: null,
 date: new Date('2019-09-10')
}

高级操作

在 yaml 实战过程中,遇到过一些特殊场景,可能需要一些特殊的处理。

字符串过长

在 shell 中我们常见到一些参数很多,然后特别长的命令,如果命令都写在一行的话可读性会非常差。

假设下面的是一条长命令:

$ docker run --name my-nginx -d nginx

在 linux 中可以这样处理:

$ docker run \
--name my-nginx \
-d nginx

就是在每行后加 \ 符号标识换行。然而在 YAML 中更简单,不需要加任何符号,直接换行即可:

cmd: docker run
--name my-nginx
-d nginx

YAML 默认会把换行符转换成空格,因此转换后 JSON 如下,正是我们需要的:

{ "cmd": "docker run --name my-nginx -d nginx" }

然而有时候,我们的需求是保留换行符,并不是把它转换成空格,又该怎么办呢?

这个也简单,只需要在首行加一个 | 符号:

cmd: |
docker run
--name my-nginx
-d nginx

转换成 JSON 变成了这样:

{ "cmd": "docker run\n--name my-nginx\n-d nginx" }

获取配置

获取配置是指,在 YAML 文件中定义的某个配置,如何在代码(JS)里获取?

比如前端在 package.json 里有一个 version 的配置项表示应用版本,我们要在代码中获取版本,可以这么写:

import pack from './package.json'
console.log(pack.version)

JSON 是可以直接导入的,YAML 可就不行了,那怎么办呢?我们分环境解析:

在浏览器中

浏览器中代码用 webapck 打包,因此加一个 loader 即可:

$ yarn add -D yaml-loader

然后配置 loader:

// webpack.config.js
module.exports = {
 module: {
   rules: [
    {
       test: /\.ya?ml$/,
       type: 'json', // Required by Webpack v4
       use: 'yaml-loader'
    }
  ]
}
}

在组件中使用:

import pack from './package.yaml'
console.log(pack.version)

在 Node.js 中

Node.js 环境下没有 Webpack,因此读取 yaml 配置的方法也不一样。

首先安装一个 js-yaml 模块:

$ yarn add js-yaml

然后通过模块提供的方法获取:

const yaml = require('js-yaml')
const fs = require('fs')

const doc = yaml.load(fs.readFileSync('./package.yaml', 'utf8'))
console.log(doc.version)

配置项复用

配置项复用的意思是,对于定义过的配置,在后面的配置直接引用,而不是再写一遍,从而达到复用的目的。

YAML 中将定义的复用项称为锚点,用& 标识;引用锚点则用 * 标识。

name: &name my_config
env: &env
version: 1.0

compose:
key1: *name
key2: *env

对应的 JSON 如下:

{
 "name": "my_config",
 "env": { "version": 1 },
 "compose": { "key1": "my_config", "key2": { "version": 1 } }
}

但是锚点有个弊端,就是不能作为 变量 在字符串中使用。比如:

name: &name my_config
compose:
key1: *name
key2: my name is *name

此时 key2 的值就是普通字符串 my name is *name,引用变得无效了。

其实在实际开发中,字符串中使用变量还是很常见的。比如在复杂的命令中多次使用某个路径,这个时候这个路径就应该是一个变量,在多个命令中复用。

GitHub Action 中有这样的支持,定义一个环境变量,然后在其他的地方复用:

env:
NAME: test
describe: This app is called ${NAME}

这种实现方式与 webpack 中使用环境变量类似,在构建的时候将变量替换成对应的字符串。

作者:杨成功
来源:https://segmentfault.com/a/1190000041108051

收起阅读 »

LOOK 直播活动地图生成器方案

在最近的活动开发中,笔者就刚好碰到了这个问题。这次活动开发需要完成一款大富翁游戏,而作为一款大富翁游戏,地图自然是必不可少的。在整个地图中,有很多的不同种类的方格,如果一个个手动去调整位置,工作量是很大的。那么有没有一种方案能够帮助我们快速确定方格的位置和种类...
继续阅读 »

对于前端而言,与视觉稿打交道是必不可少的,因为我们需要对照着视觉稿来确定元素的位置、大小等信息。如果是比较简单的页面,手动调整每个元素所带来的工作量尚且可以接受;然而当视觉稿中素材数量较大时,手动调整每个元素便不再是个可以接受的策略了。

在最近的活动开发中,笔者就刚好碰到了这个问题。这次活动开发需要完成一款大富翁游戏,而作为一款大富翁游戏,地图自然是必不可少的。在整个地图中,有很多的不同种类的方格,如果一个个手动去调整位置,工作量是很大的。那么有没有一种方案能够帮助我们快速确定方格的位置和种类呢?下面便是笔者所采用的方法。

方案简述

位点图

首先,我们需要视觉同学提供一张特殊的图片,称之为位点图。

这张图片要满足以下几个要求:

  1. 在每个方格左上角的位置,放置一个 1px 的像素点,不同类型的方格用不同颜色表示。

  2. 底色为纯色:便于区分背景和方格。

  3. 大小和地图背景图大小一致:便于从图中读出的坐标可以直接使用。

bitmap

上图为一个示例,在每个路径方格左上角的位置都有一个 1px 的像素点。为了看起来明显一点,这里用红色的圆点来表示。在实际情况中,不同的点由于方格种类不同,颜色也是不同的。

bitmap2

上图中用黑色边框标出了素材图的轮廓。可以看到,红色圆点和每个路径方格是一一对应的关系。

读取位点图

在上面的位点图中,所有方格的位置和种类信息都被标注了出来。我们接下来要做的,便是将这些信息读取出来,并生成一份 json 文件来供我们后续使用。

const JImp = require('jimp');
const nodepath = require('path');

function parseImg(filename) {
   JImp.read(filename, (err, image) => {
       const { width, height } = image.bitmap;

       const result = [];

       // 图片左上角像素点的颜色, 也就是背景图的颜色
       const mask = image.getPixelColor(0, 0);

       // 筛选出非 mask 位置点
       for (let y = 0; y < height; ++y) {
           for (let x = 0; x < width; ++x) {
               const color = image.getPixelColor(x, y);
               if (mask !== color) {
                   result.push({
                       // x y 坐标
                       x,
                       y,
                       // 方格种类
                       type: color.toString(16).slice(0, -2),
                  });
              }
          }
      }

       // 输出
       console.log(JSON.stringify({
           // 路径
           path: result,
      }));
  });
}

parseImg('bitmap.png');

在这里我们使用了 jimp 用于图像处理,通过它我们能够去扫描这张图片中每个像素点的颜色和位置。

至此我们得到了包含所有方格位置和种类信息的 json 文件:

{
   "path": [
      {
           "type": "",
           "x": 0,
           "y": 0,
      },
       // ...
  ],
}

其中,x y 为方格左上角的坐标;type 为方格种类,值为颜色值,代表不同种类的地图方格。

通路连通算法

对于我们的项目而言,只确定路径点是不够的,还需要将这些点连接成一个完整的通路。为此,我们需要找到一条由这些点构成的最短连接路径。

代码如下:

function takePath(point, points) {
   const candidate = (() => {
       // 按照距离从小到大排序
       const pp = [...points].filter((i) => i !== point);
       const [one, two] = pp.sort((a, b) => measureLen(point, a) - measureLen(point, b));

       if (!one) {
           return [];
      }

       // 如果两个距离 比较小,则穷举两个路线,选择最短连通图路径。
       if (two && measureLen(one, two) < 20000) {
           return [one, two];
      }
       return [one];
  })();

   let min = Infinity;
   let minPath = [];
   for (let i = 0; i < candidate.length; ++i) {
       // 递归找出最小路径
       const subpath = takePath(candidate[i], removeItem(points, candidate[i]));

       const path = [].concat(point, subpath);
       // 测量路径总长度
       const distance = measurePathDistance(path);

       if (distance < min) {
           min = distance;
           minPath = subpath;
      }
  }

   return [].concat(point, minPath);
}

到这里,我们已经完成了所有的准备工作,可以开始绘制地图了。在绘制地图时,我们只需要先读取 json 文件,再根据 json 文件内的坐标信息和种类信息来放置对应素材即可。

方案优化

上述方案能够解决我们的问题,但仍有一些不太方便的地方:

  1. 只有 1px 的像素点太小了,肉眼无法辨别。不管是视觉同学还是开发同学,如果点错了位置就很难排查。

  2. 位点图中包含的信息还是太少了,颜色仅仅对应种类,我们希望能够包含更多的信息,比如点之间的排列顺序、方格的大小等。

像素点合并

对于第一个问题,我们可以让视觉同学在画图的时候,将 1px 的像素点扩大成一个肉眼足够辨识的区域。需要注意两个区域之间不要有重叠。

bitmap3

这时候就要求我们对代码做一些调整。在之前的代码中,当我们扫描到某个颜色与背景色不同的点时,会直接记录其坐标和颜色信息;现在当我们扫描到某个颜色与背景色不同的点时,还需要进行一次区域合并,将所有相邻且相同颜色的点都纳入进来。

区域合并的思路借鉴了下图像处理的区域生长算法。区域生长算法的思路是以一个像素点为起点,将该点周围符合条件的点纳入进来,之后再以新纳入的点为起点,向新起点相邻的点扩张,直到所有符合条件条件的点都被纳入进来。这样就完成了一次区域合并。不断重复该过程,直到整个图像中所有的点都被扫描完毕。

我们的思路和区域生长算法非常类似:

  1. 依次扫描图像中的像素点,当扫描到颜色与背景色不同的点时,记录下该点的坐标和颜色。

    步骤1.png

  2. 之后扫描与该点相邻的 8 个点,将这些点打上”已扫描“的标记。筛选出其中颜色与背景色不同且尚未被扫描过的点,放入待扫描的队列中。

    步骤2.png

  3. 从待扫描队列中取出下一个需要扫描的点,重复步骤 1 和步骤 2。

  4. 直到待扫描的队列为空时,我们就扫描完了一整个有颜色的区域。区域合并完毕。

    步骤3.png

const JImp = require('jimp');

let image = null;
let maskColor = null;

// 判断两个颜色是否为相同颜色 -> 为了处理图像颜色有误差的情况, 不采用相等来判断
const isDifferentColor = (color1, color2) => Math.abs(color1 - color2) > 0xf000ff;

// 判断是(x,y)是否超出边界
const isWithinImage = ({ x, y }) => x >= 0 && x < image.width && y >= 0 && y < image.height;

// 选择数量最多的颜色
const selectMostColor = (dotColors) => { /* ... */ };

// 选取左上角的坐标
const selectTopLeftDot = (reginDots) => { /* ... */ };

// 区域合并
const reginMerge = ({ x, y }) => {
   const color = image.getPixelColor(x, y);
   // 扫描过的点
   const reginDots = [{ x, y, color }];
   // 所有扫描过的点的颜色 -> 扫描完成后, 选择最多的色值作为这一区域的颜色
   const dotColors = {};
   dotColors[color] = 1;

   for (let i = 0; i < reginDots.length; i++) {
       const { x, y, color } = reginDots[i];

       // 朝临近的八个个方向生长
       const seeds = (() => {
           const candinates = [/* 左、右、上、下、左上、左下、右上、右下 */];

           return candinates
               // 去除超出边界的点
              .filter(isWithinImage)
               // 获取每个点的颜色
              .map(({ x, y }) => ({ x, y, color: image.getPixelColor(x, y) }))
               // 去除和背景色颜色相近的点
              .filter((item) => isDifferentColor(item.color, maskColor));
      })();

       for (const seed of seeds) {
           const { x: seedX, y: seedY, color: seedColor } = seed;

           // 将这些点添加到 reginDots, 作为下次扫描的边界
           reginDots.push(seed);

           // 将该点设置为背景色, 避免重复扫描
           image.setPixelColor(maskColor, seedX, seedY);

           // 该点颜色为没有扫描到的新颜色, 将颜色增加到 dotColors 中
           if (dotColors[seedColor]) {
               dotColors[seedColor] += 1;
          } else {
               // 颜色为旧颜色, 增加颜色的 count 值
               dotColors[seedColor] = 1;
          }
      }
  }

   // 扫描完成后, 选择数量最多的色值作为区域的颜色
   const targetColor = selectMostColor(dotColors);

   // 选择最左上角的坐标作为当前区域的坐标
   const topLeftDot = selectTopLeftDot(reginDots);

   return {
       ...topLeftDot,
       color: targetColor,
  };
};

const parseBitmap = (filename) => {
   JImp.read(filename, (err, img) => {
       const result = [];
       const { width, height } = image.bitmap;
       // 背景颜色
       maskColor = image.getPixelColor(0, 0);
       image = img;

       for (let y = 0; y < height; ++y) {
           for (let x = 0; x < width; ++x) {
               const color = image.getPixelColor(x, y);

               // 颜色不相近
               if (isDifferentColor(color, maskColor)) {
                   // 开启种子生长程序, 依次扫描所有临近的色块
                   result.push(reginMerge({ x, y }));
              }
          }
      }
  });
};

颜色包含额外信息

在之前的方案中,我们都是使用颜色值来表示种类,但实际上颜色值所能包含的信息还有很多。

一个颜色值可以用 rgba 来表示,因此我们可以让 r、g、b、a 分别代表不同的信息,如 r 代表种类、g 代表宽度、b 代表高度、a 代表顺序。虽然 rgba 每个的数量都有限(r、g、b 的范围为 0-255,a 的范围为 0-99),但基本足够我们使用了。

rgba.png

当然,你甚至可以再进一步,让每个数字都表示一种信息,不过这样每种信息的范围就比较小,只有 0-9。

总结

对于素材量较少的场景,前端可以直接从视觉稿中确认素材信息;当素材量很多时,直接从视觉稿中确认素材信息的工作量就变得非常大,因此我们使用了位点图来辅助我们获取素材信息。

无标题-2021-09-28-1450.png

地图就是这样一种典型的场景,在上面的例子中,我们已经通过从位点图中读出的信息成功绘制了地图。我们的步骤如下:

  1. 视觉同学提供位点图,作为承载信息的载体,它需要满足以下三个要求:

    1. 大小和地图背景图大小一致:便于我们从图中读出的坐标可以直接使用。

    2. 底色为纯色:便于区分背景和方格。

    3. 在每个方格左上角的位置,放置一个方格,不同颜色的方格表示不同类型。

  2. 通过 jimp 扫描图片上每个像素点的颜色,从而生成一份包含各个方格位置和种类的 json。

  3. 绘制地图时,先读取 json 文件,再根据 json 文件内的坐标信息和种类信息来放置素材。

上述方案并非完美无缺的,在这里我们主要对于位点图进行了改进,改进方案分为两方面:

  1. 由于 1px 的像素点对肉眼来说过小,视觉同学画图以及我们调试的时候,都十分不方便。因此我们将像素点扩大为一个区域,在扫描时,对相邻的相同颜色的像素点进行合并。

  2. 让颜色的 rgba 分别对应一种信息,扩充位点图中的颜色值能够给我们提供的信息。

我们在这里只着重讲解了获取地图信息的部分,至于如何绘制地图则不在本篇的叙述范围之内。在我的项目中使用了 pixi.js 作为引擎来渲染,完整项目可以参考这里,在此不做赘述。

FAQ

  • 在位点图上,直接使用颜色块的大小作为路径方格的宽高可以不?

    当然可以。但这种情况是有局限性的,当我们的素材很多且彼此重叠的时候,如果依然用方块大小作为宽高,那么在位点图上的方块就会彼此重叠,影响我们读取位置信息。

  • 如何处理有损图的情况?

    有损图中,图形边缘处的颜色和中心的颜色会略微有所差异。因此需要增加一个判断函数,只有扫描到的点的颜色与背景色的差值大于某个数字后,才认为是不同颜色的点,并开始区域合并。同时要注意在位点图中方块的颜色尽量选取与背景色色值相差较大的颜色。

    这个判断函数,就是我们上面代码中的 isDifferentColor 函数。

    const isDifferentColor = (color1, color2) => Math.abs(color1 - color2) > 0xf000ff;
  • 判断两个颜色不相等的 0xf000ff 是怎么来的?

    随便定的。这个和图片里包含颜色有关系,如果你的背景色和图片上点的颜色非常相近的话,这个值就需要小一点;如果背景色和图上点的颜色相差比较大,这个值就可以大一点。

参考资料

作者:李一笑
来源:https://segmentfault.com/a/1190000041115022

收起阅读 »

一个Vue3可使用的JSON转excel组件

JSON to Excel for VUE3在浏览器中将JSON格式数据以excel文件的形式下载。该组件是基于this thread 提出的解决方案。支持Vue3.2.25及以上版本使用重要提示! Microsoft Excel中的额外提示此组件中实现的方法...
继续阅读 »



JSON to Excel for VUE3

在浏览器中将JSON格式数据以excel文件的形式下载。该组件是基于this thread 提出的解决方案。支持Vue3.2.25及以上版本使用

重要提示! Microsoft Excel中的额外提示

此组件中实现的方法使用HTML表绘制。在xls文件中,Microsoft Excel不再将HTML识别为本机内容,因此在打开文件之前会显示警告消息。excel的内容已经完美呈现,但是提示信息无法避免,请不要在意!

Getting started

安装依赖:

npm install vue3-json-excel

在vue3的应用入口处有两种注册组件的方式:

import Vue from "vue"
import {vue3JsonExcel} from "vue3-json-excel"

Vue.component("vue3JsonExcel", vue3JsonExcel)

或者

import Vue from "vue"
import vue3JsonExcel from "vue3-json-excel"

Vue.use(vue3JsonExcel)

在template文件中直接使用即可

<vue3-json-excel :json-data="json_data">
Download Data
</vue3-json-excel>

Props List

NameTypeDescriptionDefaultremark
json-dataArray即将导出的数据

fieldsObject要导出的JSON对象内的字段。如果未提供任何属性,将导出JSON中的所有属性。

export-fields (exportFields)Object用于修复使用变量字段的其他组件的问题,如vee-validate。exportFields的工作原理与fields完全相同

typestringMime 类型 [xls, csv]xls1.0.x版本暂时只支持xls,csv会在下个版本迭代
namestringFile 导出的文件名jsonData.xls
headerstring/Array数据的标题。可以是字符串(一个标题)或字符串数组(多个标题)。

title(deprecated)string/Array与header相同,title是出于追溯兼容性目的而维护的,但由于与HTML5 title属性冲突,不建议使用它。

License

MIT

Status

该项目处于早期开发阶段。欢迎参与共建。
有好的产品建议可以联系我!!!!

npm地址

vue3-json-excel

作者:小章鱼
来源:https://segmentfault.com/a/1190000041117522

收起阅读 »

kotlin函数

1.概念函数是执行操作并可以返回值的离散代码块。在 Kotlin 中,函数是使用 fun 关键字声明的,并且可以使用接收具名值或默认值的参数。与特定类关联的函数称为方法。一个用于执行特定任务的代码块它可以将大型的程序分解为小型的模块使用关键字 fun 来声明可...
继续阅读 »

1.概念

函数是执行操作并可以返回值的离散代码块。在 Kotlin 中,函数是使用 fun 关键字声明的,并且可以使用接收具名值或默认值的参数。与特定类关联的函数称为方法。

  • 一个用于执行特定任务的代码块
  • 它可以将大型的程序分解为小型的模块
  • 使用关键字 fun 来声明
  • 可以通过参数接收具名值或默认值


2.函数的组成部分

我们可以使用 fun 关键字,并紧跟一个函数名来定义一个函数。例如:
fun sayHello(text:String) :Unit{
println("hello world!")
}

其中 sayHello就是函数名小括号()内text是函数参数大括号{}中是函数具体代码Unit是返回值(当函数没有任何返回的时候那它的返回值便是UnitUnit 类型只包含一个值,即:Unit 。返回Unit类型的声明是可选的。)。

以上函数等价于:

fun sayHello(text:String){
println("hello world!")
}

2.1函数参数

函数可能会有:

  • 默认参数

  • 必选参数

  • 具名参数

我们重新编写一个 sayHello 函数,这个函数接收一个 String 类型的参数: name,并且会打印出输入的姓名。name 的默认值为 "张三"。

fun sayHello(name: String = "张三") {
println("你好:${name}")
}

sayHello() =>你好:张三

sayHello("李四") =>你好:李四

sayHello(name="王五") =>你好:王五

在 main() 函数中,有三种方法可以调用 sayHello() 函数:

  • 在调用函数时,使用默认参数;
  • 调用函数时,不使用参数名传入 speed;
  • 调用函数时,传入名为 speed 的参数。

如果没有为某个参数指定默认值,则该参数为必选参数。

fun sayHello(name: String) {
println("你好:${name}")
}

sayHello() =>编译错误:Kotlin: No value passed for parameter 'name'

sayHello("李四") =>你好:李四

sayHello(name="王五") =>你好:王五

为了提升可读性,可以向必选参数传入具名值。就是我们上述代码中的 sayHello(name="王五")调用方式 。

函数可以同时拥有默认参数和必选参数。我们改造一下sayHello函数 。

fun sayHello(name: String, age: Int = 6, sex: String = "男") {
println("你好:${name} ,你的年龄:$age ,你的性别:$sex")
}
sayHello(name = "王五") =>你好:王五,你的年龄:6,你的性别:男

sayHello("王五",sex = "女") =>你好:王五 ,你的年龄:6 ,你的性别:女

参数可以通过它们的名称进行传递 (具名参数)。在函数包含大量参数或者默认参数时,具名参数非常方便。默认参数和具名参数可以减少重载并优化代码可读性。


3.紧凑函数

如果一个函数只返回了一个表达式,那么它的大括号可以省略,而函数体可以在 “=” 符号后指定。

fun getSum(x: Int, y: Int): Int {          =>完整版
return x + y
}

fun getSum(x: Int, y: Int): Int = x + y =>紧凑版


4.Lambda 表达式与高阶函数

Kotlin 中函数是 first-class(头等函数)

  • Kotlin 的函数可以储存在变量和数据结构中
  • 函数可以作为其他高阶函数的参数和返回值
  • 可以使用高阶函数创建新的 “内建” 函数

4.1 Lambda 函数

除了传统的命名函数外,Kotlin 还支持 lambda 表达式。lambda 是用来创建函数的表达式,但不同于声明已命名的函数,以这种方式声明的函数没有名称。lambda 很好用的一点便是可以作为数据传递。在其他编程语言中,lambda 也被称为匿名函数、函数字面量或其他类似的名称。
像具名函数一样,lambda 也可以有参数。对于 lambda 来说,参数 (及其类型,如果需要声明的话) 位于函数箭头 -> 的左侧。要执行的代码在函数箭头的右侧。将 lambda 赋值给变量后,就可以像函数一样调用它。

kotlin可以声明一个存储函数的变量

val getSum = { x: Int, y: Int -> x + y }
println(getSum(1, 2)) =>3

其中x,y就是参数。->为函数箭头 ,需要执行的代码再其右侧。其中getSum就是变量名。

4.2高阶函数

高阶函数接收函数作为参数,或以函数作为返回值。

fun getSum(x: Int, y: Int, sum: (Int, Int) -> Int): Int {
return sum(x, y)
}
val sum: (Int, Int) -> Int = { x, y -> x + y }
println(getSum(1, 2, sum)) =>3

此段代码中函数体调用了作为第三个参数传入的函数,并将第一个第二个参数传递给该函数。

lambda 的真正作用在于可以使用它们创建高阶函数,高阶函数接收另一个函数作为参数。

使用函数类型可将其实现与使用处分离。

4.3传递函数引用

 使用:: 操作符将具名函数作为参数传入另一个函数。

fun sum(x: Int, y: Int): Int {
return x + y
}
println(getSum(1, 2, ::sum))

:: 操作符让 Kotlin 知道我们正在将函数引用作为参数传递,这样一来 Kotlin 便不会尝试调用该函数。

在使用高阶函数时,Kotlin 倾向于将接收函数的参数作为函数的最后一个参数。Kotlin 有一个特别的语法,叫做尾随参数调用语法,可以让我们的代码更加简洁。在下面代码中中,我们可以为函数参数传递一个 lambda,而不必将 lambda 放在括号中。

println(getSum(1, 2,{ x, y -> x + y }) )

等价于
println(getSum(1, 2) { x, y -> x + y })

Kotlin 内建的许多函数,其声明方式都遵循了尾随参数调用语法。

inline fun repeat(times: Int, action: (Int) -> Unit)

repeat(3) {
println("hello world!")
}
收起阅读 »

小程序框架对比(Taro VS uni-app)

前前段时间,要开发一个小程序,需要选一个跨平台的框架,为此做了一些调研,在这里记录一下。 目前的跨平台方案大致是以下三种类型,各有优劣。 结合项目自身情况,我选择了第三种类型的框架,再结合支持多平台的要求,重点锁定在了Taro和uni-app之间。 ...
继续阅读 »

前前段时间,要开发一个小程序,需要选一个跨平台的框架,为此做了一些调研,在这里记录一下。


目前的跨平台方案大致是以下三种类型,各有优劣。


image.png


结合项目自身情况,我选择了第三种类型的框架,再结合支持多平台的要求,重点锁定在了Tarouni-app之间。















































框架技术栈微信小程序H5App支付宝/百度小程序
TaroReact/Vue
uni-appVue
WePYVue
mpvueVue

Taro


开发者:


京东


优缺点:


Taro在App端使用的是React Native的渲染引擎,原生的UI体验较好,但据说在实时交互和高响应要求的操作方面不是很理想。


微信小程序方面,结合度感觉没有那么顺滑,有一些常见功能还是需要自己去封装。


另外就是开发环境难度稍高,需要自己去搭建iOS和Android的环境,对于想要一处开发到处应用的傻瓜式操作来讲,稍显繁琐。


但Taro 3的出现,支持了React 和 Vue两种DSL,适合的人群会更多一点,并且对快应用的支持也更好。


案例:


image.png


学习成本:


React、RN、小程序、XCode、Android Studio


uni-app


开发者:


DCloud


优缺点:


uni-app在App渲染方面,提供了原生渲染引擎和小程序引擎的双选方案,加上自身的一些技术优化(renderjs),对于高性能和响应要求的场景展现得更为流畅。


另外它整体的开发配套流程也做得很容易上手。比如有丰富的插件市场,使用简单,支持大量常用场景。


还比如它的定制IDE——HBuilder,提供了强大的整合能力。在用HBuilder之前,我心想:“还要多装一个编辑器麻烦,再好用能有VS Code好用?”用过之后:“真香!”


虽然用惯了VS Code对比起来还是有一些痛点没有解决,但是对于跨平台开发太友好了,其他缺点都可以忍受。HBuilder里支持直接跳转到微信开发者工具调试,支持真机实时预览,支持直接打包小程序和App,零门槛上手。


image.png


不过,uni-app也还是不够成熟,开发中也存在一些坑,需要不时到论坛社区去寻找答案。


代表产品:


image.png


学习成本:


Vue、小程序


总结


跨平台方案目前来看都不完善,适合以小程序、H5为主,原生APP(RN)为辅,不涉及太过复杂的交互的项目。


uni-app 开发简单,小项目效率高,入门容易debug难,不适合中大型项目。
Taro 3 开发流程稍微复杂一点,但复杂项目的支持度会稍好,未来可以打通React和Vue,已经开始支持RN了。



  1. 不考虑原生RN的话二者差不多,考虑RN目前Taro3不支持,只能选uni-app;

  2. 开发效率uni-app高,有自家的IDE(HBuilderX),编译调试打包一体化,对原生App开发体验友好;

  3. 个人技术栈方面倾向于Taro/React,但项目角度uni-app/Vue比较短平快,社区活跃度也比较高。

作者:sherryhe
链接:https://juejin.cn/post/6974584590841167879

收起阅读 »

前端重新部署后,领导跟我说页面崩溃了..

背景: 每次前端更新,重新部署后,用户还停留在更新之前的页面,当请求页面数据时,会导致页面白屏,报错信息如下: Uncaught ChunkLoadError: Loading chunk {n} failed. 原因 每次更新后,用户端的html文件中的 j...
继续阅读 »

背景:


每次前端更新,重新部署后,用户还停留在更新之前的页面,当请求页面数据时,会导致页面白屏,报错信息如下:


Uncaught ChunkLoadError: Loading chunk {n} failed.


原因


每次更新后,用户端的html文件中的 js 和 css 名称就和服务器上的不一致导致,导致加载失败。


解决方案


1.对error事件进行监听,检测到错误之后重新刷新


      window.addEventListener(
'error',
function (event) {
if (
event.message &&
String(event.message).includes('Loading chunk') &&
String(event.message).includes('failed')
) {
window.location.replace(window.location.href);
}
},
true,
);

2.对window.console.error事件监听,效果同上


      window.console.error = function () {
console.log(JSON.stringify(arguments), 'window.console.error'); // 自定义处理
};

3.其他方案


如:HTTP2.0推送机制 / fis3 构建 /webSocket通知等,未尝试


注:有好的方案可以在下面评论讨论哈


本篇收录在个人工作记录专栏中,专门记录一些比较有意思的场景和问题。


后记


在之后的某一天,该问题再次暴露出来。源于一位同事在使用过程中,会不定页面的出现报错情况,报错如下:


image.png


很明显,还是资源加载问题,按道理讲应该可以走入我们逻辑进行刷新。但是当时用户反馈:刷新也不能解决问题,强制刷新才可以解决。


分析:刷新为什么不能解决问题?其实还是因为当用户第一次因为网络原因未成功加载到资源,后续刷新均走的缓存,因此让用户手动去刷新解决不了问题。


解决方案:


不知道大家发现没有,上面对error事件进行监听代码中,并没有包括css失败的情况,因此匹配上css加载失败的情况即可。


更新代码如下:


      window.addEventListener(
'error',
function (event) {
if (
event.message &&
String(event.message).includes('chunk') &&
String(event.message).includes('failed')
) {
window.location.replace(window.location.href);
}
},
true,
);

可能有人会问,既然刷新解决不了问题,那window.location.replace(window.location.href)可以解决吗?


其实刷新的方法有很多,根据MDN的说法,Location.replace() 方法会以给定的URL替换当前的资源,因此可解决此问题。



作者:纵有疾风起
链接:https://juejin.cn/post/6981718762483679239

收起阅读 »

上一个程序员提桶跑路了!我接手后用这些方法优化了项目

平常我们在开发和维护项目的过程中,如果我们跑的项目有点大啊,或者数据太多,导致项目跑起来弊蜗牛还要慢,然后用户体验还不友好,对于新手程序员来说!老板天天都要你加班改!你还不敢辞职!这种时候,就很让人头痛了,怎么办! 但是!也不是没有办法的!骚年!你当时学vue...
继续阅读 »

平常我们在开发和维护项目的过程中,如果我们跑的项目有点大啊,或者数据太多,导致项目跑起来弊蜗牛还要慢,然后用户体验还不友好,对于新手程序员来说!老板天天都要你加班改!你还不敢辞职!这种时候,就很让人头痛了,怎么办!


但是!也不是没有办法的!骚年!你当时学vue的时候可不是这样说的!


接下来我来给你浓重介绍几个优化性能的小技巧,让你的项目蹭蹭蹭的飞起来!老板看了都直呼内行!


1.v-if和v-show的使用场景要区分


v-if 是条件渲染,当条件满足了,那肯定是渲染哇!如果你需要设置一个元素随时隐藏或者消失,然后用v-if是非常的浪费性能的,因为它不停的创建然后销毁。


但是它也是惰性的,如果你开始一给它条件为 false,它就害羞不出来了,跟你家女朋友一样天天晚上都不跟你回家,甚至你家里都没有你女朋友的衣物!然后结构页面里也不会渲染出来查不到这个元素,不知情的朋友以为你谈了个虚拟女友,直到你让它条件为 true ,它才开始渲染,也就是拿了结婚证才跟你回家。


v-show 就很简单,他的原理就是利用 css display 的属性,让他隐藏或者出现,所以一开始渲染页面哪怕我们看不到这个元素,但是它在文档的话,是确确实实存在的,只是因为 display:none; 隐藏了。




就像是你的打气女朋友,平常有人你肯定不敢打气哇!肯定是等夜深人静的时候,才偷偷打气,然后早上又继续放气藏起来,这样是不是很方便咧!所以这个元素你也就是你打气女朋友每天打气放气,是不是也没有那么费力咧!白天就可以藏起来快乐的上班啦!


好啦划重点啦!不要瞎鸡巴想什么女朋友了,女朋友只会影响我码项目的速度!


所以这样看来,如果是很少改变条件来渲染或者销毁的,建议是用 v-if ,如果是需要不断地切换,不断地隐藏又出现又隐藏这些场景的话, v-show 更适合使用!所以要在这些场景里合适的运用 v-if v-show 会节省很多性能以达到性能优化。


2.v-if和v-for不能同时使用


v-if v-for 一起使用时, v-for **** v-if 更高的优先级。这样就意味着 v-if 将分别重复运行于每一个 v-for 循中,那就是先运行 v-for 的循环,然后在每一个 v-for 的循环中,再进行 v-if 的条件对比,会造成性能浪费,影响项目的速度




如果按照下面的写法(我是用vue3写的),好家伙!直接不工作了,没有报错也没有页面,诡异的很


<template>
<div id="app">
<div v-for="item in list" v-if="list.flag" :key="item">{{item.color}}</div>
</div>
</template>
<script>
export default {
data(){
return{
list:[
{
color:"red",
flag:true
},
{
color:"green",
flag:false
},
{
color:"blue",
flag:true
},
],
}
}}
</script>

当你真的需要根据条件渲染页面的时候,建议是采用计算属性,这样及高效且美观,又不会出问题,如下面的代码展示


<template>
<div id="app">
<div v-for="item in newList" :key="item">{{item.color}}</div>
</div>
</template>
<script>
export default {
data(){
return{
list:[
{
color:"red",
flag:true
},
{
color:"green",
flag:false
},
{
color:"blue",
flag:true
},
],
}
},
computed:{
newList(){
return this.list.filter(list => {
return list.flag
})
}
}
}
</script>

3.computed和watch使用要区分场景


先看一下计算属性computed,它支持缓存,当依赖的数据发生变化的时候,才会重新计算,但是它并不支持异步操作,它无法监听数据变化。而且计算属性是基于响应式依赖进行缓存的。


再看一下侦听属性watch,它不支持缓存,它支持异步操作,当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。这是和computed最大的区别。


所以说,如果你的需求是写像购物车那种的,一个属性受其他属性影响的,用计算属性 computed 就像是你家的二哈,你不带它出去玩,你一回家就发现你家能拆的都被二哈拆掉了,因为你不带它出去跟你女朋友逛街!


如果是像写那种像模糊查询的,可以用侦听属性 watch ,因为可以一条数据影响多条数据。 比如你双12给你女朋友买了很多东西,那双十二之后,是不是很多机会回不去宿舍咧!


用好这两个,可以让你的代码更加高效,看起来也更加简洁优雅,让项目蹭蹭跑起来!这样都是一种优化性能的方式!


4.路由懒加载


当Vue项目是单页面应用的时候,可能会有很多路由引入 ,这样的话,使用 webpcak 打包后的文件会非常的大,当第一次进入首页的时候,加载的资源很多多,页面有时候会出现白屏的情况,对用户很不友好,体验也差。


但是,当我们把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应的组件,这样就很高效了。会大大提升首屏加载显示的速度,但是可能其他的页面的速度就会降下来。有利有弊吧,根据自己业务需求来使用,实现效果也非常的简单,在 router index.js 文件下,如下所示


import Home from '../views/Home'

const routes = [
{
path:'/home',
name:"Home", //这里没有用懒加载
component:Home
},
{
path:'/about',
name:'About',
component:()=>import(/*webpackChunkName:"about"*/ '../views/About.vue') //这里用了懒加载
}
]

打开浏览器运行,当我没有点击进入 about 组件的时候包的大小就如蓝色框住的那些,当我点击了 about 组件进入后,就增加了后面红色圈住的包,总的大小是增加了



所以,使用路由懒加载可以降低首次加载的时候的性能消耗,但是后面打开这些组件可能会有所减慢,建议是如果体积不大的又不用马上展示的页面可以使用路由懒加载降低性能消耗,从而做到性能优化!


5.第三方插件按需引入


比如我们做一个项目,如果是全局引入第三方插件,打包构建的时候,会将别人整个插件包也一起打包进去,这样的话文件是非常庞大的,然后我们就需要将第三方插件按需引入,这个就需要自己去根据每个插件的官方文档在项目配置,始终就是一句话,用什么引什么!


6.优化列表的数据


当我们遇到哪些,一开始取的数据非常的庞大,然后还要渲染在页面上的时候,比如一下子给你传回来10w条数据还要渲染在页面上,项目一下子渲染出来是非常的有难度的。


这个时候,我们就 需要采用窗口化的技术来优化性能,只需要渲染少部分的内容(比如一下子拿多少条数据),这样就可以减少重新渲染组件和创建dom节点的时间。可以看看下面代码


<template>
<div>
<h3>列表的懒加载</h3>
<div>
<div v-for="item in list">
<div>{{ item }}</div>
</div>
</div>
<div>
<div v-if="moreShowBoolen">滚动加载更多</div>
<div v-else>已无更多</div>
</div>
</div>
</template>
<script>
export default {
name: 'List',
data() {
return {
list: [],
moreShowBoolen: false,
nowPage: 1,
scrollHeight: 0,
};
},
mounted() {
this.init();
// document.documentElement.scrollTop获取当前页面滚动条的位置,documentElement对应的是html标签,body对应的是body标签
// document.compatMode用来判断当前浏览器采用的渲染方式,CSS1Compat表示标准兼容模式开启
window.addEventListener("scroll", () => {
let scrollY=document.documentElement.scrollTop || document.body.scrollTop; // 滚动条在Y轴上的滚动距离
let vh=document.compatMode === "CSS1Compat"?document.documentElement.clientHeight:document.body.clientHeight; // 页面的可视高度
let allHeight = Math.max(
document.body.scrollHeight,
document.documentElement.scrollHeight
); // 整个页面的高度
if (scrollY + vh >= allHeight) {
// 当滚动条滑到页面底部的时候触发这个函数继续添加数据
this.init2();
}
});
},
methods: {
init() {
//一开始就往list添加数据
for (let i = 0; i < 100; i++) {
this.list.push(i);
}
},
init2() {
for (let i = 0; i < 200; i++) {
// 当滑动到底部的时候,继续触发这个函数
this.list.push(i);
}
},
},
};
</script>

这样的话,就可以做到数据懒加载啦,根据需要,逐步添加数据进去,减少一次性拉取所有数据,因为数据是非常庞大的,这样就可以优化很多性能了!


作者:零零后程序员小三
链接:https://juejin.cn/post/7041471019327946759

收起阅读 »

axios 封装,API接口统一管理,支持动态API!

vue
分享一个自己封装的 axios 网络请求 主要的功能及其优点: 将所有的接口放在一个文件夹中管理(api.js)。并且可以支持动态接口,就是 api.js 文件中定义的接口可以使用 :xx 占位,根据需要动态的改变。动态接口用法模仿的是vue的动态路由,如果你...
继续阅读 »

分享一个自己封装的 axios 网络请求


主要的功能及其优点:


将所有的接口放在一个文件夹中管理(api.js)。并且可以支持动态接口,就是 api.js 文件中定义的接口可以使用 :xx 占位,根据需要动态的改变。动态接口用法模仿的是vue的动态路由,如果你不熟悉动态路由可以看看我的这篇文章:Vue路由传参详解(params 与 query)


1.封装请求:



  1. 首先在 src 目录下创建 http 目录。继续在 http 目录中创建 api.js 文件与 index.js 文件。

  2. 然后再 main.js 文件中导入 http 目录下的 index.js 文件。将请求注册为全局组件。

  3. 将下面封装所需代码代码粘到对应的文件夹


2.基本使用:


//示例:获取用户列表
getUsers() {
 const { data } = await this.$http({
   url: 'users' //这里的 users 就是 api.js 中定义的“属性名”
})
},

3.动态接口的使用:


//示例:删除用户
deleteUser() {
 const { data } = await this.$http({
   method: 'delete',
   //动态接口写法模仿的是vue的动态路由
   //这里 params 携带的是动态参数,其中 “属性名” 需要与 api 接口中的 :id 对应
   //也就是需要保证携带参数的 key 与 api 接口中的 :xx 一致
   url: {
     // 这里的 name 值就是 api.js 接口中的 “属性名”
     name: 'usersEdit',
     params: {
       id: userinfo.id,
    },
  },
})
},

4.不足:


封装的请求只能这样使用 this.$http() 。不能 this.$http.get()this.$http.delete()


由于我感觉使用 this.$http() 这种就够了,所以没做其他的封装处理


如果你有更好的想法可以随时联系我


如下是封装所需代码:



  • api.js 管理所有的接口


// 如下接口地址根据自身项目定义
const API = {
 // base接口
 baseURL: 'http://127.0.0.1:8888/api/private/v1/',
 // 用户
 users: '/users',
 // “修改”与“删除”用户接口(动态接口)
 usersEdit: '/users/:id',
}

export default API


  • index.js 逻辑代码


// 这里请求封装的主要逻辑,你可以分析并将他优化,如果有更好的封装方法欢迎联系我Q:2356924146
import axios from 'axios'
import API from './api.js'

const instance = axios.create({
 baseURL: API.baseURL,
 timeout: '8000',
 method: 'GET'
})

// 请求拦截器
instance.interceptors.request.use(
 config => {
   // 此处编写请求拦截代码,一般用于加载弹窗,或者每个请求都需要携带的token
   console.log('正在请求...')
   // 请求携带的token
   config.headers.Authorization = sessionStorage.getItem('token')
   return config
},
 err => {
   console.log('请求失败', err)
}
)

// 响应拦截器
instance.interceptors.response.use(
 res => {
   console.log('响应成功')
   //该返回对象会绑定到响应对象中
   return res
},
 err => {
   console.log('响应失败', err)
}
)

//options 接收 {method, url, params/data}
export default function(options = {}) {
 return instance({
   method: options.method,
   url: (function() {
     const URL = options.url

     if (typeof URL === 'object') {
       //拿到动态 url
       let DynamicURL = API[URL.name]

       //将 DynamicURL 中对应的 key 进行替换
       for (const key of Object.keys(URL.params)) {
         DynamicURL = DynamicURL.replace(':' + key, URL.params[key])
      }

       return DynamicURL
    } else {
       return API[URL]
    }
  })(),
   //获取查询字符串参数
   params: options.params,
   //获取请求体字符串参数
   data: options.data
})
}



  • main.js 将请求注册为全局组件


import Vue from 'vue'

// 会自动导入 http 目录中的 index.js 文件
import http from './http'

Vue.prototype.$http = http

作者:寸头男生
链接:https://juejin.cn/post/7006508579595223070

收起阅读 »

AES 前后端加解密方案

AES
AES 前后端加解密方案 背景 最近有一个需求:后端对敏感数据进行加密传输给前端,由前端解密后进行回显。在讨论之后,定下了AES加解密方案 概念 AES: 密码学中的高级加密标准(Advanced Encryption Standard,AES),又称Rijn...
继续阅读 »

AES 前后端加解密方案


背景


最近有一个需求:后端对敏感数据进行加密传输给前端,由前端解密后进行回显。在讨论之后,定下了AES加解密方案


概念


AES: 密码学中的高级加密标准(Advanced Encryption Standard,AES),又称Rijndael加密法,是美国联邦政府采用的一种区块加密标准,是最为常见的对称加密算法


密码说明


AES算法主要有四种操作处理,分别是:



  1. 密钥轮加(Add Round Key)

  2. 字节代换层(SubBytes)

  3. 行位移层(Shift Rows)

  4. 列混淆层(Mix Column)


主要是讲使用方案,所以这里不说太多废话了,对算法感兴趣的同学移步这里, 讲的非常详细,不过文章里的代码是使用C语言写的,为此找到了github上aes.js 的源码,感兴趣的同学移步这里


前端实现


现在简单说一下前端的实现:


我先找到了github上的源码,看了一下大概800行的样子。本来打算直接改吧改吧,封装成一个加解密的工具方法,直接扔在utils目录里的。本来也很成功的改好了,本地加解密试了一下,效果也很不错。根据github链接上的readme文档说明,封装了如下函数:


// 省略了改完的aes.js的代码。。。

// 加密 text 需要加密的文本 key 密钥
const toAESBytes = (text, key) => {
const textBytes = aesjs.utils.utf8.toBytes(text);
const aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(5));
const encryptedBytes = aesCtr.encrypt(textBytes);
const encryptedHex = aesjs.utils.hex.fromBytes(encryptedBytes);
console.log('加密后的文本:', encryptedHex);
return encryptedHex;
};

// 解密
const fromAESBytes = (text, key) => {
const encryptedBytes = aesjs.utils.hex.toBytes(text);
const aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(5));
const decryptedBytes = aesCtr.decrypt(encryptedBytes);
const decryptedText = aesjs.utils.utf8.fromBytes(decryptedBytes);
console.log('解密后的文本:', decryptedText);
return decryptedText;
}

但是这个方法在和后端对接的时候出现了一点偏差,死活也不能将后端加密后的数据成功解密。于是又向后端同学请教了一下,发现原因如下:


在AES加解密算法中,除了加解密的密文,也就是key需要一样之外,还有几样东西也非常重要:




  • AES 的算法模式需要保持一致


    关于算法模式,主要有以下几种:


      1. 电码本模式 Electronic Codebook Book (ECB);
    2. 密码分组链接模式 Cipher Block Chaining (CBC)
    3. 计算器模式Counter (CTR)
    4. 密码反馈模式(Cipher FeedBack (CFB)
    5. 输出反馈模式Output FeedBack (OFB)

    这么看的话,我上面的demo应该使用的就是计算器模式了!关于算法模式的介绍,感兴趣的同学请移步这里




  • 补码方式保持一致


    关于补码方式,我查到的以下几种:


      1. PKCS5Padding PKCS7Padding的子集,块大小固定为8字节
    2. PKCS7Padding 假设数据长度需要填充n(n>0)个字节才对齐,那么填充n个字节,每个字节都是n;如果数据本身就已经对齐了,则填充一块长度为块大小的数据,每个字节都是块大小。
    3. ZeroPadding 数据长度不对齐时使用0填充,否则不填充



  • 密钥长度保持一致


    AES算法一共有三种密钥长度:128、192、256。这个前后端的密钥长度确实是保持一致的。




  • 加密结果编码方式保持一致


    一般情况下,AES加密结果有两种编码方式:base64 和 16进制




所以到底是哪里出了问题呢?后端同学好心发给了我他后端的代码:


/**
* aes 加密 Created by xingxiping on 2017/9/20.
*/
public class AesUtils {
private static final String CIPHER_ALGORITHM = "AES"; // optional value AES/DES/DESede

private AesUtils(){

}
/**
* 加密
*
* @param content
* 源内容
* @param key
* 加密密钥
* @return
*/
public static String encrypt(String content, String key) throws Exception {
Cipher cipher = getCipher(key, Cipher.ENCRYPT_MODE);
byte[] byteContent = content.getBytes(StandardCharsets.UTF_8);
byte[] result = cipher.doFinal(byteContent);
return Base64Utils.encode(result);
}

/**
* 解密
*
* @param content
* 内容
* @param key
* 解密密钥
* @return
*/
public static byte[] decrypt(String content, String key) throws Exception {
Cipher cipher = getCipher(key, Cipher.DECRYPT_MODE);
byte[] bytes = Base64Utils.decode(content);
bytes = cipher.doFinal(bytes);
return bytes;
}

private static Cipher getCipher(String key, int cipherMode) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException {
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(), "AES");
cipher.init(cipherMode, secretKey);
return cipher;
}
}

破案了,后端老哥对加密后的结果进行了base64编码,然后我又仔细去看了一下aes.js源码,根本没有找到base64的影子啊!


于是在查找一翻资料以后,决定使用crypto-j,使用 Crypto-JS 可以非常方便地在 JavaScript 进行 MD5、SHA1、SHA2、SHA3、RIPEMD-160 哈希散列,进行 AES、DES、Rabbit、RC4、Triple DES 加解密。真是方便呀,老规矩,感兴趣的同学可以移步这里


以下是我又一轮的解决步骤:




  1. npm install crypto-js




  2. 在utils目录下新建一个文件aes.js




  3. 封装如下代码:


    // aes 解密
    import CryptoJS from 'crypto-js';

    // 解密 encryptedStr待解密字符串 pass 密文
    export const aesDecode = (encryptedStr, pass) => {
    const key = CryptoJS.enc.Utf8.parse(pass); // 通过密钥获取128位的key
    const encryptedHexStr = CryptoJS.enc.Base64.parse(encryptedStr); // 解码base64编码结果
    const encryptedBase64Str = CryptoJS.enc.Base64.stringify(encryptedHexStr);
    const decryptedData = CryptoJS.AES.decrypt(encryptedBase64Str, key, {
    mode: CryptoJS.mode.ECB,
    padding: CryptoJS.pad.Pkcs7
    });
    return decryptedData.toString(CryptoJS.enc.Utf8);
    }



  4. 然后就可以正常调用了!




最后,终于成功解密!


一点点小感悟


在日常工作中真的很少使用算法,对称加密在学校里听起来好像非常简单的样子,但是真的应用到生活中,特别是安全领域,还是非常复杂的。哎,学无止境吧~


感谢大家的阅读!


作者:溜溜球形废物
链接:https://juejin.cn/post/6951368041590423582
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

聊一聊ThreadLocal,终于搞明白了

ThreadLocal是什么? 试想以下情况: 在多线程的情况下,对一同一个变量操作可能会出现冲突,解决的办法就是对这个变量加锁。但是我们有时候其实就是想要一个全局变量,不想让多个线程去干扰。那么能不能有一个变量,名字相同,但是多个线程操作的时候又不会相互影响...
继续阅读 »

ThreadLocal是什么?


试想以下情况:


在多线程的情况下,对一同一个变量操作可能会出现冲突,解决的办法就是对这个变量加锁。但是我们有时候其实就是想要一个全局变量,不想让多个线程去干扰。那么能不能有一个变量,名字相同,但是多个线程操作的时候又不会相互影响呢?


从另外一个角度来说,对于一个变量,在一个线程的任何一个地方都可能需要用到,但是通过传参的方式又比较麻烦,有没有一个变量是贯穿整个线程,我们想取就能取到的呢。


ThreadLocal就是这么一个变量,那么这个变量是怎么实现的呢?


ThreadLocal源码分析


ThreadLocal github地址


首先看用法


public class Client {
private static final ThreadLocal<String> myThreadLocal = ThreadLocal.withInitial(() -> "This is the initial value");

public static void main(String[] args) {

for (int i = 0; i < 6; i++){
new Thread(new MyRunnable(), "线程"+i).start();
}

}

public static class MyRunnable implements Runnable {

@Override
public void run() {
String name = Thread.currentThread().getName();
System.out.println(name + "的threadLocal"+ ",设置为" + name);
myThreadLocal.set(name);
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {}
System.out.println(name + ":" + myThreadLocal.get());
}

}
}

------

线程0的threadLocal,设置为线程0
线程3的threadLocal,设置为线程3
线程4的threadLocal,设置为线程4
线程2的threadLocal,设置为线程2
线程1的threadLocal,设置为线程1
线程5的threadLocal,设置为线程5
线程0:线程0
线程4:线程4
线程5:线程5
线程2:线程2
线程1:线程1
线程3:线程3

例子中有六个线程,myThreadLocal里存的都是自己线程独有的变量。这样就实现了变量的线程隔离,而且如果不向传参数,在另外一个函数里直接就能get到这个变量,这对于很多场景下都非常有用。


我们下面来分析一下源码:



  1. 首先每一个Thread,都有一个ThreadLocalMap,变量名字叫做threadLocas,里面保存的是多个ThreadLocal,所以每一个线程才能保存属于自己线程的值。

  2. ThreadLocal封装了getMap()、Set()、Get()、Remove()4个核心方法。主要是对ThreadLocalMap来进行操作。

  3. ThreadLocalMap是一个ThreadLocal的内部类,它实现了一个自定义的Map,ThreadLocalMap中的Entry[]数组存储数据。

  4. Entry的键是threadLocal变量本身,值就是设置的变量的值。Entry的key是对ThreadLocal的弱引用,当ThreadLocal不再有强引用的时候,就会清理掉这个key,防止内存泄漏(然而并不能,后面会说)


5abe86d1459c394b7552c1ef9d7e370c.png


get方法


public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}



  1. 获取当前的Thread对象,通过getMap获取Thread内的ThreadLocalMap,ThreadLocalMap的定义如下:ThreadLocal.ThreadLocalMap threadLocals = null;

  2. 如果map已经存在,以当前的ThreadLocal为键,获取Entry对象,并从从Entry中取出值

  3. 否则,调用setInitialValue进行初始化。


setInitialValue


private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}



  1. 首先是调用initialValue生成一个初始的value值,深入initialValue函数,我们可知它就是返回一个null;

  2. 然后还是在get以下Map,如果map存在,则直接map.set,这个函数会放在后文说;

  3. 如果不存在则会调用createMap创建ThreadLocalMap,这里正好可以先说明下ThreadLocalMap了。


ThreadLocalMap


createMap


void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private static final int INITIAL_CAPACITY = 16;

private Entry[] table;

private int size = 0;

private int threshold; // Default to 0

private void setThreshold(int len) {
threshold = len * 2 / 3;
}

private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}

private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
...
}


  1. 首先是Entry的定义,前面已经说过;

  2. 初始的容量为INITIAL_CAPACITY = 16

  3. 主要数据结构就是一个Entry的数组table;

  4. size用于记录Map中实际存在的entry个数;

  5. threshold是扩容上限,当size到达threashold时,需要resize整个Map,threshold的初始值为len * 2 / 3

  6. nextIndex和prevIndex则是为了安全的移动索引,后面的函数里经常用到。


map.getEntry


private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}



  1. 计算索引位置

  2. 获取Entry,如果Entry存在,且key和threadLocal相等,那么返回

  3. 否则,调用getEntryAfterMiss。


getEntryAfterMiss


private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;

while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}


  1. 如果k==key,那么代表找到了这个所需要的Entry,直接返回;

  2. 如果k==null,那么证明这个Entry中key已经为null,那么这个Entry就是一个过期对象,这里调用expungeStaleEntry清理该Entry。



为什么会需要清理呢?


如果说ThreadLocal变量被人为的置为null了,ThreadLocal对象只有一个弱引用指着,就会被GC,Entry的key没有了,value可能会内存泄漏。ThreadLocal在每一个get,set的时候都会清理这种过期的key。


为什么需要循环查找key?


这是一种解决hash冲突的手段,这里用的是开放地址法,既有冲突之后,把要插入的元素放在要插入的位置后面为null的地方。HashMap则采用的是链地址法。



expungeStaleEntry


private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;

// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;

// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;

// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}


  1. expunge entry at staleSlot:这段主要是将i位置上的Entry的value设为null,Entry的引用也设为null,那么系统GC的时候自然会清理掉这块内存;

  2. Rehash until we encounter null: 这段就是扫描位置staleSlot之后,null之前的Entry数组,清除每一个key为null的Entry,同时若是key不为空,做rehash,调整其位置。



这里rehash的作用是什么?


我们清理的过程中会把某个值设置为null,如果之前这个值后面的区域是和前两连起来的,那么下次循环查找的时候,就会只查到null为止。比如三个hash值碰撞的key,中间的那个被删除了,那么第三个key在查找的时候会从第一个开始查找,查找到第二个就停止了,第三个就查不到了。



set


public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

map.set


private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);

for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();

if (k == key) {
e.value = value;
return;
}

if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}

tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}


  1. 首先还是根据key计算出位置i,然后查找i位置上的Entry,

  2. 若是Entry已经存在并且key等于传入的key,那么这时候直接给这个Entry赋新的value值。

  3. 若是Entry存在,但是key为null,则调用replaceStaleEntry来更换这个key为空的Entry

  4. 不断循环检测,直到遇到为null的地方,这时候要是还没在循环过程中return,那么就在这个null的位置新建一个Entry,并且插入,同时size增加1。

  5. 最后调用cleanSomeSlots,这个函数就不细说了,你只要知道内部还是调用了上面提到的expungeStaleEntry函数清理key为null的Entry就行了,最后返回是否清理了Entry,接下来再判断sz>thresgold,这里就是判断是否达到了rehash的条件,达到的话就会调用rehash函数。


rehash


private void rehash() {
expungeStaleEntries();

// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}


private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;

for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null; // Help the GC
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}

setThreshold(newLen);
size = count;
table = newTab;
}


  1. 首先,size大于threshold的时候才会rehash。

  2. 清理空key,如果size大于3/4的threshold,调用resize()函数。

  3. 每次扩容大小扩展为原来的2倍,然后再一个for循环里,清除空key的Entry,同时重新计算key不为空的Entry的hash值,把它们放到正确的位置上,再更新ThreadLocalMap的所有属性。


remove


private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}

先计算出hash值,若是第一次没有命中,就循环直到null,在此过程中也会调用expungeStaleEntry清除空key节点。


什么是内存泄漏?


当程序分配了空间但是却忘了回收导致以后的程序都无法或暂时无法使用这块空间,就发生了内存泄漏。和内存溢出不一样,内存溢出是内存不足的时候出现的。这块要理解清楚,才能明白ThreadLocal为什么会导致内存泄漏。


Java 引用类型


要说到ThreadLocal引起内存泄漏,还得从java的四种引用类型说起。


java中有四种引用类型,分别是强软弱虚。


强引用


一个对象被强引用,那么他就不会被回收。


软引用


如果一个对象只具有软引用,那么它的性质属于可有可无的那种。如果此时内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。


软引用可以和一个引用队列联合使用,如果软件用所引用的对象被垃圾回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。


    Object obj = new Object();
ReferenceQueue queue = new ReferenceQueue();
SoftReference reference = new SoftReference(obj, queue);
//强引用对象滞空,保留软引用
obj = null;
当内存不足时,软引用对象被回收时,reference.get()为null,此时软引用对象的作用已经发挥完毕,这时将其添加进ReferenceQueue 队列中

如果要判断哪些软引用对象已经被清理:


    SoftReference ref = null;
while ((ref = (SoftReference) queue.poll()) != null) {
//清除软引用对象
}

弱引用


弱引用和软引用的区别就是,如果一个对象只有弱引用,那么只要GC,不管内存够不够,都会回收他的内存。注意这里的”只有弱引用“。如果这个对象还被其他变量强引用,那么他是不会被回收的。


虚引用


如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收的活动。





































引用类型被垃圾回收时间用途生存时间
强引用从来不会对象的一般状态JVM停止运行时终止
软引用在内存不足时对象缓存内存不足时终止
弱引用在垃圾回收时对象缓存垃圾回收时终止
虚引用UnkonwnUnkonwnUnkonwn


为什么要有四种引用类型?



  1. 可以让程序员通过代码的方式来决定某个对象的生命周期。

  2. 有利于垃圾回收

  3. 能够实现一些复杂的数据结构。



ThreadLocal什么情况下会出现内存泄漏?


threadlocal里面使用了一个存在弱引用的map,当释放掉threadlocal的强引用以后,map里面的value却没有被回收.而这块value永远不会被访问到了. 所以存在着内存泄露。


如果这个时候还会去调用get set方法,那么这块内存可能会被清理掉。


如果没有去调用get set方法,如果这个线程很快销毁了,那么也不会内存泄漏。


最坏的情况就是,threadLocal对象设置成null了,然后使用线程池,这个线程被重复使用了,但是有一直没有调用get set方法,这个期间就会发生真正的内存泄漏。


其实ThreadLocal发生内存泄露的条件还是比较苛刻的,只要是使用规范,那么就没有什么问题。


ThreadLocal最佳实践



  1. 每次使用完手动调用remove函数,删除不再使用的ThreadLocal.

  2. 可以将ThreadLocal设置成private static的,这样ThreadLocal会尽量和线程本身一起消亡。


ThreadLocal应用案例



管理数据库连接。


  假如A类的方法a中,会调用B类的方法b和C类的方法c,a方法开启了事务,b方法和c方法会去操作数据库。我们知道,要想实现事务,那么b方法和c方法中所使用的的数据库连接一定是同一个连接,那怎么才能实现所用的是同一个数据库连接呢?答案就是通过ThreadLocal来管理。


MDC日志链路追踪。


MDC(Mapped Diagnostic Contexts)主要用于保存每次请求的上下文参数,同时可以在日志输出的格式中直接使用 %X{key} 的方式,将上下文中的参数输出至每一行日志中。而保存上下文信息主要就是通过ThreadLocal来实现的。
假如在交易流程每个环节的日志中,你都想打印全局流水号transId,流程可能涉及多个系统、多个线程、多个方法。有一些环节中,全局流水号并不能当做参数传递,那你怎么才能获取这个transId参数呢,这里就是利用了Threadlocal特性。每个系统或者线程在接收到请求时,都会将transId存放到ThreadLocal中,在输出日志时,将transId获取出来,进行打印。这样,我们就可以通过transId,在日志文件中查询全链路的日志信息了。



InheritableThreadLocal


使用ThreadLocal时,子线程获取不到父线程通过set方法保存的数据,要想使子线程也可以获取到,可以使用InheritableThreadLocal类。


作者:dsvshx
链接:https://juejin.cn/post/7041215155408994317
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

【墙裂推荐】球球了,RPC之间调用别再使用 if-else做参数校验了

RPC
RPC调用时使用 @Validated进行参数校验不起作用 球球了,RPC之间调用别再使用 if-else做参数校验了。众所周知,@Validated 是一款非常好用的参数校验工具。但在RPC调用时不可用,在当前的微服务大环境下,微服务之间的调用怎么做到优雅...
继续阅读 »

RPC调用时使用 @Validated进行参数校验不起作用


球球了,RPC之间调用别再使用 if-else做参数校验了。众所周知,@Validated 是一款非常好用的参数校验工具。但在RPC调用时不可用,在当前的微服务大环境下,微服务之间的调用怎么做到优雅的参数校验呢?


话不多说,直接上干货


引包


<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

1. 参数校验 这里我们先要定义一个注解来代替来继承 @Validated


import org.springframework.validation.annotation.Validated;

@Validated
public @interface RPCValidated {
}

2. 然后使用AOP来解析参数,进行参数校验。


import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator;
import org.hibernate.validator.resourceloading.PlatformResourceBundleLocator;
import org.springframework.stereotype.Component;
import wangjubao.base.common.extend.Response;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

@Aspect
@Component
@Slf4j
public class ValidatedAop {
private static Validator validator;

static {
validator = Validation.byDefaultProvider().configure()
.messageInterpolator(new ResourceBundleMessageInterpolator(
new PlatformResourceBundleLocator("validationMessages")))
.buildValidatorFactory().getValidator();
}

@Around("@annotation(com.qiaoba.annotation.RPCValidated))")
public Object around(ProceedingJoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 执行方法参数的校验
Set<ConstraintViolation<Object>> constraintViolations = validator.forExecutables().validateParameters(joinPoint.getThis(), signature.getMethod(), args);
List<String> messages = new ArrayList<>();
for (ConstraintViolation<Object> error : constraintViolations) {
messages.add(error.getMessage());
}
if (!messages.isEmpty()) {
return Response.paramError("参数错误:", messages.get(0));
}
try {
return joinPoint.proceed(args);
} catch (Throwable e) {
e.printStackTrace();
return Response.error("操作失败:", e.getMessage());
}
}
}

3. 使用方法,在接口Impl实现类加上定义的@RPCValidated


@Override
@RPCValidated
public Response create(MessageSmsRechargeDto dto) {
//todo:业务逻辑......
}

4. 在Interfaces接口层加上@Valid注解


Response create(@Valid MessageSmsRechargeDto params);

5. 实体类


@Data
public class MessageSmsRechargeDto implements Serializable {


/**
* 充值公司
*/
@NotNull(message = "充值公司不能为空 ")
private Long companyId;

/**
* 充值备注
*/
@NotEmpty(message = "充值备注不能为空 ")
private String rechargeRemark;
}

--- 至此,整个流程完成,加上自定义的参数校验注解@RPCValidated后,就可以优雅的进行参数校验,不用再写各种if-else 做参数校验了


作者:乔巴21121
链接:https://juejin.cn/post/7040683548604366885
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Flutter APP 前期准备工作

组件库可参考:flutter.dev、bruno(贝壳开源组件库) 以下从部分GetX文档转载 用于记录。 框架: Flutter GetX GetX 是 Flutter 上的一个轻量且强大的解决方案:高性能的状态管理、智能的依赖注入和便捷的路由管理。 Get...
继续阅读 »

组件库可参考:flutter.dev、bruno(贝壳开源组件库)


以下从部分GetX文档转载 用于记录。
框架: Flutter GetX


GetX 是 Flutter 上的一个轻量且强大的解决方案:高性能的状态管理、智能的依赖注入和便捷的路由管理。


GetX 有3个基本原则:


性能: GetX 专注于性能和最小资源消耗。GetX 打包后的apk占用大小和运行时的内存占用与其他状态管理插件不相上下。效率: GetX 的语法非常简捷,并保持了极高的性能,能极大缩短你的开发时长。 结构: GetX 可以将界面、逻辑、依赖和路由完全解耦,用起来更清爽,逻辑更清晰,代码更容易维护。 GetX 并不臃肿,却很轻量。


三大功能


状态管理


目前,Flutter有几种状态管理器。但是,它们中的大多数都涉及到使用ChangeNotifier来更新widget,这对于中大型应用的性能来说是一个很糟糕的方法。你可以在Flutter的官方文档中查看到,ChangeNotifier应该使用1个或最多2个监听器,这使得它们实际上无法用于任何中等或大型应用。


Get 并不是比任何其他状态管理器更好或更差,而是说你应该分析这些要点以及下面的要点来选择只用Get,还是与其他状态管理器结合使用。


Get不是其他状态管理器的敌人,因为Get是一个微框架,而不仅仅是一个状态管理器,既可以单独使用,也可以与其他状态管理器结合使用。


Get有两个不同的状态管理器:简单的状态管理器(GetBuilder)和响应式状态管理器(GetX)。


响应式状态管理器


响应式编程可能会让很多人感到陌生,因为觉得它很复杂,但是GetX将响应式编程变得非常简单。



  • 你不需要创建StreamControllers.

  • 你不需要为每个变量创建一个StreamBuilder。

  • 你不需要为每个状态创建一个类。

  • 你不需要为一个初始值创建一个get。


使用 Get 的响应式编程就像使用 setState 一样简单。


让我们想象一下,你有一个名称变量,并且希望每次你改变它时,所有使用它的小组件都会自动刷新。


这就是你的计数变量。


var name = 'Jonatas Borges';

要想让它变得可观察,你只需要在它的末尾加上".obs"。


var name = 'Jonatas Borges'.obs;

而在UI中,当你想显示该值并在值变化时更新页面,只需这样做。


Obx(() => Text("${controller.name}"));

这就是全部,就这么简单。


关于状态管理的更多细节


关于状态管理更深入的解释请查看这里。在那里你将看到更多的例子,以及简单的状态管理器和响应式状态管理器之间的区别


你会对GetX的能力有一个很好的了解。


路由管理


如果你想免上下文(context)使用路由/snackbars/dialogs/bottomsheets,GetX对你来说也是极好的,来吧展示:


在你的MaterialApp前加上 "Get",把它变成GetMaterialApp。


GetMaterialApp( // Before: MaterialApp(
home: MyHome(),
)

导航到新页面


Get.to(NextScreen());

用别名导航到新页面。查看更多关于命名路由的详细信息这里


Get.toNamed('/details');

要关闭snackbars, dialogs, bottomsheets或任何你通常会用Navigator.pop(context)关闭的东西。


Get.back();

进入下一个页面,但没有返回上一个页面的选项(用于闪屏页,登录页面等)。


Get.off(NextScreen());

进入下一个页面并取消之前的所有路由(在购物车、投票和测试中很有用)。


Get.offAll(NextScreen());

注意到你不需要使用context来做这些事情吗?这就是使用Get路由管理的最大优势之一。有了它,你可以在你的控制器类中执行所有这些方法,而不用担心context在哪里。


关于路由管理的更多细节


关于别名路由,和对路由的低级控制,请看这里


依赖管理


Get有一个简单而强大的依赖管理器,它允许你只用1行代码就能检索到与你的Bloc或Controller相同的类,无需Provider context,无需inheritedWidget。


Controller controller = Get.put(Controller()); // 而不是 Controller controller = Controller();


  • 注意:如果你使用的是Get的状态管理器,请多注意绑定api,这将使你的界面更容易连接到你的控制器。


你是在Get实例中实例化它,而不是在你使用的类中实例化你的类,这将使它在整个App中可用。 所以你可以正常使用你的控制器(或类Bloc)。


提示:  Get依赖管理与包的其他部分是解耦的,所以如果你的应用已经使用了一个状态管理器(任何一个,都没关系),你不需要全部重写,你可以使用这个依赖注入。


controller.fetchApi();

想象一下,你已经浏览了无数条路由,现在你需要拿到一个被遗留在控制器中的数据,那你需要一个状态管理器与Provider或Get_it一起使用来拿到它,对吗?用Get则不然,Get会自动为你的控制器找到你想要的数据,而你甚至不需要任何额外的依赖关系。


Controller controller = Get.find();
//是的,它看起来像魔术,Get会找到你的控制器,并将其提供给你。你可以实例化100万个控制器,Get总会给你正确的控制器。

然后你就可以恢复你在后面获得的控制器数据。


Text(controller.textFromApi);

作者:_Archer
链接:https://juejin.cn/post/7040432677299683365
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

cocoapods-binary工作原理及改进

iOS
「这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战」 在iOS开发中,如果能够对一些稳定的组件能够二进制化,那么将大大的缩减我们在开发过程中的编译时间。在基于Cocaopods工程,快速实现Swift组件二进制一文中,我们讲述了,借助P...
继续阅读 »

「这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战


iOS开发中,如果能够对一些稳定的组件能够二进制化,那么将大大的缩减我们在开发过程中的编译时间。在基于Cocaopods工程,快速实现Swift组件二进制一文中,我们讲述了,借助Pods工程和Shell脚本,一步实现二进制打包,但需要我们手动更改podspec文件,采用这种方式,如果作为依赖项加入到其他工程中,还会出现二进制源码共存的情况,今天介绍一个cocoapods插件 cocoapods-binary,可以实现组件预编译,该工程已经两年多没维护了,随着pods更新,有了一些小bug。基于源码,我对该插件做了几点更改,又可以开心的玩耍了.在了解该插件之前,我们先大概了解下,输入pod install之后发生了什么?


Pod install


如果你想调试cocoapods工程,可以查看之前的文章Ruby和Cocoapods文章合集


一图胜千言,当输入Pod install


Pod install.png



  • 1,首先校验是否有Podfile文件,并解析为Podfile对象

  • 2,准备阶段,安装插件,调用pre_install阶段的插件

  • 3,解析依赖,通过 当前的Podfile文件 和 上一次的Pofile.Lock,Manifest.Lock文件进行比对,确认哪些文件需要更新。

  • 4,下载依赖,更新需要更改的文件,并执行Podfile里面定义的pre_installhook函数。

  • 5,集成阶段,生成新的Pods.xcodeproj工程文件,并执行Podfile里面的post_installhook函数。

  • 6,写入新的Lockfile信息。

  • 7,执行 post_install阶段的插件,并输出安装信息。


cocoapods-binary是以插件的形式,在Pod工程的pre_install阶段进行预编译的,


cocoapods-binary工作流


pre_install 插件


cocoapods-binary中的,通过HookManager来注册插件的执行时机


Pod::HooksManager.register('cocoapods-binary', :pre_install) do |installer_context|

end


主流程


主要流程如下图所示


截屏2021-11-28 下午7.20.16.png



  • 1,必须使用framework的形式,也就是use_frameworks

  • 2,在Pods文件夹下,创建一个名为_Prebuild的文件夹,作为预编译沙箱

  • 3,在当前环境下,读取Podfile文件,并创建Podfile对象。

  • 4,读取Podflie.lock文件,创建 Lockfile对象。

  • 5,创建预编译安装器,沙箱地址为 预编译沙箱

  • 6,对比PodfilePodfile.lock,得到已更改的pod_name

  • 7,使用预编译安装器pod_name的源代码下载到预编译沙箱中,并生成新的Pods.xcodeproj文件。开始编译需要更新的framework

  • 8,回到主工程,继续执行 pod install的后续流程,在这一步修该需要二进制文件的podspec文件。


解析自定义参数


在插件中,有两个自定义的参数 :binaryall_binary!,是通过自定义DSL来实现的,有对这一块不熟悉的,可以参考我的这篇文章Cocoapods之 Podfile文件


Podfile.png
创建Podfile对象时,通过 method swizzling来hook:parse_inhibit_warnings方法,拿到我们在Podfile文件中写入的配置选项。将需要预编译pod,保存到数组中。


old_method = instance_method(:parse_inhibit_warnings)
define_method(:parse_inhibit_warnings) do |name, requirements|
variables = requirements
parse_prebuild_framework(name, requirements)
old_method.bind(self).(name, variables)
end
复制代码

Ruby中,Method Swizzling主要分为三步:



  • 1,获取parse_inhibit_warnings实例方法。

  • 2,定义一个相同名字的方法。

  • 3,调用原来的方法。


对比Lockfile


lockfile.png
在这里 Podfle.lock预编译沙箱Manifest.lock是一样的,通过对比可以一个Hash对象


<Pod::Installer::Analyzer::SpecsState:0x00007f83370c61a8 @added=#<Set: {}>, @deleted=#<Set: {}>, @changed=#<Set: {}>, @unchanged=#<Set: {"Alamofire", "SnapKit"}>>


可以很清楚的知道哪些pod库发生了更改。如果有改动,则就在预编译沙箱进行install


binary_installer.install!


pre_install.png
在这一阶段,主要是在预编译沙箱中拉取framework源码和修改Pods.xcodeproj文件,在 Manifest.lock成功写入预编译沙箱,通过hook run_plugins_post_install_hooks函数,在预编译沙箱中,使用 xcodebuild命令,编译每一个需要更新的pod_target,并将编译好的framework放至GeneratedFrameworks目录下。


回到主工程执行pod install


截屏2021-11-28 下午8.22.44.png


编译完成后,就回到了主工程里面的 Pod install流程中。对:resolve_dependencies方法进行Method Swizzling,对需要更改的pod_target进行修改。通过修改内存中的Pod::Specification对象的vendored_frameworkssource_filesresource_bundlesresources属性,来引用已经编译好的framework


工作流总结


通过对每一个阶段的了解,我们了解了作者的思路是这样的:
1,先将源码和Pods.project安装到预编译沙箱中 。
2,借助于Pods.project工程,使用xcodebuild编译需要预编译的scheme
3,巧妙的利用Method Swizzling,在分析依赖阶段,修改Pod::Specification对象,完成二进制的引用等工作。


现有问题


1,:binary => true 无效


ruby 2.6.8p205 (2021-07-07 revision 67951) [universal.x86_64-darwin21]版本中,使用:binary => true无效,无法编译为framework。在D ebug模式下生效,在发布后就失效了。


def set_prebuild_for_pod(pod_name, should_prebuild)
Pod::UI.puts("pod_name: #{pod_name} prebuild:#{should_prebuild}")
if should_prebuild == true
@prebuild_framework_pod_names ||= []
@prebuild_framework_pod_names.push pod_name
else
@should_not_prebuild_framework_pod_names ||= []
@should_not_prebuild_framework_pod_names.push pod_name
end
end


ruby 2.6.8中,release模式下,执行了两次,参数还不一致,导致 @prebuild_framework_pod_names@should_not_prebuild_framework_pod_names相等,最终需要预编译的数组为[]


pod_name: SnapKit, should_prebuild:true
pod_name: SnapKit, should_prebuild:


2,pod update没有及时更新最新的framework


frameworkA依赖frameworkB,在 Podfile中,只引入了 frameworkA


target xxx do 
pod "frameworkA", :binary => true
end


frameworkB有新版本时,没有更新最新的frameworkB,对于我们自己的组件,我们期望有新版本发布时,能及时更新。


解决办法:
在检测更新的方法中,读取最新的Manifest.lock文件,读取已编译的frameworkplist文件,比较两个版本号是否一致,不一致则重新编译。


cocoapods-binary-bel


为了能充分利用该插件,我根据实际产生的问题,对源码进行了一些修改,增加了 plist版本校验。使用方式和cocoapods-binary一致。
源码github链接cocoapods-binary-bel


安装


sudo gem install cocoapods-binary-bel


流程图


cocoapods-binary的基础上增加了版本校验工程,总的流程图如下所示:


cocoapods-binary-bel.png


一键转源码


1,:binary => false,指定某一个framework为源码


2, 新增 --hsource选项,输入 pod install --hsource,可以将所有的framework转为源码。


去除了依赖分析


在实际运用的工程中,如果 A 依赖 BC, B又依赖D,如果 A需要预编译,那么 BCD都需要重新编译,实际上此时BCD已经有了已经编译好的版本,无需重新编译。在Podfile指明 BCD即可。


pod "A"
pod "B"
pod "C"
pod "D"


增加静态库的resource处理


在 cocoapods中,如果使用 resource_bundle 处理资源文件,会生成一个相对应的target来处理资源文件,如果是动态库,会在动态库Target,添加资源工程依赖,使用 xcodebuild命令制作二进制会将bundle文件编译至xxx.framework目录下,使用 resources同样也会将资源文件编译至xxx.framework目录下。


对于静态库而言,如果是使用resource_bundle,也同样生成一个会生成一个相对应的target来处理资源文件,但对资源文件的拷贝,是由主工程做的,无需静态库工程处理, 如果使用 resources,则需要将资源文件从源码中,拷贝到 xxx.framework下,在主工程编译时,由主工程处理即可。


作者:Bel李玉
链接:https://juejin.cn/post/7035628418972516360

收起阅读 »

iOS 简单封装一个新用户功能模块引导工具类小玩儿意

iOS
废话开篇:新手引导功能就是简单的告诉用户某一模块下能够进行什么样的操作,起到指引用户的作用,那么就简单的实现一下这样的功能模块。一、实现效果展示可以从效果图中看到,对新用户的必要模块都会进行简单的功能解释。二、调用代码添加待引导功能视图到管理类管理类进行展示三...
继续阅读 »

废话开篇:新手引导功能就是简单的告诉用户某一模块下能够进行什么样的操作,起到指引用户的作用,那么就简单的实现一下这样的功能模块。

一、实现效果展示

屏幕录制2021-12-08 上午11.04.57.gif

可以从效果图中看到,对新用户的必要模块都会进行简单的功能解释。

二、调用代码

添加待引导功能视图到管理类

image.png

管理类进行展示

image.png

三、工具类解析

image.png

1、KDSGuideMannager 类

(1)统一管理全局下需要进行 “引导” 的功能区域(UIView)的保存。

(2)控制引导界面的显示与消失。

(3)控制下一个功能区域(UIView)圈定及描述展示。

2、KDSGuideView 类

(1)整体的蒙板视图层。

(2)对当前所选引导功能区域(UIView)进行镂空标注

(3)调整气泡(KDSGuideBubbleView)位置。

3、KDSGuideBubbleView 类

(1)气泡标注视图。

4、KDSGuideModel 类

(1)保存功能区域(UIView)视图及功能描述文字

四、实现代码

1、KDSGuideMannager 类

KDSGuideMannager.h

image.png

KDSGuideMannager.m

image.png

image.png

image.png

2、KDSGuideView 类

KDSGuideView.h

image.png

KDSGuideView.m

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

3、KDSGuideBubbleView 类

KDSGuideBubbleView.h

image.png

KDSGuideBubbleView.m

image.png

image.png

4、KDSGuideModel 类

KDSGuideModel.h

image.png

KDSGuideModel.m

image.png

五、其他效果展示

屏幕录制2021-12-08 下午1.54.31.gif

个人总结,代码拙劣,大神勿笑。

收起阅读 »

SDWebImage从小白到大师蜕变

iOS
简介SDWebImage提供的简洁的获取远程URL图片的API;平时开发中使用最多场景就是列表中的cell中要显示远程图片的需求,在具体的实现中要避免加载图片造成的界面卡顿,列表卡顿等现象的出现;所以需要编码实现如下功能:使用占位图片显示UI界面,异步线程加载...
继续阅读 »

简介

SDWebImage提供的简洁的获取远程URL图片的API;平时开发中使用最多场景就是列表中的cell中要显示远程图片的需求,在具体的实现中要避免加载图片造成的界面卡顿,列表卡顿等现象的出现;所以需要编码实现如下功能:

  • 使用占位图片显示UI界面,异步线程加载图片成功后刷新控件
  • 缓存机制,下载过的图片做内存缓存和磁盘缓存
  • app内存吃紧的状态下移除缓存的内容

SDWebImage的框架结构

SDWebImage的框架结构

SDWebImage的图片下载分类,只要一行代码就可以实现图片异步下载和缓存功能。

功能简介

  1. 一个添加了web图片加载和缓存管理的UIImageView分类
  2. 一个异步图片下载器
  3. 一个异步的内存加磁盘综合存储图片并且自动处理过期图片
  4. 支持动态gif图
    • 4.0 之前的动图效果并不是太好
    • 4.0 以后基于 FLAnimatedImage加载动图
  5. 支持webP格式的图片
  6. 后台图片解压处理
  7. 确保同样的图片url不会下载多次
  8. 确保伪造的图片url不会重复尝试下载
  9. 确保主线程不会阻塞

实现原理

  1. 架构图(UML 类图)

架构图(UML 类图)

  1. 流程图(方法调用顺序图)

1559217862563-364c0d60-3f2a-4db9-b5c5-e81f01cd125e.png

目录结构

  • Downloader
    ○ SDWebImageDownloader
    ○ SDWebImageDownloaderOperation

  • Cache
    ○ SDImageCache

  • Utils
    ○ SDWebImageManager
    ○ SDWebImageDecoder
    ○ SDWebImagePrefetcher

  • Categories
    ○ UIView+WebCacheOperation
    ○ UIImageView+WebCache
    ○ UIImageView+HighlightedWebCache
    ○ UIButton+WebCache
    ○ MKAnnotationView+WebCache
    ○ NSData+ImageContentType
    ○ UIImage+GIF
    ○ UIImage+MultiFormat
    ○ UIImage+WebP

  • Other
    ○ SDWebImageOperation(协议)
    ○ SDWebImageCompat(宏定义、常量、通用函数)

相关类名与功能描述

SDWebImageDownloader:是专门用来下载图片和优化图片加载的,跟缓存没有关系

SDWebImageDownloaderOperation:继承于 NSOperation,用来处理下载任务的

SDImageCache:用来处理内存缓存和磁盘缓存(可选)的,其中磁盘缓存是异步进行的,因此不会阻塞主线程

SDWebImageManager:作为 UIImageView+WebCache 背后的默默付出者,主要功能是将图片下载(SDWebImageDownloader)和图片缓存(SDImageCache)两个独立的功能组合起来

SDWebImageDecoder:图片解码器,用于图片下载完成后进行解码

SDWebImagePrefetcher:预下载图片,方便后续使用,图片下载的优先级低,其内部由

SDWebImageManager :来处理图片下载和缓存

UIView+WebCacheOperation:用来记录图片加载的 operation,方便需要时取消和移除图片加载的 operation

UIImageView+WebCache:集成 SDWebImageManager 的图片下载和缓存功能到 UIImageView 的方法中,方便调用方的简单使用

UIImageView+HighlightedWebCache:跟 UIImageView+WebCache 类似,也是包装了 SDWebImageManager,只不过是用于加载 highlighted 状态的图片

UIButton+WebCache:跟 UIImageView+WebCache 类似,集成 SDWebImageManager 的图片下载和缓存功能到 UIButton 的方法中,方便调用方的简单使用

MKAnnotationView+WebCache:跟 UIImageView+WebCache 类似

NSData+ImageContentType:用于获取图片数据的格式(JPEG、PNG等)

UIImage+GIF:用于加载 GIF 动图

UIImage+MultiFormat:根据不同格式的二进制数据转成 UIImage 对象

UIImage+WebP用于解码并加载 WebP 图片

工作流程

工作流程

  • 入口 setImageWithURL:placeholderImage:options: 会先把 placeholderImage 显示,然后 SDWebImageManager 根据 URL 开始处理图片。

  • 进入 SDWebImageManager-downloadWithURL:delegate:options:userInfo:交给 SDImageCache 从缓存查找图片是否已经下载 queryDiskCacheForKey:delegate:userInfo:。

  • 先从内存图片缓存查找是否有图片,如果内存中已经有图片缓存,SDImageCacheDelegate 回调 imageCache:didFindImage:forKey:userInfo: 到 SDWebImageManager。

  • SDWebImageManagerDelegate 回调 webImageManager:didFinishWithImage: 到 UIImageView+WebCache 等前端展示图片。

  • 如果内存缓存中没有,生成 NSInvocationOperation 添加到队列开始从硬盘查找图片是否已经缓存。

  • 根据 URLKey 在硬盘缓存目录下尝试读取图片文件。这一步是在 NSOperation 进行的操作,所以回主线程进行结果回调 notifyDelegate:。

  • 如果从硬盘读取到了图片,将图片添加到内存缓存中(如果空闲内存过小,会先清空内存缓存)。SDImageCacheDelegate 回调 imageCache:didFindImage:forKey:userInfo:进而回调展示图片。

  • 如果从硬盘缓存目录读取不到图片,说明所有缓存都不存在该图片,需要下载图片,回调 imageCache:didNotFindImageForKey:userInfo:。

  • 共享或重新生成一个下载器 SDWebImageDownloader 开始下载图片。

  • 图片下载由 NSURLConnection(3.8.0之后使用了NSURLSession),实现相关 delegate 来判断图片下载中、下载完成和下载失败。

  • connection:didReceiveData: 中利用 ImageIO 做了按图片下载进度加载效果。connectionDidFinishLoading: 数据下载完成后交给 SDWebImageDecoder 做图片解码处理。

  • 图片解码处理在一个 NSOperationQueue 完成,不会拖慢主线程 UI。如果有需要对下载的图片进行二次处理,最好也在这里完成,效率会好很多。

  • 在主线程 notifyDelegateOnMainThreadWithInfo: 宣告解码完成,imageDecoder:didFinishDecodingImage:userInfo: 回调给 SDWebImageDownloader。

  • imageDownloader:didFinishWithImage: 回调给 SDWebImageManager 告知图片下载完成。

  • 通知所有的 downloadDelegates 下载完成,回调给需要的地方展示图片。

  • 将图片保存到 SDImageCache 中,内存缓存和硬盘缓存同时保存。写文件到硬盘也在以单独 NSInvocationOperation 完成,避免拖慢主线程。

  • SDImageCache 在初始化的时候会注册一些消息通知,在内存警告或退到后台的时候清理内存图片缓存,应用结束的时候清理过期图片。

  • SDWebImagePrefetcher 可以预先下载图片,方便后续使用。

常见面试题

  1. 图片文件缓存的时间有多长:1周

_maxCacheAge = kDefaultCacheMaxCacheAge

  1. SDWebImage 的内存缓存是用什么实现的?

NSCache

  1. SDWebImage 的最大并发数是多少?

maxConcurrentDownloads = 6

  • 是程序固定死了,可以通过属性进行调整!
  1. SDWebImage 支持动图吗?GIF
1. #import <ImageIO/ImageIO.h>
2. [UIImage animatedImageWithImages:images duration:duration];
复制代码
  1. SDWebImage是如何区分不同格式的图像的

    • 根据图像数据第一个字节来判断的!
    • PNG:压缩比没有JPG高,但是无损压缩,解压缩性能高,苹果推荐的图像格式!
    • JPG:压缩比最高的一种图片格式,有损压缩!最多使用的场景,照相机!解压缩的性能不好!
    • GIF:序列桢动图,特点:只支持256种颜色!最流行的时候在1998~1999,有专利的!

6.SDWebImage 缓存图片的名称是怎么确定的!

  • md5

  • 如果单纯使用 文件名保存,重名的几率很高!

  • 使用 MD5 的散列函数!对完整的 URL 进行 md5,结果是一个 32 个字符长度的字符串!

  1. SDWebImage 的内存警告是如何处理的!
    • 利用通知中心观察
    • - UIApplicationDidReceiveMemoryWarningNotification 接收到内存警告的通知
    • 执行 clearMemory 方法,清理内存缓存!
    • - UIApplicationWillTerminateNotification 接收到应用程序将要终止通知
    • 执行 cleanDisk 方法,清理磁盘缓存!
    • - UIApplicationDidEnterBackgroundNotification 接收到应用程序进入后台通知
    • 执行 backgroundCleanDisk 方法,后台清理磁盘!
    • 通过以上通知监听,能够保证缓存文件的大小始终在控制范围之内!
    • clearDisk 清空磁盘缓存,将所有缓存目录中的文件,全部删除!

实际工作,将缓存目录直接删除,再次创建一个同名空目录!

青山不改,绿水长流,后会有期,感谢每一位佳人的支持!

收起阅读 »

闲鱼正在悄悄放弃 Flutter 吗?

iOS
闲鱼技术阿里巴巴集团采访嘉宾 | 于佳(宗心)编辑 | Tina闲鱼在 2017 年引入 Flutter,当时的 Flutter 还远未成熟,行业内也没有把 Flutter 放入已有工程体系进行开发的先例。之后这支不到 15 人的闲鱼团队从工程架构、混合栈调用...
继续阅读 »

闲鱼技术lv-4阿里巴巴集团

采访嘉宾 | 于佳(宗心)

编辑 | Tina

闲鱼在 2017 年引入 Flutter,当时的 Flutter 还远未成熟,行业内也没有把 Flutter 放入已有工程体系进行开发的先例。

之后这支不到 15 人的闲鱼团队从工程架构、混合栈调用、打包构建、协同模式上都做了一些创新,保证了 Flutter 能融入到闲鱼已有的客户端工程体系内。在 2017 年到 2019 年期间,闲鱼也不断的修正 Bug 提高 Flutter 的稳定性并同步给 Google,并在实践中沉淀出一套自己的混合技术方案,开源了 Flutter Boost 引擎。

2019 年,闲鱼开始大规模落地,推进 Flutter 在闲鱼的应用。2020 年,闲鱼线上的主链路几乎已经完全拥抱 Flutter。这两年,Flutter 也逐渐在其他企业里落地,但同时也不断有质疑的声音发出。甚至有传言表示“闲鱼的新业务已经放弃 Flutter”、“相信闲鱼遇到了很大的难题”......

那么,作为 Flutter 先驱和探路者,闲鱼在过去几年的摸索过程中是否有走弯路?闲鱼现在到底面临着什么样的挑战?是否会放弃 Flutter?新业务选择了什么技术?对应的技术选型原则是什么?针对这些疑问,闲鱼技术团队客户端负责人于佳(宗心)逐一给了我们解答。

国内第一个引进 Flutter 的团队

InfoQ:闲鱼当时引进 Flutter 时主要是为了解决什么问题?

于佳(宗心):闲鱼在 17 年调研的时候,客户端团队只有不到 15 人,而闲鱼的业务场景可以称得上是一个 “小淘宝”,相对比较复杂。这种场景下我们首先需要解决的是多端人力共享的问题。多端人力带来的好处不只是可以一人开发双端,也代表着更好的研发资源调配灵活性(这意味着团队的 iOS:Android 的比例不再需要 1:1,而市面上 Android 的工程师基数远大于 iOS)。

另外我们希望这个技术是贴合移动端研发技术栈的,而非前端技术栈,本身对于 RN 和 Weex 来说,工具链和研发习惯还是有比较大的差异的。最后我们希望这个技术的体验可以做到接近原生,AOT 下的 Flutter 基本满足我们当时的要求,在实际测试过程中,同样未深度优化的详情页面,Flutter 在低端机的表现比 Native 更好。因此当时基于这三个条件选择了 Flutter。

2018 年的尝试投入过程中,整个基建和探索带来了一定的成本。2019 年,团队开始正式大量使用 Flutter 进行研发,目前整个团队 70% 的 commit 来自 Dart,可以说基本完成了我们当初的期望。在实际的研发过程中,基本可以完成一个需求一个客户端投入的目标。

InfoQ:很多人质疑 Dart 语言,认为这个语言独特小众,还存在比如说多层嵌套的问题,您们怎么看待新语言的应用?

于佳(宗心):语言是我们选择技术方案的其中一个因素,但是相对比较弱的因素。

我们会从几个角度去看:

  • 语言的背景,从我们的角度来看 Dart 是大厂研发的,也有比较久的历史。

  • 语言的学习成本,从语法糖和学习曲线上来看,Dart 成本都比较低,首先 Android 同学的上手率很快。另外熟悉 swift 的 iOS 同学,上手也很快。现代语言的特性有很多是相通的。这部分是它的优势。

  • 语言带来的其他优势,如编译产物支持 AOT 和 JIT,比较灵活。AOT 有明显的性能优势。

  • 语言的未来的趋势。Dart 在 2020 年第四季度 Github Pull Request 的排名已经到了全网第 13 位,超过了 Kotlin(15 位),Swift(16 位),Objective-C(18 位)。作为移动技术领域的新语言成长性还是非常不错的。

对于像多层嵌套的问题,可以通过进一步抽象一些控件类或方法解决,并不是特别大的问题。

InfoQ:闲鱼引入 Flutter 之后做了哪些关键创新?在使用 Flutter 上有哪些收益?

于佳(宗心):闲鱼在这部分创新非常多,并在内部申请了非常多专利。

  • 我们的开源项目 Flutter Boost 彻底改变了 Flutter 官方的一些 RoadMap。目前 Add2ExistApp 是行业最主流的研发方式。混合开发一方面帮助了业务更平滑的迁移到了新的技术栈,另一方面可以更好的利用已有的 Native 能力,大幅减少了重复开发的工作。

  • 针对音视频的外接纹理方案,也是目前行业大厂常见的解决方案,在外接纹理方案下,Native 和 Flutter 侧的缓存管理得到了统一,在性能上也有一定的提升。

  • Flutter APM,基于 Flutter 技术栈的性能稳定性数据采集和加工方案,目前在集团内部也是跟多个 BU 一起共建,为大的 AliFlutter 组织提供服务。

  • Flutter 相关的动态模版方案,Flutter DX,兼容集团的已有的 Native 模版,保证了业务的平滑迁移,并为 Flutter 提供了部分业务动态性。

  • 其他还有很多,包括内部的高性能长列表容器 PowerScrollView,动画框架 Fish-Lottie,游戏引擎 Candy,我们现在还有一些新的方向在沉淀,在基于 Flutter 的研发流程和研发工具上也有投入,未来大家如果感兴趣可以去 InfoQ 组织的行业大会与我们交流。

闲鱼有想过放弃 Flutter 吗?

InfoQ:最近一两年,您们在 Flutter 开发上,遇到的最大挑战是什么?跟最初使用 Flutter 时的挑战一样吗?

于佳(宗心):早先几年闲鱼作为整个行业的先驱,主要的挑战是整个技术生态太差,都需要自己做。另外就是前期引擎的稳定性有比较大的问题。

最近几年随着整个技术的深度使用,以及闲鱼这两年业务快速发展背后,越来越多的体验问题被大家提及,因此我们从去年开始进行了整个产品的大改版,同时客户端的目标就是全面优化,打造更好的用户端产品体验。

因此在生态逐渐完善后,我们的挑战是,怎么通过 Flutter 来实现更加精细化的用户体验。去年,这部分确实花了我们比较多的精力。基于这个命题,我们在内存和卡顿上内部也开发了较多的基于 Flutter 的检测工具,在内存优化和卡顿优化上也有一些比较具体的方法,但不得不说,所有的细节优化都是比较耗人力的,不管是 Native 还是 Flutter 都要投入相当的精力,所以我们目前也面向全行业进行客户端的招聘,希望有志在 Flutter 领域进行探索的同学联系我。

InfoQ:在混合研发体系下,闲鱼还进行了引擎定制,那么官方提供的方案主要问题是什么?对于一般小企业来说,混合开发复杂度会不会太高?

于佳(宗心):闲鱼在前期有不少修改引擎的动作,我针对当时有一些 自己的反思,一方面是确实因为 Flutter 不太完善,另一方面在 18 年左右,我们自己引擎的理解也不够深刻,很多时候可以通过更上层的方案解决,这也间接导致了我们的很多引擎定制修改难以合入主干。

所以这部分我想说的是,目前官方的方案可以解决 90% 的问题,如果一定要说定制,目前在性能侧还是有一些问题的。比如闲鱼目前首页还是 native 没有使用 Flutter,就是因为替换以后启动加载体验不佳,另外在长列表侧大家一直诟病的卡顿问题,我们有尝试通过上层框架解决了一部分,接下来可能还需要底层引擎帮忙优化。另外一些包括双端字体不一致的问题,还有输入框体验不一致的问题,都需要官方进行长期的优化。

目前我们主要还是希望跟随主干分支,尽量不修改 Flutter 的代码,闲鱼团队会储备一些引擎侧的专家,同时也会依靠集团 AliFlutter 的生态做事情。在整个 AliFlutter 的组织里不同的 BU 擅长的也不同,如 UC 同学更擅长引擎定制,闲鱼团队有大量的上层应用框架,淘宝团队提供基于构建相关的基础设施。这样在大型公司中通过内部开源社区的方式就可以解决大部分的问题,放心开发了。

对于中小企业来说,要明确下大家面临的场景,如果前期快速迭代跑起来,对细节问题可以有一部分妥协,选择 Flutter 是一个比较明确的路径。今天大家所处的环境比闲鱼当年所处的环境要完善的多。推荐使用 Flutter Boost 进行混合开发,在部分场景下遇到问题无法快速响应时,也可以通过混合工程使用 native 进行兜底。复杂度方面,单纯引入混合栈能力,整体复杂度一般。

InfoQ:有传言,闲鱼有新业务没采用 Flutter,这给很多人造成了闲鱼放弃 Flutter 的观念,那么您们在新业务的技术选型上,考虑了哪些因素?

于佳(宗心):作为技术决策者,是应该避免自己被某一个技术绑架而在落地过程中产生谬误的。Flutter 和其他技术一样,最终是为了帮助团队实现业务价值,同时它也只是移动端的一种技术,捧杀和谩骂都是不合适的。这也是我特别不想在公众面前回应这个事情的原因,因为 技术本身要看适用场景。

从目前闲鱼的人员规模和业务规模来看。对于架构设计,我的理念是尽量追求一致性和架构的简洁。

整个客户端组织未来从语言的方向来看是 Dart First,尽量减少双端的研发投入。而对其他容器的选择,主要以 H5 为主,在未来的路径上尽量减少其他容器的接入,让前端开发也回归到标准路线来。

这里有两个好处:

  1. 组织成本最低,组织成本包括了同学们的学习成本、协同成本等等,多技术栈和容器多会带来额外的成本,这是我不愿意看到的。

  2. 架构的一致性对研发效能和质量都有帮助。举个例子,随着业务复杂性加大,多容器带来的内存飙升和包大小的问题是非常致命的,而且几乎是无解的,这就需要架构师作出决策,干掉什么留下什么。回到研发效能上,配套的工具,流程一定是围绕一类容器和语言来扩展的,如果方案特别多,每个方向都需要做额外的配套设施,成本收益很低,研发的幸福感也很低。

从这个设计的角度出发,我们会有几个明确的选择

  • 在默认场景下使用 Flutter 作为首选的方案;

  • 在投放活动、前台导购、非常不确定的新业务、以及管理后台等使用 H5 作为首选实现方案;

  • 在极少场景下,比如已有完整的 SDK 附带 UI 的支持如直播,以及未来中台的拍摄功能 SDK 也是自带 UI 的部分,如要切换,Native 成本最低,选择 Native。另外目前 Flutter 在首页加载还有一定的性能问题,因此还在使用 Native。从长远发展来看,未来到一定程度可能随改版直接改为 Flutter。

关于未来发展

InfoQ:使用 Flutter 多年后,现在回过头去看,您认为哪些公司哪些场景适合 Flutter?

于佳(宗心):目前看起来有几个典型场景比较适合:

  • 中台战略下的小前台产品,从大公司的组织里看阿里、头条、美团都有相对完善的 Flutter 组织或内部技术社区可以提供一些基础服务,保证了基于 Flutter 基础设施在前期投入过程中的成本均摊,在未来落地过程中,业务团队可以更加专注于业务研发,而更少的担心过程中填坑的成本。

  • 中小型企业的初创 App,在人力成本资源都不够的情况下,希望先跑通流程上线验证的团队,可以尝试使用 Flutter 进行研发,在我自己实际的面试和行业交流过程中,这一类情况也比较典型。这种方式可以避免前期成本过度投入,在人员调配上也更加灵活。

  • 另外这个观点还没有验证,但是逻辑上应该可行。未来面向企业内部流程工具,政府部门的部分工具属性较强的 App,可以尝试使用 Flutter。因为目前我了解的情况来看,在企业这边的应用来看,整体 ToB(美团商家端)和 ToD(比如饿了么骑手端)的场景的 App 特别多。横向比较来看,场景比较类似,也就是说更多中长尾应用有可能是 Flutter 技术的主要场景。

InfoQ:您认为未来 Flutter 急需改善的地方是什么?

于佳(宗心):从 Flutter 2.0 发布后我跟一些一线开发者交流的感受来看,Flutter 还是需要推进跨端性能和细节体验的优化。去年一年在大的战略方向上(跨终端),Flutter 做的不错,在 PC 和 Web 侧都有建树,跟车企以及操作系统厂商合作都有一定进展。但回归到产品体验和开发者体验上,还有不少路要走,很多时候对于一个严苛的业务方来说,小到字体和控件的体验都会成为最后不选择这门技术的原因。这部分希望整个开源社区在新的一年能有一些进步。我们 AliFlutter 组织内部,以 UC 内核团队为首的同学们,在这方面就有非常多的沉淀以及 PR,在内部引擎制定上有很多体验的提升。未来在 AliFlutter 组织内,我们也会除了完善整个公司的基建外,进一步关注细节体验,沉淀一些最佳实践给到其他的开发同学。大家会在2个月内看到我们最新出版的书籍,欢迎交流。

InfoQ:Flutter2.0 来了,那么 Flutter 会成为主流选择吗?

于佳(宗心):可以讲一下我对 Flutter 未来的判断。一方面在未来操作系统有可能走向分裂,多终端的场景下,Flutter 会有比较不错的发展,跨平台本身的对企业来说在成本侧是有很大的诉求的,尤其是互联网公司。但是从历史的经验来看,Flutter 只是渲染引擎,即使今天的游戏开发,在游戏引擎和配套工具完善的情况下,有部分的功能模块(比如社区 / 直播的功能)依然还是混合的框架,所以混合开发最后一定是一直存在的。能不能成为未来整个移动研发的主流这件事情上看,我无法给出答案,但可以肯定的是,在生态更加完善后,会在一定的历史阶段成为客户端研发的另一种常见的技术选择。

嘉宾介绍:

于佳,花名 宗心,闲鱼技术团队客户端负责人。2012 年应届毕业加入阿里巴巴,经历集团无线化转型的重要时期,参与过集团多款重量级 App 以及移动中间件的设计与开发,多年客户端老兵。2014 年参与了手机淘宝的 iOS 客户端的架构升级,该架构首次完成了对百人团队并行开发的支持,同年主导了手机天猫客户端基础架构以及交易链路向手淘架构的归一,为手机淘宝作为未来集团无线中台奠定了坚实的基础。2015 年加入闲鱼客户端团队负责端架构和团队建设,工作期间完成了基于 Flutter 混合架构的闲鱼客户端的整体架构设计,在工程体系上完善了针对 Flutter 的持续集成以及高可用体系的支撑,同时推进了闲鱼主链路业务的 Flutter 化。未来将持续关注终端技术的演变及发展趋势。

收起阅读 »

如何用 GPU硬件层加速优化Android系统的游戏流畅度

作为一款VR实时操作游戏App,我们需要根据重力感应系统,实时监控手机的角度,并渲染出相应位置的VR图像,因此在不同 Android 设备之间,由于使用的芯片组和不同架构的GPU,游戏性能会因此受到影响。举例来说:游戏在 Galaxy S20+ 上可能以 6...
继续阅读 »

作为一款VR实时操作游戏App,我们需要根据重力感应系统,实时监控手机的角度,并渲染出相应位置的VR图像,因此在不同 Android 设备之间,由于使用的芯片组和不同架构的GPU,游戏性能会因此受到影响。举例来说:游戏在 Galaxy S20+ 上可能以 60fps 的速度渲染,但它在HUAWEI P50 Pro上的表现可能与前者大相径庭。 由于新版本的手机具有良好的配置,而游戏需要考虑基于底层硬件的运行情况。

如果玩家遇到帧速率下降或加载时间变慢,他们很快就会对游戏失去兴趣。
如果游戏耗尽电池电量或设备过热,我们也会流失处于长途旅行中的游戏玩家。
如果提前预渲染不必要的游戏素材,会大大增加游戏的启动时间,导致玩家失去耐心。
如果帧率和手机不能适配,在运行时会由于手机自我保护机制造成闪退,带来极差的游戏体验。

基于此,我们需要对代码进行优化以适配市场上不同手机的不同帧率运行。

所遇到的挑战

首先我们使用Streamline获取在 Android 设备上运行的游戏的配置文件,在运行测试场景时将 CPU 和 GPU性能计数器活动可视化,以准确了解设备处理 CPU 和 GPU 工作负载,从而去定位帧速率下降的主要问题。

以下的帧率分析图表显示了应用程序如何随时间运行。

在下面的图中,我们可以看到执行引擎周期与 FPS 下降之间的相关性。显然GPU 正忙于算术运算,并且着色器可能过于复杂。

为了测试在不同设备中的帧率情况,使用友盟+U-APM测试不同机型上的卡顿状况,发现在onSurfaceCreated函数中进行渲染时出现卡顿, 应证了前文的分析,可以确定GPU是在算数运算过程中发生了卡顿:

因为不同设备有不同的性能预期,所以需要为每个设备设置自己的性能预算。例如,已知设备中 GPU 的最高频率,并且提供目标帧速率,则可以计算每帧 GPU 成本的绝对限制。

数学公式: $ 每帧 GPU 成本 = GPU 最高频率 / 目标帧率 $

CPU 到 GPU 的调度存在一定的约束,由于调度上存在限制所以我们无法达到目标帧率。

另外,由于 CPU-GPU 接口上的工作负载序列化,渲染过程是异步进行的。
CPU 将新的渲染工作放入队列,稍后由 GPU 处理。

数据资源问题

CPU 控制渲染过程并且实时提供最新的数据,例如每一帧的变换和灯光位置。然而,GPU 处理是异步的。这意味着数据资源会被排队的命令引用,并在命令流中停留一段时间。而程序中的OpenGL ES 需要渲染以反映进行绘制调用时资源的状态,因此在引用它们的 GPU 工作负载完成之前无法修改资源。

调试过程

我们曾做出尝试,对引用资源进行代码上的编辑优化,然而当我们尝试修改这部分内容时,会触发该部分的新副本的创建。这将能够一定程度上实现我们的目标,但是会产生大量的 CPU 开销。

于是我们使用Streamline查明高 CPU 负载的实例。在图形驱动程序内部libGLES_Mali.so路径函数, 视图中看到极高的占用时间。

由于我们希望在不同手机上适配不同帧率运行,所以需要查明libGLES_Mali.so是否在不同机型的设备上都产生了极高的占用时间,此处采用了友盟+U-APM来检测用户在不同机型上的函数占用比例。

友盟+ U-APM自定义异常测试,下列机型会产生高libGLES_Mali.so占用的问题,因此我们需要基于底层硬件的运行情况来解决流畅性问题,同时由于存在问题的机型不止一种,我们需要从内存层面着手,考虑如何调用较少的内存缓存区并及时释放内存。

解决方案及优化

基于前文的分析,我们首先尝试从缓冲区入手进行优化。

单缓冲区方案
• 使用glMapBufferRange和GL_MAP_UNSYNCHRONIZED.然后使用单个缓冲区内的子区域构建旋转。这避免了对多个缓冲区的需求,但是这一方案仍然存在一些问题,我们仍需要处理管理子区域依赖项,这一部分的代码给我们带来了额外的工作量。

多缓冲区方案
• 我们尝试在系统中创建多个缓冲区,并以循环方式使用缓冲区。通过计算我们得到了适合的缓冲区的数目,在之后的帧中,代码可以去重新使用这些循环缓冲区。由于我们使用了大量的循环缓冲区,那么大量的日志记录和数据库写入是非常有必要的。但是有几个因素会导致此处的性能不佳:

1. 产生了额外的内存使用和GC压力
2. Android 操作系统实际上是将日志消息写入日志而并非文件,这需要额外的时间。
3. 如果只有一次调用,那么这里的性能消耗微乎其微。但是由于使用了循环缓冲区,所以这里需要用到多次调用。
我们会在基于c#中的 Mono 分析器中启用内存分配跟踪函数用于定位问题:

$ adb shell setprop debug.mono.profile log:calls,alloc

我们可以看到该方法在每次调用时都花费时间:

Method call summary Total(ms) Self(ms) Calls Method name 782 5 100 MyApp.MainActivity:Log (string,object[]) 775 3 100 Android.Util.Log:Debug (string,string,object[]) 634 10 100 Android.Util.Log:Debug (string,string)

在这里定位到我们的日志记录花费了大量时间,我们的下一步方向可能需要改进单个调用,或者寻求全新的解决方案。

log:alloc还让我们看到内存分配;日志调用直接导致了大量的不合理内存分配:

Allocation summary Bytes Count Average Type name 41784 839 49 System.String 4280 144 29 System.Object[]

硬件加速

最后尝试引入硬件加速,获得了一个新的绘图模型来将应用程序渲染到屏幕上。它引入了DisplayList 结构并且记录视图的绘图命令以加快渲染速度。

同时,可以将 View渲染到屏幕外缓冲区并随心所欲地修改它而不用担心被引用的问题。此功能主要适用于动画,非常适合解决我们的帧率问题,可以更快地为复杂的视图设置动画。

如果没有图层,在更改动画属性后,动画视图将使其无效。对于复杂的视图,这种失效会传播到所有的子视图,它们反过来会重绘自己。

在使用由硬件支持的视图层后,GPU 会为视图创建纹理。因此我们可以在我们的屏幕上为复杂的视图设置动画,并且使动画更加流畅。

代码示例:

// Using the Object animator view.setLayerType(View.LAYER_TYPE_HARDWARE, null); ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(view, View.TRANSLATION_X, 20f); objectAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { view.setLayerType(View.LAYER_TYPE_NONE, null); } }); objectAnimator.start(); // Using the Property animator view.animate().translationX(20f).withLayer().start();

另外还有几点在使用硬件层中仍需注意:

(1)在使用之后进行清理:

硬件层会占用GPU上的空间。在上面的 ObjectAnimator代码中,侦听器会在动画结束时移除图层。在 Property animator 示例中,withLayers()方法会在开始时自动创建图层并在动画结束时将其删除。

(2)需要将硬件层更新可视化:

使用开发人员选项,可以启用“显示硬件层更新”。
如果在应用硬件层后更改视图,它将使硬件层无效并将视图重新渲染到该屏幕外缓冲区。

硬件加速优化

但是由此带来了一个问题是,在不需要快速渲染的界面,比如滚动栏, 硬件层也会更快地渲染它们。当将 ViewPager 滚动到两侧时,它的页面在整个滚动阶段会以绿色突出显示。

因此当我滚动ViewPager时,我使用DDMS运行 TraceView
,按名称对方法调用进行排序,搜索“android/view/View.setLayerType”,然后跟踪它的引用:

ViewPager#enableLayers(): private void enableLayers(boolean enable) { final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final int layerType = enable ? ViewCompat.LAYER_TYPE_HARDWARE : ViewCompat.LAYER_TYPE_NONE; ViewCompat.setLayerType(getChildAt(i), layerType, null); } }

该方法负责为 ViewPager的孩子启用/禁用硬件层。它从 ViewPaper#setScrollState() 调用一次:

private void setScrollState(int newState) { if (mScrollState == newState) { return; } mScrollState = newState; if (mPageTransformer != null) { enableLayers(newState != SCROLL_STATE_IDLE); } if (mOnPageChangeListener != null) { mOnPageChangeListener.onPageScrollStateChanged(newState); } }

正如代码中所示,当滚动状态为IDLE时硬件被禁用,否则在DRAGGINGSETTLING时启用。PageTransformer 旨在“使用动画属性将自定义转换应用于页面视图”(Source)。

基于我们的需求,只在渲染动画的时候启用硬件层,所以我想覆盖ViewPager 方法,但由于它们是私有的,我们无法修改这个方法。

所以我采取了另外的解决方案:在 ViewPage#setScrollState() 上,在调用 enableLayers()之后,我们还会调用

OnPageChangeListener#onPageScrollStateChanged()

。所以我设置了一个监听器,当 ViewPager的滚动状态不同于 IDLE时,它将所有ViewPager的孩子的图层类型重置为 NONE

@Override public void onPageScrollStateChanged(int scrollState) { // A small hack to remove the HW layer that the viewpager add to each page when scrolling. if (scrollState != ViewPager.SCROLL_STATE_IDLE) { final int childCount = <your_viewpager>.getChildCount(); for (int i = 0; i < childCount; i++) <your_viewpager>.getChildAt(i).setLayerType(View.LAYER_TYPE_NONE, null); } }

这样,在ViewPager#setScrollState()为页面设置了一个硬件层之后——我将它们重新设置为NONE,这将禁用硬件层,因此而导致的帧率区别主要显示在Nexus上。

作者:六一
来源:https://segmentfault.com/a/1190000040864118

收起阅读 »

黄仁勋要造“第二颗地球”?对“元宇宙”意味着什么

随着元宇宙概念的爆火,英伟达正凭借其Omniverse平台及在AI、高性能计算等方面的建树,迅速成长为市值高达7700亿美元的AI顶级玩家。作为科技圈极具个性的大佬“黄教主”——NVIDIA创始人兼CEO黄仁勋,每年都会在行业科技展会上拿出一些有趣的东西。在刚...
继续阅读 »


随着元宇宙概念的爆火,英伟达正凭借其Omniverse平台及在AI、高性能计算等方面的建树,迅速成长为市值高达7700亿美元的AI顶级玩家。

作为科技圈极具个性的大佬“黄教主”——NVIDIA创始人兼CEO黄仁勋,每年都会在行业科技展会上拿出一些有趣的东西。

在刚刚结束的 NVIDIA GTC 2021 大会上,黄仁勋惊喜亮相,表达了对今年爆火的元宇宙的痴迷,更宣布要造“第二颗地球”!

一时间,引发“全网”热议。

“元宇宙”格局打开:Omniverse Avatar

此次大会上,英伟达推出了全球最小、功能强大、能效最高的新一代AI超级计算机NVIDIA Jetson AGX Orin、NVIDIA Triton推理服务器及NVIDIA A2 Tensor Core GPU加速器、NeMo Megatron、NVIDIA Modulus及新自动驾驶平台DRIVE Hyperion 8 GA等一系列重磅新技术。

同时,英伟达还推出了3个新加速库:一、针对运筹优化问题的加速求解器——NVIDIA ReOpt,可实现实时路线规划优化。二、cuQuantum DGX设备,配备有针对量子计算工作流的加速库,可用态矢量和张量网络的方法来加速量子电路模拟。三、在PyData和NumPy生态系统的大规模加速计算cuNumeric,属于NVIDIA RAPIDS开源Python数据科学套件。

除此之外,会上黄仁勋还带来了承载着其“元宇宙”愿景的全新虚拟化身平台——Omniverse Avatar,彻底将元宇宙格局打开。

作为NVIDIA一系列先进AI技术的集大成者,Omniverse Avatar可以将Metropolis的感知能力、Riva的语音识别能力、Merlin的推荐能力、Omniverse的动画渲染能力等交汇于一体。

Omniverse Avatar能帮助开发者能构建出一个完全交互式的虚拟化身,它足够生动,能对语音和面部提示做出反应,能理解多种语言,能给出智能的建议。

通过Omniverse Avatar平台,用户可以为视频会议和协作平台、客户支持平台、内容创建、应用收益和数字孪生、机器人应用等等构建定制的AI助理。

英伟达的“元宇宙”探索之路

对于英伟达来说,早在2020年10月份,该公司就10月推出了面向企业的实时仿真和协作平台Omniverse的测试版,当时就吸引了包括宝马、爱立信、沃尔沃、Adobe、Epic Games在内的众多公司与之合作。

而今年4月份, Omniverse 的正式版一经推出后,便被称为“工程师的元宇宙”的虚拟工作平台。

黄仁勋评价称,“Omniverse可以让个人模拟制造出遵从物理规律的共享3D虚拟世界。”

自此,英伟达彻底掀起了国内“元宇宙”的浪潮。

而据英伟达相关负责人透露,英伟达已经为此花费了数年时间和数亿美元。

“人类需要‘在为时已晚之前采取行动缓解和适应’”。本次GTC大会上,黄仁勋谈到了许多关于未来的畅想:英伟达未来的规划,将主要着力于生物合成、气候预测等与人类未来息息相关的方面。

”使用物理原理以及源自原理型模型和观测结果的数据 Physics-ML 模型,经过优化在多 GPU 和多节点上进行训练,以超实时方式预测气候变化、创造地球的数字孪生模型或者将其数据传输到元宇宙中。”

最后,黄仁勋正式宣布了“Earth-two第二颗地球”即将到来。

据悉,英伟达上一台超级计算机名为 Cambridge-1,即 C-1,接下来英伟达将着手研发新的超级计算机“E-2”,即“Earth-two”第二颗地球。

黄仁勋强调:“我们目前所发明的所有技术均是实现元宇宙所必不可少的,我想象不出比这更宏伟更重要的用途。”

元宇宙火爆背后也需要警醒

其实除了英伟达,对于元宇宙的突然爆火,今年以来已经有不少业内不少科技公司都纷纷跟进布局,乘此风口发力突围。

比如Facebook(现更名为Meta)早在2019年就同意收购了专门从事云游戏开发的PlayGiga;今年早些时候,微软(Microsoft Corp.)以75亿美元收购电子游戏公司ZeniMax Media Inc.等等,再比如从过去到现在一直在对元宇宙进行探索的英伟达,这些都让VR/AR及相关衍生产业热度大涨。

当然,也并不是所有科技公司都认同元宇宙这个概念。也有内人士评论称,“目前构成“元宇宙”概念的技术力量薄弱,商品化程度低,处于发展的初级阶段”。

Facebook (现更名为Meta)的老对手Snap Inc.,就对此有不同看法,尽管该公司目前也在投资增强现实技术。

据《华尔街日报》报道,此前Snap Inc.公司首席执行官埃文·斯皮格尔(Evan Spiegel)在Tech Live大会上曾表示,“我们之所以对增强现实如此感兴趣,因为它的立足点是我们共享的这个世界。”

所以,看到这里,我们也会思考,如果能够让这个世界变的更好,那么这些技术这个发展趋势某些方面会值得人们追捧。但同时,我们也要时刻保持警醒,避免被“元宇宙”概念中所描绘的巨无霸虚拟世界所“迷惑而上瘾”。

作者:MissD
来源:https://segmentfault.com/a/1190000040937338


收起阅读 »

关于组件文档从编写到生成的那些事

前言说到前端领域的组件,Vue 技术体系下有 Element UI,React 技术体系下有 Ant Design,这些都是当前的前端攻城狮们都免不了要实际使用到的基础组件库。而在实际工作中,我们也总免不了要根据自己的工作内容,整理一些适合自己业务风格的一套组...
继续阅读 »



前言

说到前端领域的组件,Vue 技术体系下有 Element UI,React 技术体系下有 Ant Design,这些都是当前的前端攻城狮们都免不了要实际使用到的基础组件库。而在实际工作中,我们也总免不了要根据自己的工作内容,整理一些适合自己业务风格的一套组件库,基础组件部分可以基于上面开源的组件库以及 less 框架等多主题样式方案做自己的定制,但更多的是一些基于这些基础组件整理出适合自己业务产品的一套业务组件库。

而说到开发组件库,我们或选择 Monorepo 单仓库多包的形式(参考网文 https://segmentfault.com/a/11... 等)或其他 Git 多仓库单包的形式来维护组件代码,最终都免不了要将组件真正落到一个文档中,提供给其他同事去参考使用。

本篇文章就产出组件文档这件事,聊聊我在产出文档过程中的一系列思考过程,解决组件开发这「最后一公里」中的体验问题。

组件文档的编写

规范与搭建的调研

组件文档是要有一定的规范的,规范是任何软件工程阶段的第一步。对于组件来说,文档透出的内容都应包含哪些内容,决定着组件开发者和使用者双方的所有的体验。确定这些规范并不难,参考开源的组件库的文档,我们会有一些初步的印象。

因为我们团队使用的是 React 技术栈,这里我们参考 Ant Design 组件库。

比如这个最基本的 Button 组件,官方文档从上至下的内容结构是这样:

  1. 显示组件标题,下跟组件的简单描述。

  2. 列出组件的使用场景。

  3. 不同使用场景下的效果演示、代码案例。可外跳 CodeSandbox、CodePen 等在线源码编辑器网站编辑实时查看效果。

  4. 列出组件可配置的属性、接口方法列表。列表中包含属性/方法名、字段类型、默认值、使用描述等。

  5. 常见问题的 FAQ。

  6. 面向设计师的一些 Case 说明链接。

这些文档内容很丰富,作为一个开放的组件库,几乎考虑到的从设计到开发视角的方方面面,使用体验特别好。而在好奇心驱使下,我去查看了官网源码方库,比如 Button 组件:https://github.com/ant-design...。在源码库下,放置了和组件入口文件同名的分别以 .zh-CN.md.en-US.md 后缀命名的 Markdown 文件,而在这些 Markdown 文件中,便是我们看到的官网文档内容...咦?不对,好像缺少了什么,案例演示和示例代码呢?

难道 AntD 官网文档是另外自己手动开发维护的?这么大的一个项目肯定不至于,根据其官网类似 docs/react/introduce-cn 这种访问路径在源码库中有对应的 Markdown 文件来看,官网的文档肯定是官方仓库根据一种规范来生成的。那么是怎么生成的呢?第一次做组件文档规范的我被挑起了兴趣。

而作为一个前端工程老手,我很熟练地打开了其 package.json 文件,通过查看其中的 scripts 命令,轻易便发现了其下的 site 命令(源码仓库 package.json):

npm run site:theme && cross-env NODE_ICU_DATA=node_modules/full-icu ESBUILD=1 bisheng build --ssr -c ./site/bisheng.config.js

原来如此,网站的构建使用了 bisheng。通过查阅了解 bisheng 这个工具库,发现它确实是一个文档系统的自动生成工具,其下有一个插件 bisheng-plugin-react,可以将 Markdown 文档中的 JSX 源码块转换成可以运行演示的示例。而每个组件自身的示例代码文档,则在每个组件路径下的
demo 目录下维护。

Emmm,bisheng 确实是很好很强大,还能支持多语言,结合一定的文档规范约束下,能够快速搭建一个文档的主站。但在深入了解 bisheng 的过程中,发现其文档相对来说比较缺乏,包装的东西又那么多,使用过程中黑盒感严重,而我们团队的组件库其实要求很简单,一是能做到方便流通,而是只在内部流通使用,不会开源。那么,有没有更简单的搭建文档的方式呢?

更多的文档工具库的调研

在谷歌搜索中敲入如 React Components Documentation 等关键字,我们很快便能搜索出很多与 React 组件文档相关的工具库,这里我看到了如下这些:DoczStoryBookReact Styleguidist 、UMI 构建体系下的 dumi 等等。

这些工具库都支持解析 Markdown 文档,其中 DoczStoryBook 还支持使用 mdx 格式(Markdown 和 JSX 的混合写法),且在文档内容格式都能支持到组件属性列表、示例代码演示等功能。

接下来,我们分别简单看下这些工具库对于组件文档的支持情况。

Docz

在了解过程中,发现 Docz 其实是一个比较老牌的文档系统搭建工具了。它本身便主推 MDX 格式的文档,基本不需要什么配置便能跑起来。支持本地调试和构建生成可发布产物,支持多包仓库、TypeScript 环境、CSS 处理器、插件机制等,完全满足功能需要。

只是 Docz 貌似只支持 React 组件(当然对于我们来说够用),且看其 NPM 包最近更新已经是两年之前。另外 MDX 格式的文档虽然理解成本很少但对于使用不多的同事来说还是有一定的接受和熟练上手的成本。暂时备选。

StoryBook

在初次了解到 StoryBook 时便被其 66.7K 的 Star 量惊到了(Docz 是 22K),相对 Docz 来说,StoryBook 相关的社区内容非常丰富,它不依赖组件的技术栈体系,现在已经支持 React、Vue、Angular、Web Components 等数十个技术栈。

StoryBook 搭建文档系统的方式不是去自动解析 Markdown 文件,而是暴露一系列搭建文档的接口,让开发者自己为组件手动编写一个个的 stories 文件,StoryBook 会自动解析这些 stories 文件来生成文档内容。这种方式会带来一定的学习和理解接口的成本,但同时也基于这种方式实现了支持跨组件技术栈的效果,并让社区显得更为丰富。

官方示例:https://github.com/storybookj...

StoryBook 的强大毋庸置疑,但对于我们团队的情况来说还是有些杀鸡用牛刀了。另外,其需要额外理解接口功能并编写组件的 stories 文件在团队内很难推动起来:大家都很忙,组件开发分布在团队几十号人,情况比较复杂,将文档整理约束到一个人身上又不现实。继续调研。

React Styleguidist

React Styleguidist 的 Star 量没有 StoryBook 那么耀眼(10K+),但包体的下载量也比较大,且近期的提交也是相当活跃。由名字可知,它支持的是 React 组件的环境。它是通过自动解析 Mardown 文件的形式来生成文档的,实现方式是自动解析文档中 JSX 声明代码块,按照名称一一对应的规则查找到组件源码,然后将声明的代码块通过 Webpack 打包产生出对应的演示示例。

而在继续试用了 React Styleguidist 的一些基础案例后,它的一个功能让我眼前一亮:它会自动解析组件的属性,并解析出其类型、默认值、注释描述等内容,然后将解析到的内容自动生成属性表格放置在演示示例的上方。这就有点 JSDOC 的意思了,对于一个组件开发者来说,TA 确实需要关心组件属性的透出、注释以及文档案例的编写,但编写完也就够了,不用去考虑怎么适应搭建出一个文档系统。

另外, React Styleguidist 解析组件属性是基于解析 AST 以及配合工具 react-docgen 来实现的,并且还支持配合 react-docgen-typescript 来实现解析 TypeScript 环境下的组件,另外还能很多配置项支持更改文档站点相关的各个部分的展示样式、内容格式等,配置自定义支持相当灵活。

当然,它也有一些缺点,比如内嵌 Webpack,对于已经将编译组件库的构建工具换为 Rollup.js 的情况是一个额外的配置负担。

总的来说,React Styleguidist 在我看来是一个小而美的工具库,很适合我们团队协作参与人多、且大都日常开发工作繁重的情况。暂时备选。

dumi

了解到 dumi 是因为我们团队内已经有部分组件文档站点是基于它来搭建的了。dumi 一样是通过自动解析 Markdown 文档的方式来实现搭建文档系统,同样基本零配置,也有很多灵活的配置支持更改文档站点一些部分的显示内容、(主题)样式等,整体秉承了 UMI 体系的风格:开箱即用,封装极好。它能单独使用,也能结合 UMI 框架一起配置使用。

只是相比于上面已经了解到的 React Styleguidist 来说,并未看到有其他明显的优势,且貌似没有看到有自动解析组件属性部分的功能,对于我来说没有 React Styleguidist 下得一些亮点。可以参考,不再考虑。

组件文档的生成

在多方对比了多个文档搭建的工具库后,我最终还是选用了 React Styleguidist。在我看来,自然是其基于 react-docgen 来实现解析组件属性、类型、注释描述等的功能吸引到了我,这个功能一方面能在较少的额外时间付出下规范团队同事开发组件过程中一系列规范,另一方面其 API 接口的接入形式能够通过统一构建配置而统一产出文档内容格式和样式,方便各业务接入使用。

决定了技术方案后,便是如何具体实现基于其封装一个工具,便于各业务仓库接入了。

我们团队有自己统一的 CLI 构建工具,再多一个 React Styleguidist 的 CLI 配置会在理解上有一定的熟悉成本,但我可以基于 React Styleguidist 的 Node API 接入形式,将 React Styleguidist 的功能分别融入我们自身 CLI 的 devbuild 命令。

首先,基于 React Styleguidist API 的形式,统一一套配置,将生成 React Styleguidist 示例的代码抽象出来:

// 定义一套统一的配置,生成 react-styleguidist 实例
import styleguidist from 'react-styleguidist/lib/scripts/index.esm';
import * as docgen from 'react-docgen';
import * as docgenTS from 'react-docgen-typescript';

import type * as RDocgen from 'react-docgen';

export type DocStyleguideOptions = {
 cwd?: string;
 rootDir: string;
 workDir: string;
 customConfig?: object;
};

const DOC_STYLEGUIDE_DEFAULTS = {
 cwd: process.cwd(),
 rootDir: process.cwd(),
 workDir: process.cwd(),
 customConfig: {},
};

export const createDocStyleguide = (
 env: 'development' | 'production',
 options: DocStyleguideOptions = DOC_STYLEGUIDE_DEFAULTS,
) => {
 // 0. 处理配置项
 const opts = { ...DOC_STYLEGUIDE_DEFAULTS, ...options };
 const {
   cwd: cwdPath = DOC_STYLEGUIDE_DEFAULTS.cwd,
   rootDir,
   workDir,
   customConfig,
} = opts;

 // 标记:是否正在调试所有包
 let isDevAllPackages = true;

 // 解析工程根目录包信息
 const pkgRootJson = Utils.parsePackageSync(rootDir);

 // 1. 解析指定要调试的包下的组件
 let componentsPattern: (() => string[]) | string | string[] = [];
 if (path.relative(rootDir, workDir).length <= 0) {
   // 选择调试所有包时,则读取根路径下 packages 字段定义的所有包下的组件
   const { packages = [] } = pkgRootJson;
   componentsPattern = packages.map(packagePattern => (
     path.relative(cwdPath, path.join(rootDir, packagePattern, 'src/**/[A-Z]*.{js,jsx,ts,tsx}'))
  ));
} else {
   // 选择调试某个包时,则定位至选择的具体包下的组件
   componentsPattern = path.join(workDir, 'src/**/[A-Z]*.{js,jsx,ts,tsx}');
   isDevAllPackages = false;
}

 // 2. 获取默认的 webpack 配置
 const webpackConfig = getWebpackConfig(env);

 // 3. 生成 styleguidist 配置实例
 const styleguide = styleguidist({
   title: `${pkgRootJson.name}`,
   // 要解析的所有组件
   components: componentsPattern,
   // 属性解析设置
   propsParser: (filePath, code, resolver, handlers) => {
     if (/\.tsx?/.test(filePath)) {
       // ts 文件,使用 typescript docgen 解析器
       const pkgRootDir = findPackageRootDir(path.dirname(filePath));
       const tsConfigParser = docgenTS.withCustomConfig(
         path.resolve(pkgRootDir, 'tsconfig.json'),
        {},
      );
       const parseResults = tsConfigParser.parse(filePath);
       const parseResult = parseResults[0];
       return (parseResult as any) as RDocgen.DocumentationObject;
    }
     // 其他使用默认的 react-docgen 解析器
     const parseResults = docgen.parse(code, resolver, handlers);
     if (Array.isArray(parseResults)) {
       return parseResults[0];
    }
     return parseResults;
  },
   // webpack 配置
   webpackConfig: { ...webpackConfig },
   // 初始是否展开代码样例
   // expand: 展开 | collapse: 折叠 | hide: 不显示;
   exampleMode: 'expand',
   // 组件 path 展示内容
   getComponentPathLine: (componentPath) => {
     const pkgRootDir = findPackageRootDir(path.dirname(componentPath));
     try {
       const pkgJson = Utils.parsePackageSync(pkgRootDir);
       const name = path.basename(componentPath, path.extname(componentPath));
       return `import ${name} from '${pkgJson.name}';`;
    } catch (error) {
       return componentPath;
    }
  },
   // 非调试所有包时,不显示 sidebar
   showSidebar: isDevAllPackages,
   // 日志配置
   logger: {
     // One of: info, debug, warn
     info: message => Utils.log('info', message),
     warn: message => Utils.log('warning', message),
     debug: message => console.debug(message),
  },
   // 覆盖自定义配置
   ...customConfig,
});

 return styleguide;
};

这样,在 devbuild 命令下可以分别调用实例的 server 接口方法和 build 接口方法来实现调试和构建产出文档静态资源。

// dev 命令下启动调试
// 0. 初始化配置
const HOST = process.env.HOST || customConfig.serverHost || '0.0.0.0';
const PORT = process.env.PORT || customConfig.serverPort || '6060';

// 1. 生成 styleguide 实例
const styleguide = createDocStyleguide(
 'development',
{
   cwd: cwdPath,
   rootDir: pkgRootPath,
   workDir: workPath,
   customConfig: {
     ...customConfig,
     // dev server host
     serverHost: HOST,
     // dev server port
     serverPort: PORT,
  },
},
);

// 2. 调用 server 接口方法启动调试
const { compiler } = styleguide.server((err, config) => {
 if (err) {
   console.error(err);
} else {
   const url = `http://${config.serverHost}:${config.serverPort}`;
   Utils.log('info', `Listening at ${url}`);
}
});
compiler.hooks.done.tap('done', (stats: any) => {
 const timeStr = stats.toString({
   all: false,
   timings: true,
});

 const statStr = stats.toString({
   all: false,
   warnings: true,
   errors: true,
});

 console.log(timeStr);

 if (stats.hasErrors()) {
   console.log(statStr);
   return;
}
});
// build 命令下执行构建

// 生成 styleguide 实例
const styleguide = MonorepoDev.createDocStyleguide('production', {
 cwd,
 rootDir,
 workDir,
 customConfig: {
   styleguideDir: path.join(pkgDocsDir, 'dist'),
},
});
// 构建文档内容
await new Promise<void>((resolve, reject) => {
 styleguide.build(
  (err, config, stats) => {
     if (err) {
       reject(err);
    } else {
       if (stats != null) {
         const statStr = stats.toString({
           all: false,
           warnings: true,
           errors: true,
        });
         console.log(statStr);
         if (stats.hasErrors()) {
           reject(new Error('Docs build failed!'));
           return;
        }
         console.log('\n');
         Utils.log('success', `Docs published to ${path.relative(workDir, config.styleguideDir)}`);
      }
       resolve();
    }
  },
);

最后,在组件多包仓库的每个包下的 package.json 中,分别配置 devbuild 命令即可。实现了支持无感启动调试和构建产出文档资源。

小结

本文主要介绍了我在调研实现组件文档规范和搭建过程中的一个思考过程,诚如文中介绍其他文档系统搭建工具时所说,有很多优秀的开源工具能够支持实现我们想要的效果,这是前端攻城狮们的幸运,也是不幸:我们可以站在前人的肩膀上,但要在这么多优秀库中选择一个适合自己的,更需要多做一些了解和收益点的权衡。一句老话经久不衰:适合自己的才是最好的。

希望这篇文章对看到这里的你能有所帮助。

作者:ES2049 / 靳志凯
链接:https://segmentfault.com/a/1190000041097170

收起阅读 »

驳“低代码开发取代程序员”论 为什么专业开发者也需要低代码?

低代码又火了。近几年,腾讯、阿里、百度等互联网大厂纷纷入局,国内外低代码平台融资动辄数千万甚至数亿,以及伴随着热度而来的巨大争议……无不说明“低代码”的火爆。事实上,低代码并非新概念,它可以追溯到上世纪80年代的“第四代编程语言”。2014年,Forreste...
继续阅读 »

低代码又火了。

近几年,腾讯、阿里、百度等互联网大厂纷纷入局,国内外低代码平台融资动辄数千万甚至数亿,以及伴随着热度而来的巨大争议……无不说明“低代码”的火爆。

事实上,低代码并非新概念,它可以追溯到上世纪80年代的“第四代编程语言”。2014年,Forrester正式提出低代码的概念。低代码是一种软件开发技术,衍生于软件开发的高级语言,让使用者通过可视化的方式,以更少的编码,更快速地构建和交付应用软件,全方位降低软件的开发成本。与传统软件开发方式相比,低代码开发平台整合了软件开发和部署所需的 IDE(集成开发环境)、服务器和数据库管理工具,覆盖软件开发的全生命周期,我们可以将其理解为 Visual Studio + IIS + SQL Management Studio(.NET 技 术)或 Eclipse + Tomcat + MySQL Workbench(Java 技术)的组合。

编码更少、交付更快、成本更低,还覆盖软件开发全生命周期,怎么看低代码都可以说是不错的软件开发工具。那么,它又为什么引发争议,甚至被其主要用户群体之一——程序员所诟病呢?“低代码开发会取代程序员” 这一观点大行其是,它说得对吗?

为什么低代码引起专业开发者的反感?

技术浪潮引发巨大变革,也带来了无数“取代论”,比如机器翻译是否取代人类翻译、机器人记者是否取代人类记者,以及低代码开发是否取代程序员。

低代码虽然火爆,但程序员对此抱有不同的心态:

  • 轻视:低代码技术的诸多优势只是炒作,该技术更适合初学者,解决不了复杂的技术问题;

  • 恐惧:担心被低代码取代;

  • 抵触:低代码开发平台能够覆盖所有需求吗;大量封装组件使得低代码开发平台更像一个黑盒子,可能导致难以debug、难以修改和迭代升级等技术问题;低代码开发平台配置有大量组件,简单的拖拉拽动作即可完成大量开发工作,程序员不再需要厉害的技术能力。

那么,上述理由真的站得住脚吗?我们一一来看。

低代码的门槛真的低吗?

低代码开发过程常被比作拼积木:像拼搭积木一样,以可视化的方式,通过拖拉拽组件快速开发出数据填报、流程审批等应用程序,满足企业里比较简单的办公需求。

但这并不意味着低代码开发平台只能做到这些。

Gartner在2020年9月发布的《企业级低代码开发平台的关键能力报告》(Critical Capabilities for Enterprise Low-Code Application Platforms)中,列举了低代码的11项关键能力。

图源:http://www.gartner.com/en/document…

这里我们着重来看其中三项关键能力。

  • 数据建模和管理:该指标就是通常所讲的 “模型驱动” 。相比于表单驱动,模型驱动能够提供满足数据库设计范式的数据模型设计和管理能力。开发的应用复杂度越高,系统集成的要求越高,这个能力就越关键。

  • 流程和业务逻辑:流程应用与业务逻辑开发能力和效率。这个能力有两层,第一层是指使用该低代码开发平台能否开发出复杂的工作流和业务处理逻辑;第二层是开发这些功能时的便利性和易用性程度有多高。

  • 接口和集成:编程接口与系统集成能力。为了避免“数据孤岛”现象,企业级应用通常需要与其他系统进行集成,协同增效。此时,内置的集成能力和编程接口就变得至关重要。除非确认可预期的未来中,项目不涉及系统集成和扩展开发,开发者都应该关注这个能力。

这些关键能力表明低代码平台在建模与逻辑方面具备较强的能力,而接口和集成能力可使专业开发人员完成低代码无法实现的部分,通过低代码与专业代码开发的协作实现复杂应用的开发。 在涉及高价值或复杂的核心业务时,专业开发人员需要理解业务需求,厘清业务逻辑。从这个层面上看,低代码开发的门槛并不低。事实也是如此:海比研究在《2021 年中国低代码/无代码市场研究报告》中提到,截至 2020 年底,技术人员在低代码使用者中的比例超 75%,占主体地位。

低代码什么都能做吗?

程序员的工作围绕开发需求展开。在选择开发工具时,程序员通常考虑的首要问题是:这款工具能否覆盖所有需求?如果需求增加或变更,该工具是否支持相关操作?这些问题同样适用于低代码平台的选型。

在实际项目交付过程中,如果我们仅可以满足99%的需求,另外1%的需求满足不了,那么真实用户大概率是不会买单的。因此,在评估低代码产品的时候,我们一定要保证该平台可以支撑所有系统模块类型的开发,同时也要具备足够的扩展性,确保使用纯代码开发出的模块能够与低代码模块进行无缝集成,而这离不开编程接口。

以国内主流低代码开发平台活字格为例。该平台提供开箱即用的开发组件,同时为系统的各个分层均提供编程扩展能力,以满足企业级应用开发对扩展性的高要求。借助分层编程接口,开发者可以用纯代码的方式实现新增功能,无需受限于低代码开发平台的版本和现有功能。


活字格的编程扩展能力

当然,就具体应用领域而言,低代码开发平台也有其擅长和不擅长的地方。目前,低代码开发更多地被应用于2B企业应用开发,而对于用户量特大的头部互联网应用、对算法和复杂数据结构要求较高的应用,低代码平台则不太适合。

低代码开发不可控?

“低代码开发平台是个黑盒子,内部出问题无法排查和解决。开发过程中发现有问题怎么办?迭代升级难以实现怎么办?”很多程序员会有这种疑惑。

但我们需要注意的是,低代码开发平台本质上仍是软件开发工具,用户模型与软件开发周期支持是其关键能力之一。也就是说,成熟的低代码开发平台具备软件开发全生命周期所需的各项功能,从而大大简化开发者的技术栈,进一步提高开发效率。

具体而言,在面对频繁的需求变更、棘手的问题排查时,低代码开发平台引入了版本管理机制,从而更高效地进行代码审查、版本管理与协调,以及软件的迭代升级。至于debug,日志分析无疑是个好办法。例如,活字格把执行过程及细节以日志方式输出,方便程序员高效debug。

对程序员而言,低代码平台是限制还是助力?

“低代码”意味着更少的代码。代码都不怎么写了,程序员又该怎么成长,怎么获得职业成就感呢?

其实不然。

首先,开发 ≠ 写代码。低代码平台可以减少大量重复工作,提升开发效率,把专业开发人员从简单、重复的开发需求中解放出来,把精力投入到更有价值的事情上,比如精进技术、理清业务逻辑。

其次,低代码平台的组件化和拖拽式配置降低了开发门槛,新手程序员能够借助此类平台快速入门,加速升级打怪;有经验的程序员也有机会参与更多项目,甚至带团队,积累更多经验值,实现快速成长。

宁波聚轩就是一个例子。这家公司自2009年起就专注于智能制造、工业4.0、系统方案集成等领域的探索研究。在接触了低代码之后,项目负责人发现开发效率得到极大提升,采用传统方式需要一个月开发量的项目,现在需要半个月甚至更短的时间就可以完成。此外,其实践经验表明,低代码开发的学习成本较低,毕业新生经过一周学习,两周就可做项目,一个月就能熟练开发。

该公司在2021企业级低代码应用大赛中获得了应用创新奖,获奖作品是一套轴承行业数字化智造系统。这套系统主要集成了ERP、MES、WMS和设备机联网系统,覆盖了销售、采购、仓库、计划、生产、财务等全流程功能,且已经在生产现场投入使用。在开发过程中,宁波聚轩的开发团队利用低代码平台成功解决了定制化要求高、多终端需求等难题,及时完成项目交付。

结语

当迷雾散尽,低代码开发平台重新露出高效率开发工具的本色时,你会选择它吗?


作者:SegmentFault思否
来源:https://juejin.cn/post/7023579572096466974

收起阅读 »

2021年爆火的“元宇宙”,是真风口还是资本骗局下的割韭菜?

Metaverse——元宇宙。一个出自1992年科幻小说「雪崩」(Snow Crash)的概念,在2021年突然火爆了起来。各路资本也纷纷下场,似乎这个「元宇宙」马上就要实现了一样。甚至有人称2021年是「元宇宙」元年。 就在今年,字节跳动、Facebook...
继续阅读 »

Metaverse——元宇宙。一个出自1992年科幻小说「雪崩」(Snow Crash)的概念,在2021年突然火爆了起来。各路资本也纷纷下场,似乎这个「元宇宙」马上就要实现了一样。甚至有人称2021年是「元宇宙」元年。


2021年爆火的“元宇宙”,是真风口还是资本骗局下的割韭菜?


就在今年,字节跳动、Facebook、腾讯、英伟达等从国外到国内的科技巨头纷纷宣布自己的元宇宙未来发展战略,把元宇宙视为公司未来存亡的关键。


Facebook创始人扎克伯格,更是雄心勃勃的表示,未来5年Facebook要变成「元宇宙公司」,可以说,在资本的涌入之下,元宇宙已经成为互联网公司的第一赛道。


一、什么是元宇宙?


各位有没有看过前两年大火的电影《头号玩家》,影片讲述2045年,由于现实生活无趣,无数年轻人迷失在一款超级火爆的游戏《绿洲》,当你穿上一身装备,站在一个可以自由奔跑的平台上时,一个崭新的世界便呈现了出来,这个新世界,就是元宇宙。


2021年爆火的“元宇宙”,是真风口还是资本骗局下的割韭菜?


在这个绿洲世界,你既可以升级打怪,也可以修仙摸鱼;既可以感受江湖恩怨、武侠情仇;又可以躲在悠远的溪谷,享受采菊东篱下的恬静。在这个世界,你可以为所欲为,体验你幻想中的一切。


在极具娱乐性的同时,这个世界并非完全脱离现实世界,例如游戏世界的金钱可以和现实世界互通共用,例如服务可以同向切换,例如你在虚拟世界也可以叫滴滴代驾,这个虚拟的世界与现实世界在很多方面重叠互通,你可以随时通过VR设备、电脑、手机登陆这个线上世界,又可以通过线上世界的行为影响到真实生活,在它脱胎于现实世界,又与现实世界平行。


这就是一个完美的元宇宙构想,是互联网高度发展后的一种延伸。


2021年爆火的“元宇宙”,是真风口还是资本骗局下的割韭菜?


很多朋友要问,我们已经有互联网了,为什么还要探索元宇宙呢?难道仅仅是为了一款游戏吗?


那肯定不是,元宇宙的思想,其实是为了提升世界的互联互通,在现实世界中,互联网技术和协议本身是开放的,但商业世界却大多是封闭的,《头号玩家》里人们在各个虚拟世界穿梭,但有着统一的账号身份、好友关系、实时联通的数据流动、资产流转。


这在今天的互联网世界里就是不存在的,比如,你在央行的征信状况和支付宝信用分就不是互通的,你的比特币和微信钱宝更不互通,人民币和美元汇率也不是实时对等,这种世界处处存在着摩擦,公司与公司之间,国际之间的信息都具有极高的壁垒。


元宇宙正是要打破这种壁垒,把所有公司和人都链接起来,构建起新的商业、金融、贸易、交流规则,实现更广阔互联网。


乍一听,这样的想法简直是天方夜谈,根本不可能实现,但是这几年区块链和VR技术的发展,让这种构想逐渐成为现实。


二、已经处于技术爆发的前夜


凌乐作为一个保守派的投资者,当看到这个概念的时候我也是一脸懵逼?这不就是一个VR游戏吗?有什么值得吹的?VR概念都炒作这么久了,无论是产品力还是实用程度,都离现实差了很远,凭什么现在来炒作?怎么看,都是一种虚假的概念。


但是仔细去研究之后,我发现,技术框架已经逐渐成型,这个飘渺的未来,或许正在实现。


区块链让框架规则出现了一定雏形:


在头号玩家中,最大的BOSS是游戏公司,也就是那个世界的神,可以制定规则,改变规则。但是在现实中,我们不需要这么一个神,这么庞大的工程,需要很多企业和用户共同搭建,需要让主流科技公司彼此合作,需要说服全球的监管机构,让他们相信互联网公司们打造的世界对社会是有利的,更需要让用户相信,这个世界没有潜规则,在部分规则方面绝对平等。


如果是在以前,根本不存在这种技术,但是区块链的产生,这种开始产生使用场景,NFT(Non-fungible Token)技术可以让区块链标识成为新的价值承载物,使虚拟物品资产化,从而实现数据内容的价值流转,规避黑箱操作,比特币的成功,又让世界意识到去中心化真的可以实现。


可以说,区块链技术,让这个绝对公平的商业规则出现了基本雏形。


VR设备快速升级,已处于爆发前夜:


2021年爆火的“元宇宙”,是真风口还是资本骗局下的割韭菜?


不管新世界的内容有多么丰富,都需要一道门来链接人和新世界,目前来看,VR设备就是开启这道新宇宙的门。


VR概念早在2016年就开始炒作了,不过因为技术水平低,实际体验感并不好,Facebook也是意识到VR设备的重要性,它们在 2016 年以 20 亿美元买下VR设备企业Oculus,并且每年在VR业务上的研发投入上百亿美元研发VR,在多家大厂入局的情况,现在VR设备技术突飞猛进,我专门去淘宝看了销量,Facebook下的这款VR眼镜居然都有四千多条评价,京东上面的销量更高。


扎克伯格认为,当产品销量突破1000万的时候,就标志着VR市场开始爆发,根据市场统计,Facebook旗下的VR设备Quest 2今年年底的销量就会突破1000万台,工信部预计2021年VR年复合增长率达91.2%,这个市场已经处于爆发前夜。


作者:IT技术管理的那些事儿
链接:https://juejin.cn/post/7008408876961759262
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

一篇完整的Swift属性参考,轻松让你提高一个档次!

iOS
属性 提供了更多关于声明和类型的信息。在 Swift 中有两种类型的属性,一种用于声明,一种用于类型。例如,required 属性-当用于类的指定或者便利初始化声明时-指明每个子类都必须实现它的初始化函数。noreturn 属性-当用于函数或者方法的类型时-指...
继续阅读 »

属性 提供了更多关于声明和类型的信息。在 Swift 中有两种类型的属性,一种用于声明,一种用于类型。例如,

required 属性-当用于类的指定或者便利初始化声明时-指明每个子类都必须实现它的初始化函数。noreturn 属性

-当用于函数或者方法的类型时-指明函数或者方法无需返回值。

咋们一起好好看,好好学

你可以用字符 @ 加上属性名和属性参数来指定一个属性:

@属性名
@属性名(属性参数)

含有参数的声明属性可以为属性指定更多的信息,可以用于特殊的声明。这些属性参数 被包含在圆括号里,参数的格式由属性决定。

声明属性

声明属性只能用于声明,当然,你也可以使用 noreturn 属性作为函数或者方法的类型。

assignment

此属性可用于修饰重载复合赋值运算符的函数。这个重载复合赋值运算符的函数必须用 inout 来标记初始输入参数。assignment属性示例参见复合赋值运算符

class_protocol

此属性可用于定义类类型协议。

如果你使用 objc 属性的协议, 那么这个协议就隐式含有 class_protocol 属性,你无需显式标记 class_protocol 属性。

exported

此属性可用于内部声明,可以将当前模块的内部模块、子模块或者声明暴露给外部其他模块。如果另一个模块引用了当前模块,那么这个模块就可以访问当前模块中暴露出来的部分。

final

此属性可用于修饰类或者类的属性、方法或成员下标运算符。用于一个类的时候,表明这个类是不能被继承的。用于类的属性、方法或成员下标运算符的时候,表明这个类的这些成员函数不能在任何子类中重写。

lazy

此属性可用于修饰类或者结构体中的存储变量属性,表明这个属性在第一次被访问时,其初始值最多只能被计算和存储一次。lazy 属性示例参见惰性存储属性

noreturn

此属性用于函数或者方法的声明,表明其函数或者方法相应的类型T是@noreturn T。当一个函数或者方法无需返回其调用者时,你可以用这个属性来修饰其类型。

你可以重写没有标示noreturn属性的函数或者方法。也就是说,你不能够重写有noreturn属性的函数或者方法。当你实现此类型的协议方法时,也有相似的规则。

NSCopying

此属性可用于修饰类中的存储变量属性。被修饰的这个属性的赋值函数是由这个属性值的拷贝组成-由copyWithZone方法返回-而不是这个属性本身的值。此属性类型必须符合NSCopying协议。
NSCopying属性类似于Objective-C中的copy属性。

NSManaged

用于修饰类中的存储变量属性,此类继承于NSManagedObject,表明这个属性的存储和实现是由Core Data基于相关的实体描述实时动态提供的。

objc

此属性可用于能用Objective-C表示的任何声明中-例如,非嵌套的类、协议、类和协议的属性和方法(包括取值函数和赋值函数)、初始化函数、析构函数以及下标运算符。objc属性告诉编译器此声明在Objective-C代码中可用。

如果你使用objc属性修饰类或者协议,它会显式的应用于这个类或者协议中的所有成员。当一个类继承于标注objc属性的另一类时,编译器会显式的为这个类添加objc属性。标注objc属性的协议不能够继承于不含有objc属性的协议。

objc属性可以接受由标识符组成的单个属性参数。当你希望暴露给Objective-C的部分是一个不同的名字时,你可以使用objc属性。你可以使用这个参数去命名类、协议、方法、取值函数、赋值函数以及初始化函数。下面的示例就是ExampleClass的enabled属性的取值函数,它暴露给Objective-C代码的是isEnabled,而不是这个属性的原名。

1.  @objc
2. class ExampleClass {
3. var enabled: Bool {
4. @objc(isEnabled) get {
5. // Return the appropriate value
6. }
7. }
8. }

optional

此属性可用于协议的属性、方法或者成员下标运算符,用来表明实现那些成员函数时,此类型的不是必需实现的。

optional属性只能用于标注objc属性的协议。因此,包含可选成员的协议只有类类型适用。更多的关于怎样使用optional属性,以及怎样访问可选协议成员的指导-例如,当你不确定它们是否实现了此类型时-参见可选协议需求

required

此属性用于类的指定或者便利初始化函数时,表明这个类的每个子类都必须实现这个初始化函数。

需求的指定初始化函数必须被显式的包含。当子类直接实现所有超类的指定初始化函数时(或者子类使用便利初始化函数重写了指定初始化函数时),需求的便利初始化函数必须被显式的包含或者继承。

使用Interface Builder声明属性

Interface Builder属性就是使用Interface Builder声明属性以与Xcode同步。Swift提供了如下几种Interface Builder属性:IBAction,IBdesignable,IBInspectable以及IBOutlet。这些属性理论上与Objective-C中相应的属性一样。

IBOutlet和IBInspectable属性可用于类的属性声明,IBAction属性可用于类的方法声明,IBDesignable属性可用于类的声明。

类型属性

类型属性可以只用于类型。当然noreturn属性也可以用于函数或者方法的声明。

auto_closure

此属性用于延迟表达式的赋值计算,将表达式自动封装成一个无参数的闭包。此属性还可作为函数或者方法的类型,此类型无参数并且其返回的是表达式类型。auto_closure属性示例参见函数类型

noreturn

此属性用于函数或者方法时表明此函数或者方法无返回值。你也可以用此属性标记函数或者方法的声明,以表明其函数或者方法相应的类型T是@noreturn T。

属性语法
attribute → @­attribute-name attribute-argument-clause opt
attribute-name → identifier
attribute-argument-clause → (balanced-tokens­ opt)
attributes → attribute­ attributes­ opt­
balanced-tokens → balanced-token ­balanced-tokens­ opt­
balanced-token → (­balanced-tokens­ opt­)­
balanced-token → [balanced-tokens­ opt­]­
balanced-token → {balanced-tokens­ opt­­}­
balanced-token → 任意标识符,关键字,常量,或运算符
balanced-token → 任意的标点符号 (­, )­, [­, ]­, {­, 或 }­

由于文章篇幅有限,只能点到即止地介绍当前一些工作成果和思考,各个 Swift 还有一些新的方向在探索,如果你对 iOS 底层原理、架构设计、构建系统、如何面试有兴趣了解,你也可以关注我及时获取最新资料以及面试相关资料。如果你有什么意见和建议欢迎给我留言!

写的不好的地方欢迎大家指出,希望大家多留言讨论,让我们共同进步!

喜欢iOS的小伙伴可以关注我,一起学习交流!!!

链接:juejin.cn/post/698169…


作者:在做开发的信哥
链接:https://juejin.cn/post/6988459235797368862

收起阅读 »

啥?iOS长列表还可以这么写

iOS
一般说,iOS界面的一些长列表,比如首页,活动页,长的会比较长,那么写起来总感觉没有那么优雅,那么如何才能做到优雅呢? 我在实践工作利用swift枚举的关联值和自定义组模型方法来实现了 下面是gif图效果 可以看到,有些组是杂乱无章的排列着,而且运营那边...
继续阅读 »

一般说,iOS界面的一些长列表,比如首页,活动页,长的会比较长,那么写起来总感觉没有那么优雅,那么如何才能做到优雅呢?
我在实践工作利用swift枚举的关联值和自定义组模型方法来实现了



  • 下面是gif图效果



可以看到,有些组是杂乱无章的排列着,而且运营那边要求,他们可以在后台自定义这些组的顺序
这可怎么办!🥺
下面看我的实现方式


定义一个组模型枚举



  • 包含可能的定义,每个枚举关联当前组需要显示的数据模型,有可能是一个对象数组,也有可能是一个对象


/// 新版首页组cell的类型
enum OriginGroupCellType {
case marquee(list: [MarqueeModel]) // 跑马灯
case beltAndRoad(list: [GlobalAdModel]) // 一带一路广告位
case shoppingCarnival(list: [GlobalAdModel]) // 购物狂欢节
case walletCard(smallWelfare: WelfareSmallResutlModel) // 钱包卡片
case wallet(list: [HomeNavigationModel]) // 钱包cell
case otc(list: [GlobalAdModel]) // OTC
case hxPrefecture(list: [GlobalAdModel]) // HX商品专区
case middleNav(list: [HomeNavigationModel]) // 中部导航
case bottomNav(list: [HomeNavigationModel]) // 底部导航
case broadcast(topSale: HomeNavigationModel, hot: OriginBroadcastModel, choiceness: OriginBroadcastModel) // 直播cell
case middleAd(list: [GlobalAdModel]) // 中间广告cell
case localService(list: [LocalServiceModel]) // 本地服务cell
case bottomFloat(headerList: [OriginBottomFloatHeaderModel]) // 底部悬停cell
}


  • 考虑到要下拉刷新等问题,可以这些枚举都得遵守Equatable协议


  extension OriginGroupCellType: Equatable {
public static func == (lhs: OriginGroupCellType, rhs: OriginGroupCellType) -> Bool {
switch (lhs, rhs) {
case (.marquee, .marquee): return true
case (.beltAndRoad, .beltAndRoad): return true
case (.shoppingCarnival, .shoppingCarnival): return true
case (.walletCard, .walletCard): return true
case (.wallet, .wallet): return true
case (.otc, .otc): return true
case (.hxPrefecture, .hxPrefecture): return true
case (.middleNav, .middleNav): return true
case (.bottomNav, .bottomNav): return true
case (.broadcast, .broadcast): return true
case (.middleAd, .middleAd): return true
case (.localService, .localService): return true
case (.bottomFloat, .bottomFloat): return true
default:
return false
}
}
}

接下来就是组模型的定义



  • 同时我抽取一个协议GroupProvider,方便复用


protocol GroupProvider {
/// 占位
associatedtype GroupModel where GroupModel: Equatable

/// 是否需要往组模型列表中添加当前组模型
func isNeedAppend(with current: GroupModel, listMs: [GroupModel]) -> Bool
/// 获取当前组模型在组模型列表的下标
func index(with current: GroupModel, listMs: [GroupModel]) -> Int
}

extension GroupProvider {
func isNeedAppend(with current: GroupModel, listMs: [GroupModel]) -> Bool {
return !listMs.contains(current)
}

func index(with current: GroupModel, listMs: [GroupModel]) -> Int {
return listMs.firstIndex(of: current) ?? 0
}
}




  • OriginGroupModel,同样也遵守Equatable协议,防止重复添加


func addTo(listMs: inout [OriginGroupModel]) 



  • 这个方法是方便于下拉刷新时,替换最新数据所用


public struct OriginGroupModel: GroupProvider {
typealias GroupModel = OriginGroupModel

/// 组模型的类型
var cellType: OriginGroupCellType
/// 排序
var sortIndex: Int

/// 把groupModel添加或替换到listMs中
func addTo(listMs: inout [OriginGroupModel]) {
if isNeedAppend(with: self, listMs: listMs) {
listMs.append(self)
} else {
let index = self.index(with: self, listMs: listMs)
listMs[index] = self
}
}
}

extension OriginGroupModel: Equatable {
public static func == (lhs: OriginGroupModel, rhs: OriginGroupModel) -> Bool {
return lhs.cellType == rhs.cellType
}
}


  • 考虑要自定义顺序,所以需要定义一个排序的实体


// MARK: - 新版首页组模型的排序规则模型
struct OriginGroupSortModel {
/// 搜索历史的排序
var marqueeIndex: Int
var beltAndRoadIndex: Int
var shoppingCarnivalIndex: Int
var walletCardIndex: Int
var walletIndex: Int
var otcIndex: Int
var hxPrefectureIndex: Int
var middleNavIndex: Int
var bottomNavIndex: Int
var broadcastIndex: Int
var middleAdIndex: Int
var localServiceIndex: Int
var bottomFloatIndex: Int

static var defaultSort: OriginGroupSortModel {
return OriginGroupSortModel(
marqueeIndex: 0,
beltAndRoadIndex: 1,
shoppingCarnivalIndex: 2,
walletCardIndex: 3,
walletIndex: 4,
otcIndex: 5,
hxPrefectureIndex: 6,
middleNavIndex: 7,
bottomNavIndex: 8,
broadcastIndex: 9,
middleAdIndex: 10,
localServiceIndex: 11,
bottomFloatIndex: 99)
}
}


控制器里定义一个 组模型数组



  • 这里有关键代码是


listMs.sort(by: { return $0.sortIndex < $1.sortIndex }) 



  • 所有的数据加载完毕后,会根据我们的自定义排序规则去排序


    /// 组模型数据
public var listMs: [OriginGroupModel] = [] {
didSet {
listMs.sort(by: {
return $0.sortIndex < $1.sortIndex
})
collectionView.reloadData()
}
}

/// 组模型排序规则(可以由后台配置返回,在这里我们先给一个默认值)
/// 需要做一个请求依赖,先请求排序接口,再请求各组的数据
public lazy var sortModel: OriginGroupSortModel = OriginGroupSortModel.defaultSort


网络请求代码


func loadData(_ update: Bool = false, _ isUHead: Bool = false) {
// 定义队列组
let queue = DispatchQueue.init(label: "getOriginData")
let group = DispatchGroup()

// MARK: - 文字跑马灯
group.enter()
queue.async(group: group, execute: {
HomeNetworkService.shared.getMarqueeList { [weak self] (state, message, data) in
guard let `self` = self else { return }
self.collectionView.uHead.endRefreshing()

defer { group.leave() }
let groupModel = OriginGroupModel(cellType: .marquee(list: data), sortIndex: self.sortModel.marqueeIndex)
guard !data.isEmpty else { return }

/// 把groupModel添加到listMs中
groupModel.addTo(listMs: &self.listMs)
}
})

/// .... 此处省略其它多个请求

group.notify(queue: queue) {
// 队列中线程全部结束,刷新UI
DispatchQueue.main.sync { [weak self] in
self?.collectionView.reloadData()
}
}
}


collectionView的代理方法处理


    func numberOfSections(in collectionView: UICollectionView) -> Int {
return listMs.count
}

func collectionView(_: UICollectionView, numberOfItemsInSection section: Int) -> Int {
let groupModel = listMs[section]
switch groupModel.cellType {
case .marquee, .beltAndRoad, .walletCard, .wallet, .otc, .hxPrefecture, .shoppingCarnival, .middleAd:
return 1
case .middleNav(let list):
return list.count
case .bottomNav(let list):
return list.count
case .broadcast:
return 1
case .localService(let list):
return list.count
case .bottomFloat:
return 1
}
}



  • 同理,collectionView的代理方法中,都是先拿到 cellType 来判断,达到精准定位, 举个栗子


    /// Cell大小
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let groupModel = listMs[indexPath.section]
let width = screenWidth - 2 * margin
switch groupModel.cellType {
case .marquee:
return CGSize(width: screenWidth, height: 32)
case .beltAndRoad:
return CGSize(width: width, height: 46)
case .walletCard:
return CGSize(width: width, height: 85)
case .wallet:
return CGSize(width: width, height: OriginWalletCell.eachHeight * 2 + 10)
case .otc, .hxPrefecture:
return CGSize(width: width, height: 60)
case .middleNav:
let row: CGFloat = 5
let totalWidth: CGFloat = 13 * (row - 1) + 2 * margin
return CGSize(width: (screenWidth - totalWidth) / row, height: CGFloat(98.zh(80).vi(108)))
case .bottomNav:
let isFirstRow: Bool = indexPath.item < 2
let row: CGFloat = isFirstRow ? 2 : 3
let totalWidth: CGFloat = 4 * (row - 1) + 2 * margin
let width = (screenWidth - totalWidth) / row
return CGSize(width: floor(Double(width)), height: 70)
case .shoppingCarnival:
return CGSize(width: width, height: 150)
case .broadcast:
return CGSize(width: screenWidth - 20, height: 114)
case .middleAd:
return CGSize(width: width, height: 114)
case .localService:
let width = (82 * screenWidth) / 375
return CGSize(width: width, height: 110)
case .bottomFloat:
let h = bottomCellHeight > OriginBottomH ? bottomCellHeight : OriginBottomH
return CGSize(width: screenWidth, height: h)
}
}


总结一下这种写法的优势




  • 方便修改组和组之前的顺序问题,甚至可以由服务器下发顺序




  • 方便删减组,只要把数据的添加组注释掉




  • 用枚举的方式,定义每个组,更清晰,加上swift的关联值优势,可以不用在控制器里定义多个数组




  • 考虑到要下拉刷新,所以抽取了一个协议 GroupProvider,里面提供两个默认的实现方法



    • 方法一:获取当前cellType在listMs中的下标

    • 方法二:是否要添加到listMs中




  • 界面长什么样,全部由数据来驱动,这组没有数据,界面就对应的不显示(皮之不存,毛将焉附),有数据就按预先设计好的显示




源码地址(源码内容和gif图中有差异,但是思路是一致的)


github.com/XYXiaoYuan/…


作者:Bruceyuan
链接:https://juejin.cn/post/6939767696846225421

收起阅读 »

Awesome metaverse projects (元宇宙精选资源汇总)

Awesome Metaverse 关于 Metaverse 的精彩项目和信息资源列表。由于关于 Metaverse 是什么存在许多相互竞争的想法,请随时以拉取请求、问题和评论的形式留下反馈。WebXRWebXR Explainer - 什么是 WebXR,有...
继续阅读 »



Awesome Metaverse

关于 Metaverse 的精彩项目和信息资源列表。

由于关于 Metaverse 是什么存在许多相互竞争的想法,请随时以拉取请求、问题和评论的形式留下反馈。

WebXR

社交虚拟现实(Social VR)

开源

非免费

  • Cryptovoxels - 用户拥有的虚拟世界

  • Somnium Space - 基于区块链的持久虚拟世界

  • NeosVR - 旨在加速社交 VR 应用程序开发的引擎

  • VRChat

    - 最大的社交 VR 平台,拥有世界和头像的 UGC

    • Awesome VRChat - 有兴趣为 VRchat 开发内容的人的一站式商店

  • Roblox - 大型在线多人 UGC 游戏平台

  • Omniverse - 3D 生产流水线的实时模拟和协作平台

  • dot bigbang - 基于 Web 的多人 UGC 游戏平台,内置工具和 Typescript 脚本

  • Helios - 基于虚幻引擎的 UGC 世界、头像和游戏平台

  • Meta - The Metaverse 的 Meta(前 Facebook)公告视频

头像提供者

协议和标准

书籍

科幻

  • Neuromancer - (80s) 定义了赛博朋克流派和赛博空间一词

  • Snow Crash - (90 年代) 创造术语 Metaverse 作为互联网的继承者

  • Ready Player One - (2011) 后来成为斯皮尔伯格电影的热门书

  • Ready Player Two - (2020) 准备好的玩家一的续集

  • Rainbows End - (2006) 越来越多的数字/虚拟世界与无处不在的计算

  • Idoru - 虚拟名人和分散的虚拟世界,桥梁三部曲中的第 2 册

非小说

  • 空间网络 - Web 3.0 将如何连接人类、机器和人工智能以改变世界

电影

  • The Matrix 矩阵

  • The Thirteenth Floor 十三楼

  • Existenz 存在

  • Free Guy 自由人

  • Tron 创

  • Wreck it Ralph 2 破坏它拉尔夫 2

  • Ready Player One 准备好球员一

文章和博客

加密

白皮书

链接

翻译文章

元节入门

元节入门

后期 ROAD-MAP

  • 整理相关的资源到当前仓库。

  • 白皮书翻译

作者:houbb
来源:https://github.com/houbb/awesome-metaverse-zh

收起阅读 »

到2030年将存在的10个元宇宙工作

还记得2016年么? 宝可梦GO(Pokémon GO)席卷全球,当时许多人认为我们正站在增强现实技术(AR)革命的风口上。 显然, 这并没有实现。快进到今天,我们再次就Facebook/Meta 疯狂投资为创建一个元宇宙(Metaverse)——人人都生活在...
继续阅读 »



还记得2016年么? 宝可梦GO(Pokémon GO)席卷全球,当时许多人认为我们正站在增强现实技术(AR)革命的风口上。 显然, 这并没有实现。快进到今天,我们再次就Facebook/Meta 疯狂投资为创建一个元宇宙(Metaverse)——人人都生活在其中的完全沉浸式数字世界,展开了类似的谈论。

来自如此多参与者的这种投资往往会创造一个自我实现的预言:无论我们喜欢与否,我们都可能很快就拥有一个运转的元宇宙,只因科技霸主想这样做。

因为我的工作是规划(而非预测)未来的工作,所以我研究了这种可能性并自我发问:“元宇宙将创造什么类型的工作?” 以下是一些初步想法。

*快速定义

  • 虚拟现实 (VR):全人工环境;完全沉浸在虚拟环境中。

  • 增强现实 (AR):虚拟物体与真实世界环境重叠;数字物体增强了真实世界。

  • 混合现实 (MR):虚拟环境融合真实世界;现实世界和虚拟环境皆可交互。

扩展现实 (XR) / 元宇宙:上述一切的混合。


1. 元宇宙研究科学家

AR 和 VR 研究科学家已经是顶尖大学和大型科技公司的主要人才。但在元宇宙(或者您对“物质世界和数字世界的无缝交织”的任意称呼) 慢慢成为一个被广泛接受的观点同时,我们需要更多的智慧

开发一些真实世界的基本数字模型,让企业将能够在这些模型中吸引客户和合作伙伴,元宇宙研究科学家的工作可不仅仅是如此。这已经存在了。未来会有更大的变化。元宇宙研究科学家需要构建的是一种类似于万物理论的东西,其中整个世界都是可见的和可数字化操作。(想象一下没有乐趣的《头号玩家》)。该架构将是所有其他用例构建的基础;游戏、广告、工厂质量控制、互联健康、DeFi……等等。

这是一项极度复杂的任务,元宇宙研究科学家们需要能够使用计算机视觉算法融合的技术构建并缩放原型,用于3D计算摄影神经渲染场景重构计算成像视觉惯性里程计状态估计传感器融合,绘图与定位……这些原型将随着时间推移而变大

  • 如何成为元宇宙研究科学家:获得深度学习、计算机视觉、计算机图形学或计算成像的博士学位。你还需要了解c++,祝你好运。

2. 元宇宙规划师

一步实际行动比一打纲领更重要。一旦我们有了一个运行着的元宇宙,将所有功能规划和实施到一个完全虚拟的世界中的能力绝对是大多数公司的关键。在这个不断扩张的数字世界中选择正确的事情亦是如此。

这就是元宇宙规划师的用武之地。随着CEO为创造和增长其公司的元宇宙收入而制定愿景和战略时,规划师们需要推动从概念验证到试点再到部署的战略性机遇组合。这意指识别市场机会、建立商业案例、影响工程路线图、制定关键指标等……

你知道的…有趣的事物

这似乎并不吸引人,但您如何决定汽车公司是应该专注于创建虚拟驾驶测试,还是应实施一个数字孪生业务来预测故障?我不知道,但是规划师们肯定会给出答案。

  • 如何成为元宇宙规划师:拥有多年的管理经验,了解硬件/软件/SaaS/PaaS营销和商业模式,以及良好的创业心态。

3. 生态系统开发者

元宇宙不会靠扎克伯格的意志自行实现,需要围绕它建立一个健全的生态系统。传感器、CPU、GPU、KYC流程、数据湖(data-lakes)、绿色电能生产、边缘计算、法律、法规……世界是复杂的,因此进一步数字化(比现在更复杂)并非易事。我们可以将这一困难同目前汽车行业向电动汽车转型所面临的困难对比。电动汽车就在那里,但它们被采用的最大障碍是街道和道路上缺乏广泛分布的充电站,以及电池容量的不断变化。同样,我们可能拥有实现元宇宙的软件和硬件,但是仍然缺乏……其他一切

元宇宙生态系统开发者将负责协调合作伙伴和政府,以确保创建的各种功能能够大规模实现。他们将推动政府对基础建设的投资,并激发大型社区的活力。

元宇宙生态系统开发者们需要关注的一个关键问题是互操作性,以确保元宇宙客户能够在不同的体验中使用他们的虚拟道具。毕竟,如果你不能在商场里也穿着它,那么在迷你游戏中获得炫酷皮肤又有什么意义呢?其他游说努力将面向金融机构,它们需要支持分布式账本技术,以及在平台上交换商品和服务的智能合约。

  • 如何成为生态系统开发者:拥有多年的政务/游说经验以及对蓬勃发展的XR行业的深刻理解。

4. 元宇宙安全经理

您确信互联网对每个人来说都很安全吗? 是的,我也不。任何声称元宇宙会更安全的人都是在自欺欺人。当然,它有很多机会成为一个更安全、包容的地方,但是这不会自己发生。

隐私。虚拟身份认证。安全帽。足够的传感器……我们需要能在设计、验证和量产阶段提供指导和监督的人员,确保我们的数字世界是安全的,满足或超过适用的监管安全要求。显然,这一切都未牺牲尖端功能或设计,或削尖收入。这个人员即是元宇宙安全经理。

这也不是一个简单活。他们需要准确地预测元宇宙功能将如何被使用或者被滥用,并识别与这些预测相关的安全关键组件(safety-critical components)、系统和制造步骤。这十足的复杂性和移动部件的数量光是想想就足以让我头晕目眩。

  • 如何成为XR安全经理:拥有工程学位和消费电子产品/制造经验。

5. 元宇宙硬件制造商

元宇宙不会(仅仅)构建在代码之上,它(也)将构建在传感器、摄像头和耳机上。 这样的传感器会让你感受到被触摸就仿佛有人在网上挤压你的手臂。摄像头可以查看你是否心情不好,这样AI就不会过多打扰你。而耳机则可以感受到你周围的太阳,并在数字世界中投射出夏日,以增加真实感。这甚至还没有涉及到无聊的东西,譬如用以辅助跟踪、绘图和定位的惯性测量单元、视觉光相机、深度相机……

目前,最好的传感器是为工业作业和汽车行业制造的。这些都是拥有大量资本的行业。因此,作为一个额外的挑战,无论谁制造元宇宙的硬件,都需要确保他们足够廉价且安全,以便元宇宙就不会成为富人们的专属玩物。

  • 如何成为传感器制造商:拥有一家能够制造复杂消费电子产品的工厂。嘿,我可从没说过这很容易

6. 元宇宙作家(storyteller)

随着体验经济和游戏化概念的不断发展,我们要求我们的扩展现实体验具有很棒的并可汲取教训的故事情节,这是非常符合逻辑的。我们想笑;我们想哭;我们想要学习;我们想要从数字世界中看到一些稍微稀奇古怪点的东西。这就是元宇宙作家的用武之地.

元宇宙作家将负责为用户设计沉浸式的任务以探索元宇宙,如军事训练方案、以公司叙述的方式进行难以发现的营销机会、心理学会议(为什么杀死内心的恶魔当你可以在数字世界中模拟杀他们)……例子不胜枚举。

他们不会得到很好的报酬,除非你把为他们打call也算作报酬,但你没有。但至少他们会出售能够吸引数百万人来追的故事线,以助其摆脱枯燥的日常生活。这难道不是元宇宙作家们的梦想吗?

  • 如何成为元宇宙作家:主修文学,辅修市场营销,在游戏公司开始你的职业生涯,然后转向科技公司。

7. 世界建造者

在我们建立了我们的架构、我们的硬件和我们的故事情节后,我们仍需要创建整个世界(想想《盗梦空间》中的Ellen Page —或者是Elliot,老实说当时她仍在使用她的弃名)。我的意思不是为世界编码,我是指,想象世界

这个角色需要许多与电子游戏设计师相同的技能,尽管规则可能云泥之别。世界建设者将需要具有前瞻性并迎接未来,因为他们梦想的许多东西还没有以技术或产品解决方案的形式存在。

他们还需要考虑规章和道德。当数字世界变得真实时,在其中杀害可以吗?犯下战争罪行?我们已经自我发问这些问题了,但讨论还离得出结论很远呢。

  • 如何成为世界建造者:半战争诗人,半平面设计师。擅长我的世界(Minecraft)也没有什么坏处。

8. 广告拦截专家

Facebook…噢不好意思,是Meta,它如何赚钱?通过向它的虚假信息工厂出售订阅?通过摘取和出售器官?通过接受独裁者的捐款?当然不是(?)。他们卖广告。让我告诉你,元宇宙可能会以非常相似的方式运行。我想我们可以称之为DNA。你认为Instagram的广告很具有针对性且很烦人吗?那是因为你还没有看到他们可以用整个数据集做什么事,并且能够真正地在无处不在地关注到你。

想象一下,你在一个数字空间中走来走去,此时在现实世界的你很饿。不知不觉中,你盯着沿途的数字咖啡厅和餐馆看的时间长了很久。然后您猜怎么着,一分钟后,你开始收到食物的广告。 一开始听起来挺有趣的,但是从长远角度来看,这很干扰。因此,一旦我们丧失了新鲜感,我们就会希望广告拦截软件足够先进以发现嵌入现实中的广告,这就是广告拦截专家发挥作用的地方。

很像AdBlock Plus模式,我猜他们会开发插件来阻止广告弹出。他们不会得到太多报酬,但通过捐赠和获取数据,他们或许能维持生计。

  • 如何成为广告拦截专家:拥有基础编程知识,并能访问元宇宙的源代码。

9. 元宇宙网络安全

元宇宙是网络攻击和诈骗的完美目标:被黑的虚拟形象(avatars)、NFT 盗窃、生物识别/生理数据泄露(脑电波模式,有人在吗?),被黑的耳机……出现问题的可能几乎是无数的。

这就是我们需要元宇宙网络安全专家的原因。他们将实时阻止攻击,并确保法律和协议被重新考虑和修改,甚至再创造法律和协议,以囊括元宇宙所有可能的风险。

笔者对网络安全不甚了解,所以在这一段给出开放式的想法:虚拟世界违规成为现实世界的法案只是时间问题

  • 如何成为元宇宙网络安全专家:具有常规网络安全知识和/或具有技术倾向的法学学位。

10. 无薪实习生

笔者知道,为了得到一个漂亮整数而凑数,有点不光彩。但是请听我说完,虽然这个角色已经存在,但需要强调的是,它将是元宇宙未来的核心。

无薪实习生不仅能喝咖啡,他们研磨数据;他们制作风险投资的附录(VC’s decks’ appendices);他们为岩石和树木写代码;他们是科技帝国赖以建立的必要素材。

我们应该向他们的牺牲致敬。

而且,我们应该付钱给他们。


安全、舒适、引人入胜的叙事曲线、可定制的发型、家长控制、智力游戏、附加组件、小部件、通知、优化、意识形态一致性、娱乐和惩罚门户以及发出低沉隆隆声的传感器(beta版)……我们将在元宇宙中拥有一切!

但这不是一蹴而就的,元宇宙将需要无数新技术、协议、公司、创新和发现才能运作。并且,根据定义,它需要无数人从事不可量计的工作才能实现其宏伟目标。

如果您有幸在不久的将来得到上述之一的工作,只要记住一件事:: 世界是异常黯淡的,请你不要将其黑暗带入元宇宙


作者:用户8309268629399
来源:https://juejin.cn/post/7032552320483524645

收起阅读 »

如果将元宇宙逐层拆解,你会发现内核是“云”

2021 最火的新概念,莫过于元宇宙。2021 年 10 月 29 日,Facebook 宣布改名 Meta;2021 年 11 月 1 日,“元宇宙第一股” Roblox 经过短暂调整,宣布重新上线。接下来关于元宇宙的线下 / 线上讨论如火如荼,元宇宙概...
继续阅读 »

2021 最火的新概念,莫过于元宇宙。2021 年 10 月 29 日,Facebook 宣布改名 Meta;2021 年 11 月 1 日,“元宇宙第一股” Roblox 经过短暂调整,宣布重新上线。接下来关于元宇宙的线下 / 线上讨论如火如荼,元宇宙概念的热度可见一斑。

逐层拆解元宇宙

清华大学新闻学院教授、博士生导师沈阳教授在一场活动中分享道,元宇宙,英文是 Metaverse,从字面来理解,由 Meta(超越) 和 Universe(宇宙) 两部分组成。而沈阳教授团队也给元宇宙下了一个相对精确的定义:

元宇宙是整合多种新技术而产生的新型虚实相融的互联网应用和社会形态,它基于扩展现实技术提供沉浸式体验,基于数字孪生技术生成现实世界的镜像,基于区块链技术搭建经济体系,将虚拟世界与现实世界在经济系统、社交系统、身份系统上密切融合,并且允许每个用户进行内容生产和世界编辑。

Beamable 公司创始人 Jon Radoff 则在产业层面对元宇宙的概念做了拆解:“元宇宙构造的七个层面:体验;发现;创作者经济;空间计算;去中心化;人机互动;基础设施。”

体验层面相对最容易理解,目前我们常见到的游戏、社交等领域企业,都是在体验层面开展工作。著名游戏 《Second Life》 尤为经典。在这个游戏里,用户叫做"居民",可以通过可运动的虚拟化身互相交互。这套程序还在一个通常的元宇宙的基础上提供了一个高层次的社交网络服务。居民们可以四处逛逛,会碰到其他的居民,社交,参加个人或集体活动,制造和相互交易虚拟财产和服务。而典型《头号玩家》则是人们对于元宇宙在体验层面的自由畅想。

发现层面是用户了解到体验层的重要途径,其中包括各种应用商店,主要参与者是大型互联网公司;

创作者经济层 (Creator Economy): 帮助元宇宙创作者的成果货币化,其中包括设计工具 、 货币化技术、动画系统、图形工具等;

空间计算层 (Spital Computing): 对创作者经济层的赋能,具体包括 3D 引擎、手势识别、空 间映射和人工智能等,主要参与者是 3D 软硬件厂商;

去中心化层 (Decentralization): 这个层面的公司主要是帮助元宇宙生态系统构建分布式架 构,从而形成民主化结构;

人机交互层 (Human Interface): 人机交互层主要是大众接触元宇宙的媒介工具,主要体现在 触觉、姿势、声音、神经等层面,其中产品包括 AR/VR、手机、电脑、汽车、智能眼镜等可穿戴设备,主要参与者是 3D 软硬件厂商 ;

基础设施层 (Infrastructure):5G、半导体芯片、新型材料、云计算和电信网络等。基础设施层大概率是巨头之间的游戏,大部分是基础硬件公司。

可以说,元宇宙是整个人类经济体未来需求的一个集中出口,包含了用户对新体验的渴望,资本对新出口的渴望,技术对新领域的渴望,它是科技发展到一定阶段的必然新构想。即便 2021 没有出现“元宇宙”,可能也会出现“元世界”、“元矩阵”等其他概念。

元宇宙的关键支撑技术

以上关于元宇宙概念和产业分层方面的定义,是最近被很多人所熟知的概念,但这仍然没有解释元宇宙的实现路径,说到底,我们最想搞清楚的是,究竟该如何实现梦想中的元宇宙。

从技术维度来看,元宇宙的各部分关键支撑可以简称为:“HNCBD”,分别是硬件体验设备 (Hardware)、网络与算力 (Networking and Computing)、内容及应用生态 (Content)、区块链和 NFT(Blockchain),数字孪生(Digital Twin)。当然,这些核心技术在不同人眼里可能有细微区别,但总体相差不大。

在“HNCBD”中,H 属于硬件,不在软件开发者的常规讨论范围内;C 依赖百花齐放的应用社区;而网络与算力、区块链和 NFT、数字孪生,其实都存在一个统一的承载形式,就是云计算。

明眼人早已看出,如果排除因商业竞争而重复造轮子的问题,其实实现元宇宙最好的通路就是云。某种意义上讲,云不光承载的是元宇宙对于算力和基础设施的空前庞大的需求,更是各类在基础设施之上的 PaaS 、SaaS 服务。在元宇宙的发展过程中,如果每一家应用提供商、内容提供商,都要重构基础设施,包括基础的数据湖仓服务、数字孪生服务、机器学习服务,那成本将是不可想象的。

而当下阶段的云计算,除了提供基础的算力支撑,最关键的就是在游戏、AI 算法及 VR 三个方向上,提供了足够成熟的技术产品,其中最具代表性的就是亚马逊云科技。

回顾《头号玩家》的电影画面,演员们戴上眼镜,即进入了游戏世界,这其实是典型的云游戏场景。

目前大型游戏采用服务器 + 客户端的实现模式,对客户端硬件要求比较高,尤其是 3D 图形的渲染,基本完全依赖于终端运算。随着 5G 时代的到来,游戏将会在云端 GPU 上完成大规模渲染,游戏画面压缩后通过 5G 高速网络传送给用户。

在客户端,用户的游戏设备不需要任何高端处理器和显卡,只需要基本的视频解压能力。从游戏开发角度来看,游戏平台可以更加快速地部署新游戏功能,减少启动游戏所需的构建和测试工作量,满足玩家需求。

2020 年 9 月,亚马逊云科技就推出了自己的云游戏平台 Luna,兼容 PC、Mac、Fire TV、iPad 和 iPhone 和 Android 系统,知名游戏和平台厂商 Epic Games 也在利用 Amazon EC2 等 亚马逊云科技 服务及时扩展容量并支持远程创建者。Amazon G4 实例就是通过 GPU 来驱动云游戏渲染,通过 NVIDIA Video Codec SDK 传输最复杂的云游戏。Amazon G4 实例所搭载的 NVIDIA T4 GPU,也是云上第一款提供了 RT 核心、支持 NVIDIA RTX 实时光线追踪的 GPU 实例。

而元宇宙的体验又不仅限于云游戏,云游戏只是场景,VR 才是路径。

传统 VR 应用的局限性主要体现在四个方面,其中包括:购置主机和终端硬件成本高、设备使用率低、内容分散、移动性受限。

云计算和 VR 的结合,可以将 GPU 渲染功能从本地迁移到云端,从而使得终端的设计变得更加轻便与高性价比,降低了用户购买硬件设备的成本。VR 开发者可以 在云上进行快速的内容迭代发布,用户即点即玩、无需下载,解决内容不集中问题。

以 Amazon Sumerian 为例,开发者可以轻松创建 3D 场景并将其嵌入到现有网页中。Amazon Sumerian 编辑器则提供了现成的场景模板和 直观的拖放工具,使内容创建者、设计师和开发人员都可以构建交互式场景。Amazon Sumerian 采用最新的 WebGL 和 WebXR 标准,可直接在 Web 浏览器中创建沉浸式体验,并可通过简单的 URL 在几秒钟内进行访 问,同时能够在适用于 AR/VR 的主要硬件平台上运行。

除了云游戏和 VR,元宇宙的实现还有一个关键变量,就是 AI。AI 可以缩短数字创作时间,为元宇宙提供底层支持,主要体现在计算机视觉、智能语音语义、机器学习。三者都需要巨大的算力和存储,云计算为人工智能提供了无限的算力和存储支持。有一家叫做 GE Healthcare 的公司,就是使用 Amazon P4d 实例,将定制化 AI 模型处理时间从几天缩短为几小时,使训练模型的速度提高了两三倍,从而提供各类远程医疗、诊断服务。

AI 在虚拟形象上的价值更明显,亚马逊云科技的 AI 服务在此领域有很多的应用实践包括图像 AI 生成(自动上色、场景调整、图像二次元化)、模型自动生成(动画自动生成、场景道具生成)、游戏机器人(游戏 AI NPC、文本交互、语音驱动口型动画、动作补抓、表情迁移)、偶像营销运营(聊天观察、流行搭配、反外挂)等。

云计算企业在元宇宙领域的核心工作

如果说,以上拆解更多还属于理论分析,那么,如果我们仔细看看头部云计算企业的近期动态,就会发现关于元宇宙的种种技术支撑正在云端成为现实。

2021 亚马逊云科技 re:Invent,亚马逊云科技发布了 Amazon IoT TwinMaker 与 Amazon Private 5G。

前者让开发人员可以轻松汇集来自多个来源(如设备传感器、摄像机和业务应用程序)的数据,并将这些数据结合起来创建一个知识图谱,对现实世界环境进行建模,是实现工业元宇宙的组成技术之一。

后者则可自动设置和部署企业专有 5G 网络,并按需扩展容量以支持更多设备和网络流量,重点服务了以工业 4.0 为主的庞大传感器和端侧设备集群,前文提到的工业元宇宙、车联网自然也在同一序列。

更不用说 Amazon SageMaker Canvas,用无代码理念构建机器学习模型,做模型预测,保证在脱离数据工程团队的情况下,依然可以提供服务,进一步降低了未来元宇宙内容生产的门槛,保证了内容的多样性。

同样在 2021 亚马云科技 re:Invent 全球大会期间,元宇宙公司 Meta 宣布深化与亚马逊云科技的合作,将亚马逊云科技作为其战略云服务提供商。

据介绍,Meta 使用亚马逊云科技可靠的基础设施和全面的功能,补充其现有的本地基础设施,并将使用更多亚马逊云科技的计算、存储、数据库和安全服务,获得云端更好的隐私保护、可靠性和扩展性,包括将在亚马逊云科技上运行第三方合作应用,并使用云服务支持其收购的已经在使用亚马逊云科技的企业。

Meta 还将使用亚马逊云科技的计算服务来加速 Meta AI 部门人工智能项目的研发工作。另外,亚马逊云科技和 Meta 双方还将合作帮助客户提高在亚马逊云科技上运行深度学习计算框架 PyTorch 的性能,并助力开发人员加速构建、训练、部署和运行人工智能和机器学习模型的机制。

亚马逊全球副总裁、亚马逊云科技大中华区执行董事张文翊认为,这是云计算可以大量赋能的一个领域。她表示:“我们认为元宇宙一定是云计算可以大量赋能的一个领域。元宇宙本身需要的就是计算、存储、机器学习等,这些都离不开云计算。”

未来仍在描绘中

未来元宇宙的技术栈是否会扩展,元宇宙的呈现形式是否会出现大幅变化?

答案几乎是肯定的,就像在 4G 手机普及以前,我们完全无法想象 4G 生态下主要的应用类型。从建设到成熟,仅在算力层面,元宇宙也至少还有十余年时间的路程要走。

但万变不离其宗,关注云计算领域关于元宇宙支撑技术的更新迭代,可能是我们抛开泡沫,观察元宇宙生态进展的重要方法。


作者:SegmentFault思否
来源:https://juejin.cn/post/7041125661465346062

收起阅读 »

前端 4 种渲染技术的计算机理论基础

前端可用的渲染技术有 html + css、canvas、svg、webgl,我们会综合运用这些技术来绘制页面。有没有想过这些技术有什么区别和联系,它们和图形学有什么关系呢?本文我们就来谈一下网页渲染技术的计算机理论基础。渲染的理论基础人眼的视网膜有视觉暂留机...
继续阅读 »

前端可用的渲染技术有 html + css、canvas、svg、webgl,我们会综合运用这些技术来绘制页面。有没有想过这些技术有什么区别和联系,它们和图形学有什么关系呢?

本文我们就来谈一下网页渲染技术的计算机理论基础。

渲染的理论基础

人眼的视网膜有视觉暂留机制,也就是看到的图像会继续保留 0.1s 左右,图形界面就是根据这个原理来设计的一帧一帧刷新的机制,要保证 1s 至少要渲染 10 帧,这样人眼看到画面才是连续的。

每帧显示的都是图像,它是由像素组成的,是显示的基本单位。不同显示器实现像素的原理不同。

我们要绘制的目标是矩形、圆形、椭圆、曲线等各种图形,绘制完之后要把它们转成图像。图形的绘制有一系列的理论,比如贝塞尔曲线是画曲线的理论。图形转图像的过程叫做光栅化。这些图形的绘制和光栅化的过程,都是图形学研究的内容。

图形可能做缩放、平移、旋转等变化,这些是通过矩阵计算来实现的,也是图形学的内容。

除了 2D 的图形外,还要绘制 3D 的图形。3D 的原理是把一个个三维坐标的顶点连起来,构成一个一个三角形,这是造型的过程。之后再把每一个三角形的面贴上图,叫做纹理。这样组成的就是一个 3D 图形,也叫 3D 模型。

3D 图形也同样需要经历光栅化变成二维的图像,然后显示出来。这种三维图形的光栅化需要找一个角度去观察,就像拍照一样,所以一般把这个概念叫做相机。

同时,为了 3D 图形更真实,还引入了光线的概念,也就是一束光照过来,3D 图形的每个面都会有什么变化,怎么反射等。不同材质的物体反射的方式不同,比如漫反射、镜面反射等,也就有不同的计算公式。一束光会照射到一些物体,到物体的反射,这个过程需要一系列跟踪的计算,叫做光线追踪技术。

我们也能感受出来,3D 图形的计算量比 2D 图形大太多了,用 CPU 计算很可能达不到 1s 大于 10 帧,所以后面出现了专门用于 3D 渲染加速的硬件,叫做 GPU。它是专门用于这种并行计算的,可以批量计算一堆顶点、一堆三角形、一堆像素的光栅化,这个渲染流程叫做渲染管线。

现在的渲染管线都是可编程的,也就是可以控制顶点的位置,每个三角形的着色,这两种分别叫做顶点着色器(shader)、片元着色器。

总之,2D 或 3D 的图形经过绘制和光栅化就变成了一帧帧的图像显示出来。

变成图像之后其实还可以做一些图像处理,比如灰度、反色、高斯模糊等各种滤镜的实现。

所以,前端的渲染技术的理论基础是计算机图形学 + 图像处理。

不同的渲染技术的区别和联系

具体到前前端的渲染技术来说,html+css、svg、canvas、webgl 都是用于图形和图像渲染的技术,但是它们各有侧重:

html + css

html + css 是用于图文布局的,也就是计算文字、图片、视频等的显示位置。它提供了很多计算规则,比如流式布局很适合做图文排版,弹性布局易于做自适应的布局等。但是它不适合做更灵活的图形绘制,这时就要用其他几种技术了。

canvas

canvas 是给定一块画布区域,在不同的位置画图形和图像,它没有布局规则,所以很灵活,常用来做可视化或者游戏的开发。但是 canvas 并不会保留绘制的图形的信息,生成的图像只能显示在固定的区域,当显示区域变大的时候,它不能跟随一起放缩,就会失真,如果有放缩不失真的需求就要用其他渲染技术了。

svg

svg 会在内存中保留绘制的图形的信息,显示区域变化后会重新计算,是一个矢量图,常用于 icon、字体等的绘制。

webgl

上面的 3 种技术都是用于 2D 的图形图像的绘制,如果想绘制 3D 的内容,就要用 webgl 了。它提供了绘制 3D 图形的 api,比如通过顶点构成 3D 的模型,给每一个面贴图,设置光源,然后光栅化成图像等的 api。它常用于通过 3D 内容增强网站的交互效果,3D 的可视化,3D 游戏等,再就是虚拟现实中的 3D 交互。

所以,虽然前端渲染技术的底层原理都是图形学 + 图像处理,但上层提供的 4 种渲染技术各有侧重点。

不过,它们还是有很多相同的地方的:

  • 位置、大小等的变化都是通过矩阵的计算

  • 都要经过图形转图像,也就是光栅化的过程

  • 都支持对图像做进一步处理,比如各种滤镜

  • html + css 渲染会分不同图层分别做计算,canvas 也会根据计算量分成不同的 canvas 来做计算

因为他们底层的图形学原理还是一致的。

除此以外,3D 内容,也就是 webgl 的内容会通过 GPU 来计算,但 css 其实也可以通过 GPU 计算,这叫做 css 的硬件加速,有四个属性可以触发硬件加速:transform、opacity、filter、will-change。(更多的 GPU 和 css 硬件加速的内容可以看这篇文章:这一次,彻底搞懂 GPU 和 css 硬件加速

编译原理的应用

除了图形学和图像技术外,html+css 还用到了编译技术。因为 html、css 是一种 DSL( domin specific language,领域特定语言),也就是专门为界面描述所设计的语言。用 html 来表达 dom 结构,用 css 来给 dom 添加样式都只需要很少的代码,然后运行时解析 html 和 css 来创建 dom、添加样式。

DSL 可以让特定领域的逻辑更容易表达,前端领域还有一些其他技术也用到了 DSL,比如 graphql。

总结

因为人眼的视觉暂留机制,只要每帧绘制不超过 0.1s,人看到的画面就是连续的,这是显示的原理。每帧的绘制要经过图形绘制和图形转图像的光栅化过程,2D 和 3D 图形分别有不同的绘制和光栅化的算法。此外,转成图像之后还可以做进一步的图像处理。

前端领域的四种渲染技术:html+css、canvas、svg、webgl 各有侧重点,分别用于不同内容的渲染:

  • html+ css 用于布局

  • canvas 用于灵活的图形图像渲染

  • svg 用于矢量图渲染

  • webgl 用于 3D 图形的渲染

但他们的理论基础都是计算机图形学 + 图像处理。(而且,html+css 为了方便逻辑的表达,还设计了 DSL,这用到了编译技术)

这四种渲染技术看似差别很大,但在理论基础层面,很多东西都是一样的。这也是为什么我们要去学计算机基础,因为它可以让我们对技术有一个更深入的更本质的理解。


作者:zxg_神说要有光
来源:https://juejin.cn/post/7041157165024804895

收起阅读 »

如何用 docker 打造前端开发环境

用 docker 做开发环境的好处 保持本机清爽 做开发的都知道,电脑一买回来就要安装各种各样的环境,比如前端开发需要安装 node、yarn、git 等,为了使用某些工具或者包,可能还需要安装 python 或者 java 等(比如 jenkins 就依赖了...
继续阅读 »

用 docker 做开发环境的好处


保持本机清爽


做开发的都知道,电脑一买回来就要安装各种各样的环境,比如前端开发需要安装 node、yarn、git 等,为了使用某些工具或者包,可能还需要安装 python 或者 java 等(比如 jenkins 就依赖了 java),久而久之,本机环境会非常乱,对于一些强迫证患者或者有软件洁癖的人来说多少有点不爽。


使用 docker 后,开发环境都配置在容器中,开发时只需要打开 docker,开发完后关闭 docker,本机不用再安装乱七八糟的环境,非常清爽。


隔离环境


不知道大家在开发时有没有遇到这种情况:公司某些项目需要在较新的 node 版本上运行(比如 vite,需要在 node12 或以上),某些老的项目需要在较老的 node 版本上运行,切换起来比较麻烦,虽然可以用 nvm 来解决,但使用 docker 可以更方便的解决此问题。


快速配置环境


买了新电脑,或者重装了系统,又或者换了新的工作环境,第一件事就是配置开发环境。下载 node、git,然后安装一些 npm 的全局包,然后下载 vscode,配置 vscode,下载插件等等……


使用 docker 后,只需从 docker hub 中拉取事先打包好的开发环境镜像,就可以愉快的进行开发了。


安装 docker


到 docker 官网(http://www.docker.com)下载 docker desktop 并安装,此步比较简单,省略。


安装完成,打开 docker,待其完全启动后,打开 shell 输入:


docker images

截屏2021-09-29 22.16.13.png


显示上述信息即成功!


配置开发环境


假设有一个项目,它必须要运行在 8.14.0 版本的 node 中,我们先去 docker hub 中将这个版本的 node 镜像拉取下来:


docker pull node:8.14.0

拉取完成后,列出镜像列表:


docker images

截屏2021-09-29 23.30.16.png


有了镜像后,就可以使用镜像启动一个容器:


docker run -it --name my_container 3b7ecd51 /bin/bash

上面的命令表示以命令行交互的模式启动一个容器,并将容器的名称指定为 my_container。


截屏2021-10-08 23.16.11.png


此时已经新建并进入到容器,容器就是一个 linux 系统,可以使用 linux 命令,我们尝试输入一些命令:


截屏2021-10-08 23.18.11.png


可以看到这个 node 镜像除了预装了 node 8.14.0,还预装了 git 2.11.0。



镜像和容器的的关系:镜像只预装了最基本的环境,比如上面的 node:8.14.0 镜像可以看成是预装了 node 8.14.0 的 linux 系统,而容器是基于镜像克隆出来的另一个 linux 系统,可以在这个系统中安装其它环境比如 java、python 等,一个镜像可以建立多个容器,每个容器环境都是相互隔离的,互不影响(比如在容器 A 中安装了 java,容器 B 是没有的)。



使用命令行操作项目并不方便,所以我们先退出命令行模式,使用 exit 退出:


截屏2021-10-08 23.20.49.png


借助 IDE 可以更方便的玩 docker,这里我们选择 vscode,打开 vscode,安装 Remote - Containers 扩展,这个扩展可以让我们更方便的管理容器:


截屏2021-10-08 23.03.53.png


安装成功后,左下角会多了一个图标,点击:


截屏2021-10-08 23.23.00.png


在展开菜单中选择“Attach to Running Container”:


截屏2021-10-08 23.25.02.png


此时会报一个错“There are no running containers to attach to.”,因为我们刚刚退出了命令行交互模式,所以现在容器是处理停止状态的,我们可以使用以下命令来查看正在运行的容器:


docker ps

# 或者
docker container ls

截屏2021-10-08 23.30.51.png


发现列表中并没有正在运行的容器,我们需要找到刚刚创建的容器并将其运行起来,先显示所有容器列表:


# -a 可以显示所有容器,包括未运行的
docker ps -a

# 或者
docker container ls -a

截屏2021-10-08 23.29.25.png


运行指定容器:


# 使用容器名称
docker start my_container

# 或者使用容器 id,id 只需输入前几位,docker 会自动识别
docker start 8ceb4

再次运行 docker ps 命令后,就可以看到已运行的容器了。然后回到 vscode,再次选择"Attach to Running Container",就会出现正在运行的容器列表:


截屏2021-10-08 23.36.14.png


选择容器进入,添加一个 bash 终端,就可以进入我们刚刚的命令行模式:


截屏2021-10-08 23.40.14.png


我们安装 vue-cli,并在 /home 目录下创建一个项目:


# 安装 vue-cli
npm install -g @vue/cli

# 进入到 home 目录
cd /home

# 创建 vue 项目
vue create demo

在 vscode 中打开目录,发现打开的不再是本机的目录,而是容器中的目录,找到我们刚刚创建的 /home/demo 打开:


截屏2021-10-09 00.01.13.png


输入 npm run serve,就可以愉快的进行开发啦:


截屏2021-10-09 00.03.32.png


上面我们以 node 8.14.0 镜像为例创建了一个开发环境,如果想使用新版的 node 也是一样的,只需要将指定版本的 node 镜像 pull 下来,然后使用这个镜像创建一个容器,并在容器中创建项目或者从 git 仓库中拉取项目进行开发,这样就有了两个不同版本的 node 开发环境,并且可以同时进行开发。


使用 ubuntu 配置开发环境


上面这种方式使用起来其实并不方便,因为 node 镜像只安装了 node 和 git,有时我们希望镜像可以内置更多功能(比如预装 nrm、vue-cli 等 npm 全局包,或者预装好 vscode 的扩展等),这样用镜像新建的容器也包含这些功能,不需要每个容器都要安装一次。


我们可以使用 ubuntu 作为基础自由配置开发环境,首先获取 ubuntu 镜像:


# 不输入版本号,默认获取 latest 即最新版
docker pull ubuntu

新建一个容器:


docker run -itd --name fed 597ce /bin/bash

这里的 -itd 其实是 -i -t -d 的合写,-d 是在后台中运行容器,相当于新建时一并启动容器,这样就不用使用 docker start 命令了。后面我们直接用 vscode 操作容器,所以也不需要使用命令行模式了。


我们将容器命名为 fed(表示 front end development),建议容器的名称简短一些,方便输入。


截屏2021-10-10 00.11.40.png


ubuntu 镜像非常纯净(只有 72m),只具备最基本的能力,为了后续方便使用,我们需要更新一下系统,更新前为了速度快一点,先换到阿里的源,用 vscode 打开 fed 容器,然后打开 /etc/apt/sources.list 文件,将内容改为:


deb http://mirrors.aliyun.com/ubuntu/ focal main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ focal-security main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ focal-updates main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ focal-proposed main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ focal-backports main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal-security main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal-updates main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal-proposed main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal-backports main restricted universe multiverse

截屏2021-10-10 00.18.57.png


在下面的终端中依次输入以下命令更新系统:


apt-get update
apt-get upgrade

安装 sudo:


apt-get install sudo

安装 git:


apt-get install git

安装 wget(wget 是一个下载工具,我们需要用它来下载软件包,当然也可以选择 axel,看个人喜好):


apt-get install wget

为了方便管理项目与软件包,我们在 /home 目录中创建两个文件夹(projects 与 packages),projects 用于存放项目,packages 用于存放软件包:


cd /home
mkdir projects
mkdir packages

由于 ubuntu 源中的 node 版本比较旧,所以从官网中下载最新版,使用 wget 下载 node 软件包:


# 将 node 放到 /home/packages 中
cd /home/packages

# 需要下载其它版本修改版本号即可
wget https://nodejs.org/dist/v14.18.0/node-v14.18.0-linux-x64.tar

解压文件:


tar -xvf node-v14.18.0-linux-x64.tar

# 删除安装包
rm node-v14.18.0-linux-x64.tar

# 改个名字,方便以后切换 node 版本
mv node-v14.18.0-linux-x64 node

配置 node 环境变量:


# 修改 profile 文件
echo "export PATH=/home/packages/node/bin:$PATH" >> /etc/profile

# 编译 profile 文件,使其生效
source /etc/profile

# 修改 ~.bashrc,系统启动时编译 profile
echo "source /etc/profile" >> ~/.bashrc

# 之后就可以使用 node 和 npm 命令了
node -v
npm -v

安装 nrm,并切换到 taobao 源:


npm install nrm -g
nrm use taobao

安装一些 vscode 扩展,比如 eslint、vetur 等,扩展是安装在容器中的,在容器中会保留一份配置文件,到时打包镜像会一并打包进去。当我们关闭容器后再打开 vscode,可以发现本机的 vscode 中并没有安装这些扩展。


至此一个简单的前端开发环境已经配置完毕,可以根据自己的喜好自行添加一些包,比如 yarn、nginx、vim 等。


打包镜像


上面我们通过 ubuntu 配置了一个简单的开发环境,为了复用这个环境,我们需要将其打包成镜像并推送到 docker hub 中。


第一步:先到 docker 中注册账号。


第二步:打开 shell,登录 docker。


截屏2021-10-10 01.44.26.png


第三步:将容器打包成镜像。


# commit [容器名称] [镜像名称]
docker container commit fed fed

第四步:为镜像打 tag,因为镜像推送到 docker hub 中,要用 tag 来区分版本,这里我们先设置为 latest。tag 名称加上了用户名做命名空间,防止与 docker hub 上的镜像冲突。


docker tag fed huangzhaoping/fed:latest

第五步:将 tag 推送至 docker hub。


docker push huangzhaoping/fed:latest

第六步:将本地所有关于 fed 的镜像和容器删除,然后从 docker hub 中拉取刚刚推送的镜像:


# 拉取
docker pull huangzhaoping/fed

# 创建容器
docker run -itd --name fed huangzhaoping/fed /bin/bash

用 vscode 打开容器,打开命令行,输入:


node -v
npm -v
nrm -V
git --version

然后再看看 vscode 扩展,可以发现扩展都已经安装好了。


如果要切换 node 版本,只需要下载指定版本的 node,解压替换掉 /home/packages/node 即可。


至此一个 docker 开发环境的镜像就配置完毕,可以在不同电脑,不同系统中共享这个镜像,以达到快速配置开发环境的目的。


注意事项



  • 如果要将镜像推送到 docker hub,不要将重要的信息保存到镜像中,因为镜像是共享的,避免重要信息泄露。

  • 千万不要在容器中存任何重要的文件或信息,因为容器一旦误删这些文件也就没了。

  • 如果在容器中开发项目,记得每天提交代码到远程仓库,避免容器误删后代码丢失。

作者:Rise_
链接:https://juejin.cn/post/7017129520649994253

收起阅读 »

【手写代码】面试官:请你手写防抖和节流

一、前言当用户高频触发某一事件时,如窗口的resize、scroll,输入框内容校验等,此时这些事件调用函数的频率如果没有限制,可能会导致响应跟不上触发,出现页面卡顿,假死现象。此时,我们可以采用 防抖(debounce) 和 节...
继续阅读 »

一、前言

当用户高频触发某一事件时,如窗口的resize、scroll,输入框内容校验等,此时这些事件调用函数的频率如果没有限制,可能会导致响应跟不上触发,出现页面卡顿,假死现象。此时,我们可以采用 防抖(debounce) 和 节流(throttle) 的方式来减少调用频率,同时又不影响实际效果。

二、防抖

假设你用手压住一个弹簧,那么弹簧不会弹起来,除非你松手。

函数防抖,就是指触发事件后,函数在 n 秒后只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数的执行时间。

简单的说,当一个函数连续触发,只执行最后一次。

函数防抖一般用在什么情况之下呢?一般用在,连续的事件只需触发一次回调的场合。具体有:

  1. 搜索框搜索输入。只需用户最后一次输入完,再发送请求;
  2. 用户名、手机号、邮箱输入验证;
  3. 浏览器窗口大小改变后,只需窗口调整完后,再执行resize事件中的代码,防止重复渲染。

代码实现

在下面这段代码中,我们实现了最简单的一个防抖函数,我们设置一个定时器,你重复调用一次函数,我们就清除定时器,重新定时,直到在设定的时间段内没有重复调用函数。

// fn是你要调用的函数,delay是防抖的时间
function debounce(fn, delay) {
// timer是一个定时器
let timer = null;
// 返回一个闭包函数,用闭包保存timer确保其不会销毁,重复点击会清理上一次的定时器
return function () {
// 调用一次就清除上一次的定时器
clearTimeout(timer);
// 开启这一次的定时器
timer = setTimeout(() => {
fn();
}, delay)
}
}

代码优化

仔细一想,上面的代码是不是有什么问题?

问题一: 我们返回的fn函数,如果需要事件参数e怎么办?事件参数被debounce函数保存着,如果不把事件参数给闭包函数,若fn函数需要e我们没给,代码毫无疑问会报错。

问题二: 我们怎么确保调用fn函数的对象是我们想要的对象?你发现了吗,在上面这段代码中fn()函数的调用者是fn所定义的环境,这里涉及this指向问题,想要了解为什么可以去了解下js中的this。

为了解决上述两个问题,我们对代码优化如下

// fn是你要调用的函数,delay是防抖的时间
function debounce(fn, delay) {
// timer是一个定时器
let timer = null;
// 返回一个闭包函数,用闭包保存timer确保其不会销毁,重复点击会清理上一次的定时器
return function () {
// 保存事件参数,防止fn函数需要事件参数里的数据
let arg = arguments;
// 调用一次就清除上一次的定时器
clearTimeout(timer);
// 开启这一次的定时器
timer = setTimeout(() => {
// 若不改变this指向,则会指向fn定义环境
fn.apply(this, arg);
}, delay)
}
}

三、节流

当水龙头的水一直往下流,这十分的浪费水,所以我们可以把龙头关小一点,让水一滴一滴往下流,每隔一段时间掉下来一滴水。

节流就是限制一个函数在一段时间内只能执行一次,过了这段时间,在下一段时间又可以执行一次。应用场景如:

  1. 输入框的联想,可以限定用户在输入时,只在每两秒钟响应一次联想。
  2. 搜索框输入查询,如果用户一直在输入中,没有必要不停地调用去请求服务端接口,等用户停止输入的时候,再调用,设置一个合适的时间间隔,有效减轻服务端压力。
  3. 表单验证
  4. 按钮提交事件。

代码实现1(时间戳版)

// 方法一:时间戳
function throttle(fn, delay = 1000) {
// 记录第一次的调用时间
var prev = null;
console.log(prev);
// 返回闭包函数
return function () {
// 保存事件参数
var args = arguments;
// 记录现在调用的时间
var now = Date.now();
// console.log(now);
// 如果间隔时间大于等于设置的节流时间
if (now - prev >= delay) {
// 执行函数
fn.apply(this, args);
// 将现在的时间设置为上一次执行时间
prev = now;
}
}
}

触发事件时立即执行,以后每过delay秒之后才执行一次,并且最后一次触发事件若不满足要求不会被执行

代码实现2(定时器版)

// 方法二:定时器
function throttle(fn, delay) {
// 重置定时器
let timer = null;
// 返回闭包函数
return function () {
// 记录事件参数
let args = arguments;
// 如果定时器为空
if (!timer) {
// 开启定时器
timer = setTimeout(() => {
// 执行函数
fn.apply(this, args);
// 函数执行完毕后重置定时器
timer = null;
}, delay);
}
}
}

第一次触发时不会执行,而是在delay秒之后才执行,当最后一次停止触发后,还会再执行一次函数。

代码实现3(时间戳 & 定时器)

// 方法三:时间戳 & 定时器
function throttle(fn, delay) {
// 初始化定时器
let timer = null;
// 上一次调用时间
let prev = null;
// 返回闭包函数
return function () {
// 现在触发事件时间
let now = Date.now();
// 触发间隔是否大于delay
let remaining = delay - (now - prev);
// 保存事件参数
const args = arguments;
// 清除定时器
clearTimeout(timer);
// 如果间隔时间满足delay
if (remaining <= 0) {
// 调用fn,并且将现在的时间设置为上一次执行时间
fn.apply(this, args);
prev = Date.now();
} else {
// 否则,过了剩余时间执行最后一次fn
timer = setTimeout(() => {
fn.apply(this, args)
}, delay);
}
}
}


原文链接:https://juejin.cn/post/7040633388625035272


收起阅读 »

vue工程师必须学会封装的埋点指令思路

vue
前言 最近项目中需要做埋点功能,梳理下产品的埋点文档,发现点击埋点的场景比较多。因为使用的是阿里云sls日志服务去埋点,所以通过使用手动侵入代码式的埋点。定好埋点的形式后,技术实现方法也有很多,哪种比较好呢? 稍加思考... 决定封装个埋点指令,这样使用起来...
继续阅读 »

前言


最近项目中需要做埋点功能,梳理下产品的埋点文档,发现点击埋点的场景比较多。因为使用的是阿里云sls日志服务去埋点,所以通过使用手动侵入代码式的埋点。定好埋点的形式后,技术实现方法也有很多,哪种比较好呢?


稍加思考...



决定封装个埋点指令,这样使用起来会比较方便,因为指令的颗粒度比较细能够直击要害,挺适合上面所说的业务场景。


指令基础知识


在此之前,先来复习下vue自定义指令吧,这里只介绍常用的基础知识。更全的介绍可以查看官方文档


钩子函数




  • bind:只调用一次,指令第一次绑定到元素时调用。




  • inserted:被绑定元素插入父节点时调用。




  • update:所在组件的 VNode 更新时调用。




钩子函数参数



  • el:指令所绑定的DOM元素。

  • binding:一个对象,包含以下 property:

    • value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2。

    • arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"。



  • vnode:指令所绑定的当前组件vnode。


在这里分享个小技巧,钩子函数参数中没有可以直接获取当前实例的参数,但可以通过 vnode.context 获取到,这个在我之前的vue技巧文章中也有分享到,有兴趣可以去看看。


正文


进入正题,下面会介绍埋点指令的使用,内部是怎么实现的。


用法与思路


一般我在封装一个东西时,会先确定好它该怎么去用,然后再从用法入手去封装。这样会令整个思路更加清晰,在定义用法时也可以思考下易用性,不至于封装完之后因为用法不理想而返工。


埋点上报的数据会分为公共数据(每个埋点都要上报的数据)和自定义数据(可选的额外数据,和公共数据一起上报)。那么公共数据在内部就进行统一处理,对于自定义数据则需要从外部传入。于是有了以下两种用法:



  • 一般用法


<div v-track:clickBtn></div>


  • 自定义数据


<div v-track:clickBtn="{other:'xxx'}"></div>

可以看到埋点事件是通过 arg 的形式传入,在此之前也看到有些小伙伴封装的埋点事件是在 value 传入。但我个人比较喜欢 arg 的形式,这种更能让人一目了然对应的埋点事件是什么。


另外上报数据结构大致为:


{   
eventName: 'clickBtn'
userId: 1,
userName: 'xxx',
data: {
other: 'xxx'
}
}

eventName 是埋点对应的事件名,与之同级的是公共数据,而自定义数据放在 data 内。


实现


定义一个 track.js 的文件


import SlsWebLogger from 'js-sls-logger'

function getSlsWebLoggerInstance (options = {}) {
return new SlsWebLogger({
host: '***',
project: '***',
logstore: `***`,
time: 10,
count: 10,
...options
})
}

export default {
install (Vue, {baseData = {}, slsOptions = {}) {
const slsWebLogger = getSlsWebLoggerInstance(slsOptions)
// 获取公共数据的方法
let getBaseTrackData = typeof baseData === 'function' ? baseData : () => baseData
let baseTrackData = null
const Track = {
name: 'track',
inserted (el, binding) {
el.addEventListener('click', () => {
if (!binding.arg) {
console.error('Track slsWebLogger 事件名无效')
return
}
if (!baseTrackData) {
baseTrackData = getBaseTrackData()
}
baseTrackData.eventName = binding.arg
// 自定义数据
let trackData = binding.value || {}
const submitData = Object.assign({}, baseTrackData, {data: trackData})
// 上报
slsWebLogger.send(submitData)
if (process.env.NODE_ENV === 'development') {
console.log('Track slsWebLogger', submitData)
}
})
}
}
Vue.directive(Track.name, Track)
}
}

封装比较简单,主要做了两件事,首先是为绑定指令的 DOM 添加 click 事件,其次处理上报数据。在封装埋点指令时,公共数据通过baseData传入,这样可以增加通用性,第二个参数是上报平台的一些配置参数。


在初始化时注册指令:


import store from 'src/store'
import track from 'Lib/directive/track'

function getBaseTrackData () {
let userInfo = store.state.User.user_info
// 公共数据
const baseTrackData = {
userId: userInfo.user_id, // 用户id
userName: userInfo.user_name // 用户名
}
return baseTrackData
}

Vue.use(track, {baseData: getBaseTrackData})

Vue.use 时会自动寻找 install 函数进行调用,最终在全局注册指令。


加点通用性


除了点击埋点之外,如果有停留埋点等场景,上面的指令就不适用了。为此,可以增加手动调用的形式。


export default {
install (Vue, {baseData = {}, slsOptions = {}) {
// ...
Vue.directive(Track.name, Track)
// 手动调用
Vue.prototype.slsWebLogger = {
send (trackData) {
if (!trackData.eventName) {
console.error('Track slsWebLogger 事件名无效')
return
}
const submitData = Object.assign({}, getBaseTrackData(), trackData)
slsWebLogger.send(submitData)
if (process.env.NODE_ENV === 'development') {
console.log('Track slsWebLogger', submitData)
}
}
}
}

这种挂载到原型的方式可以在每个组件实例上通过 this 方便进行调用。


export default {
// ...
created () {
this.slsWebLogger.send({
//...
})
}
}

总结


本文分享了封装埋点指令的过程,封装并不难实现。主要有两种形式,点击埋点通过绑定 DOM click 事件监听点击上报,而其他场景下提供手动调用的方式。主要是想记录下封装的思路,以及使用方式。埋点实现也是根据业务做了一些调整,比如注册埋点指令可以接受上报平台的配置参数。毕竟人是活的,代码是死的。只要能满足业务需求并且能维护,怎么使用舒服怎么来嘛。


作者:出来吧皮卡丘
链接:https://juejin.cn/post/7040649951923142687

收起阅读 »

Android中的类加载器

类的生命周期加载阶段加载阶段可以细分如下加载类的二进制流数据结构转换,将二进制流所代表的静态存储结构转化成方法区的运行时的数据结构生成java.lang.Class对象,作为方法区这个类的各种数据的访问入口加载类的二进制流的方法从zip包中读取。我们常见的JA...
继续阅读 »

类的生命周期

image.png

加载阶段

加载阶段可以细分如下

  • 加载类的二进制流
  • 数据结构转换,将二进制流所代表的静态存储结构转化成方法区的运行时的数据结构
  • 生成java.lang.Class对象,作为方法区这个类的各种数据的访问入口

加载类的二进制流的方法

  • 从zip包中读取。我们常见的JAR、AAR依赖
  • 运行时动态生成。我们常见的动态代理技术,在java.reflect.Proxy中就是用ProxyGenerateProxyClass来为特定的接口生成代理的二进制流
验证

验证是连接阶段的第一步,这一阶段的目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

  1. 文件格式验证:如是否以魔数 0xCAFEBABE 开头、主、次版本号是否在当前虚拟机处理范围之内、常量合理性验证等。
    此阶段保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个 Java类型信息的要求。
  2. 元数据验证:是否存在父类,父类的继承链是否正确,抽象类是否实现了其父类或接口之中要求实现的所有方法,字段、方法是否与父类产生矛盾等。
    第二阶段,保证不存在不符合 Java 语言规范的元数据信息。
  3. 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。例如保证跳转指令不会跳转到方法体以外的字节码指令上。
  4. 符号引用验证:在解析阶段中发生,保证可以将符号引用转化为直接引用。

可以考虑使用 -Xverify:none 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

准备

类变量分配内存并设置类变量初始值,这些变量所使用的内存都将在方法区中进行分配。

解析

虚拟机将常量池内的符号引用替换为直接引用的过程。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行

初始化

到初始化阶段,才真正开始执行类中定义的 Java 程序代码,此阶段是执行 () 方法的过程。

类加载的时机

虚拟机规范规定了有且只有 5 种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始)

  1. 遇到new、getstatic 和 putstatic 或 invokestatic 这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。对应场景是:使用 new 实例化对象、读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)、以及调用一个类的静态方法。

  2. 对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

  3. 当初始化类的父类还没有进行过初始化,则需要先触发其父类的初始化。(而一个接口在初始化时,并不要求其父接口全部都完成了初始化)

  4. 虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),
    虚拟机会先初始化这个主类。

  5. 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

注意:

  1. 通过子类引用父类的静态字段,不会导致子类初始化。
  2. 通过数组定义来引用类,不会触发此类的初始化。MyClass[] cs = new MyClass[10];
  3. 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。

类加载器

把实现类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作的代码模块称为“类加载器”。

将 class 文件二进制数据放入方法区内,然后在堆内(heap)创建一个 java.lang.Class 对象,Class 对象封装了类在方法区内的数据结构,并且向开发者提供了访问方法区内的数据结构的接口。

类的唯一性

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性。

即使两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类也不相等。
这里所指的“相等”,包括代表类的 Class 对象的 equals() 方法、 isAssignableFrom() 方法、isInstance() 方法的返回结果,也包括使用 instanceof 关键字做对象所属关系判定等情况

双亲委托机制

image.png

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

    protected Class loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
//先从缓存中加没加载这个类
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
//从parent中加载
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
//加载不到,就自己加载
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
return c;
}


好处
  • 避免重复加载,当父加载器已经加载了该类的时候,就没有必要子ClassLoader再加载一次。
  • 安全性考虑,防止核心API库被随意篡改。

Android中ClassLoader

image.png

  • ClassLoader是一个抽象类,定义了ClassLoader的主要功能
  • BootClassLoader是ClassLoader的子类(注意不是内部类,有些材料上说是内部类,是不对的),用于加载一些系统Framework层级需要的类,是Android平台上所有的ClassLoader的最终parent
  • SecureClassLoader扩展了ClassLoader类,加入了权限方面的功能,加强了安全性
  • URLClassLoader继承SecureClassLoader,用来通过URI路径从jar文件和文件夹中加载类和资源,在Android中基本无法使用
  • BaseDexClassLoader是实现了Android ClassLoader的大部分功能
  • PathClassLoader加载应用程序的类,会加载/data/app目录下的dex文件以及包含dex的apk文件或者java文件(有些材料上说他也会加载系统类,我没有找到,这里存疑)
  • DexClassLoader可以加载自定义dex文件以及包含dex的apk文件或jar文件,支持从SD卡进行加载。我们使用插件化技术的时候会用到
  • InMemoryDexClassLoader用于加载内存中的dex文件

ClassLoader的加载流程源码分析

-> ClassLoader.java 类

protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized(this.getClassLoadingLock(name)) {
//先查找class是否已经加载过,如果加载过直接返回
Class c = this.findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();

try {
if (this.parent != null) {
//委托给parent加载器进行加载 ClassLoader parent;
c = this.parent.loadClass(name, false);
} else {
//当执行到顶层的类加载器时,parent = null
c = this.findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException var10) {
}

if (c == null) {
long t1 = System.nanoTime();
c = this.findClass(name);
PerfCounter.getParentDelegationTime().addTime(t1 - t0);
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
//如果parent加载器中没有找到,
PerfCounter.getFindClasses().increment();
}
}

if (resolve) {
this.resolveClass(c);
}

return c;
}
}

由子类实现

protected Class findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}

BaseDexClassLoader类中findClass方法

protected Class findClass(String name) throws ClassNotFoundException {
List suppressedExceptions = new ArrayList();
// pathList是DexPathList,是具体存放代码的地方。
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException(
"Didn't find class "" + name + "" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}

public Class findClass(String name, List suppressed) {
for (Element element : dexElements) {
Class clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}

if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}

public Class findClass(String name, ClassLoader definingContext,
List suppressed) {
return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed) : null;
}

public Class loadClassBinaryName(String name, ClassLoader loader, List suppressed) {
return defineClass(name, loader, mCookie, this, suppressed);
}

private static Class defineClass(String name, ClassLoader loader, Object cookie,
DexFile dexFile, List suppressed) {
Class result = null;
try {
result = defineClassNative(name, loader, cookie, dexFile);
} catch (NoClassDefFoundError e) {
if (suppressed != null) {
suppressed.add(e);
}
} catch (ClassNotFoundException e) {
if (suppressed != null) {
suppressed.add(e);
}
}
return result;
}

// 调用 Native 层代码
private static native Class defineClassNative(String name, ClassLoader loader, Object cookie, DexFile dexFile)

image.png

本文转自 juejin.cn/post/703847…,如有侵权,请联系删除。


作者:Gaga246
链接:https://juejin.cn/post/7040625001266937869
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

Android 多线程-IntentService详解

IntentService 一、IntentService概述   上一篇我们聊到了HandlerThread,本篇我们就来看看HandlerThread在IntentService中的应用,看本篇前建议先看看上篇的HandlerThread,有助于我们更好掌...
继续阅读 »

IntentService


一、IntentService概述


  上一篇我们聊到了HandlerThread,本篇我们就来看看HandlerThread在IntentService中的应用,看本篇前建议先看看上篇的HandlerThread,有助于我们更好掌握IntentService。同样地,我们先来看看IntentService的特点:



  • 它本质是一种特殊的Service,继承自Service并且本身就是一个抽象类

  • 它可以用于在后台执行耗时的异步任务,当任务完成后会自动停止

  • 它拥有较高的优先级,不易被系统杀死(继承自Service的缘故),因此比较适合执行一些高优先级的异步任务

  • 它内部通过HandlerThread和Handler实现异步操作

  • 创建IntentService时,只需实现onHandleIntent和构造方法,onHandleIntent为异步方法,可以执行耗时操作


二、IntentService的常规使用套路


  大概了解了IntentService的特点后,我们就来了解一下它的使用方式,先看个案例:

IntentService实现类如下:


package com.zejian.handlerlooper;

import android.app.IntentService;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.IBinder;
import android.os.Message;

import com.zejian.handlerlooper.util.LogUtils;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;

/**
* Created by zejian
* Time 16/9/3.
* Description:
*/
public class MyIntentService extends IntentService {
public static final String DOWNLOAD_URL="download_url";
public static final String INDEX_FLAG="index_flag";
public static UpdateUI updateUI;


public static void setUpdateUI(UpdateUI updateUIInterface){
updateUI=updateUIInterface;
}

public MyIntentService(){
super("MyIntentService");
}

/**
* 实现异步任务的方法
* @param intent Activity传递过来的Intent,数据封装在intent中
*/
@Override
protected void onHandleIntent(Intent intent) {

//在子线程中进行网络请求
Bitmap bitmap=downloadUrlBitmap(intent.getStringExtra(DOWNLOAD_URL));
Message msg1 = new Message();
msg1.what = intent.getIntExtra(INDEX_FLAG,0);
msg1.obj =bitmap;
//通知主线程去更新UI
if(updateUI!=null){
updateUI.updateUI(msg1);
}
//mUIHandler.sendMessageDelayed(msg1,1000);

LogUtils.e("onHandleIntent");
}
//----------------------重写一下方法仅为测试------------------------------------------
@Override
public void onCreate() {
LogUtils.e("onCreate");
super.onCreate();
}

@Override
public void onStart(Intent intent, int startId) {
super.onStart(intent, startId);
LogUtils.e("onStart");
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
LogUtils.e("onStartCommand");
return super.onStartCommand(intent, flags, startId);

}

@Override
public void onDestroy() {
LogUtils.e("onDestroy");
super.onDestroy();
}

@Override
public IBinder onBind(Intent intent) {
LogUtils.e("onBind");
return super.onBind(intent);
}


public interface UpdateUI{
void updateUI(Message message);
}


private Bitmap downloadUrlBitmap(String urlString) {
HttpURLConnection urlConnection = null;
BufferedInputStream in = null;
Bitmap bitmap=null;
try {
final URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
in = new BufferedInputStream(urlConnection.getInputStream(), 8 * 1024);
bitmap= BitmapFactory.decodeStream(in);
} catch (final IOException e) {
e.printStackTrace();
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
try {
if (in != null) {
in.close();
}
} catch (final IOException e) {
e.printStackTrace();
}
}
return bitmap;
}

}

  通过代码可以看出,我们继承了IntentService,这里有两个方法是必须实现的,一个是构造方法,必须传递一个线程名称的字符串,另外一个就是进行异步处理的方法onHandleIntent(Intent intent) 方法,其参数intent可以附带从activity传递过来的数据。这里我们的案例主要利用onHandleIntent实现异步下载图片,然后通过回调监听的方法把下载完的bitmap放在message中回调给Activity(当然也可以使用广播完成),最后通过Handler去更新UI。下面再来看看Acitvity的代码:


activity_intent_service.xml


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">

<ImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

IntentServiceActivity.java


package com.zejian.handlerlooper.util;

import android.app.Activity;
import android.content.Intent;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.widget.ImageView;

import com.zejian.handlerlooper.MyIntentService;
import com.zejian.handlerlooper.R;

/**
* Created by zejian
* Time 16/9/3.
* Description:
*/
public class IntentServiceActivity extends Activity implements MyIntentService.UpdateUI{
/**
* 图片地址集合
*/
private String url[] = {
"https://img-blog.csdn.net/20160903083245762",
"https://img-blog.csdn.net/20160903083252184",
"https://img-blog.csdn.net/20160903083257871",
"https://img-blog.csdn.net/20160903083257871",
"https://img-blog.csdn.net/20160903083311972",
"https://img-blog.csdn.net/20160903083319668",
"https://img-blog.csdn.net/20160903083326871"
};

private static ImageView imageView;
private static final Handler mUIHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
imageView.setImageBitmap((Bitmap) msg.obj);
}
};

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_intent_service);
imageView = (ImageView) findViewById(R.id.image);

Intent intent = new Intent(this,MyIntentService.class);
for (int i=0;i<7;i++) {//循环启动任务
intent.putExtra(MyIntentService.DOWNLOAD_URL,url[i]);
intent.putExtra(MyIntentService.INDEX_FLAG,i);
startService(intent);
}
MyIntentService.setUpdateUI(this);
}

//必须通过Handler去更新,该方法为异步方法,不可更新UI
@Override
public void updateUI(Message message) {
mUIHandler.sendMessageDelayed(message,message.what * 1000);
}
}

  代码比较简单,通过for循环多次去启动IntentService,然后去下载图片,注意即使我们多次启动IntentService,但IntentService的实例只有一个,这跟传统的Service是一样的,最终IntentService会去调用onHandleIntent执行异步任务。这里可能我们还会担心for循环去启动任务,而实例又只有一个,那么任务会不会被覆盖掉呢?其实是不会的,因为IntentService真正执行异步任务的是HandlerThread+Handler,每次启动都会把下载图片的任务添加到依附的消息队列中,最后由HandlerThread+Handler去执行。好~,我们运行一下代码:



每间隔一秒去更新图片,接着我们看一组log:



从Log可以看出onCreate只启动了一次,而onStartCommand和onStart多次启动,这就证实了之前所说的,启动多次,但IntentService的实例只有一个,这跟传统的Service是一样的,最后任务都执行完成后,IntentService自动销毁。以上便是IntentService德使用方式,怎么样,比较简单吧。接着我们就来分析一下IntentService的源码,其实也比较简单只有100多行代码。


三、IntentService源码解析


我们先来看看IntentService的onCreate方法:


@Override
public void onCreate() {
// TODO: It would be nice to have an option to hold a partial wakelock
// during processing, and to have a static startService(Context, Intent)
// method that would launch the service & hand off a wakelock.

super.onCreate();
HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
thread.start();

mServiceLooper = thread.getLooper();
mServiceHandler = new ServiceHandler(mServiceLooper);
}

  当第一启动IntentService时,它的onCreate方法将会被调用,其内部会去创建一个HandlerThread并启动它,接着创建一个ServiceHandler(继承Handler),传入HandlerThread的Looper对象,这样ServiceHandler就变成可以处理异步线程的执行类了(因为Looper对象与HandlerThread绑定,而HandlerThread又是一个异步线程,我们把HandlerThread持有的Looper对象传递给Handler后,ServiceHandler内部就持有异步线程的Looper,自然就可以执行异步任务了),那么IntentService是怎么启动异步任务的呢?其实IntentService启动后还会去调用onStartCommand方法,而onStartCommand方法又会去调用onStart方法,我们看看它们的源码:


@Override
public void onStart(Intent intent, int startId) {
Message msg = mServiceHandler.obtainMessage();
msg.arg1 = startId;
msg.obj = intent;
mServiceHandler.sendMessage(msg);
}

/**
* You should not override this method for your IntentService. Instead,
* override {@link #onHandleIntent}, which the system calls when the IntentService
* receives a start request.
* @see android.app.Service#onStartCommand
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
onStart(intent, startId);
return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;
}

  从源码我们可以看出,在onStart方法中,IntentService通过mServiceHandler的sendMessage方法发送了一个消息,这个消息将会发送到HandlerThread中进行处理(因为HandlerThread持有Looper对象,所以其实是Looper从消息队列中取出消息进行处理,然后调用mServiceHandler的handleMessage方法),我们看看ServiceHandler的源码:


private final class ServiceHandler extends Handler {
public ServiceHandler(Looper looper) {
super(looper);
}

@Override
public void handleMessage(Message msg) {
onHandleIntent((Intent)msg.obj);
stopSelf(msg.arg1);
}
}

  这里其实也说明onHandleIntent确实是一个异步处理方法(ServiceHandler本身就是一个异步处理的handler类),在onHandleIntent方法执行结束后,IntentService会通过 stopSelf(int startId)方法来尝试停止服务。这里采用stopSelf(int startId)而不是stopSelf()来停止服务,是因为stopSelf()会立即停止服务,而stopSelf(int startId)会等待所有消息都处理完后才终止服务。最后看看onHandleIntent方法的声明:


protected abstract void onHandleIntent(Intent intent);

  到此我们就知道了IntentService的onHandleIntent方法是一个抽象方法,所以我们在创建IntentService时必须实现该方法,通过上面一系列的分析可知,onHandleIntent方法也是一个异步方法。这里要注意的是如果后台任务只有一个的话,onHandleIntent执行完,服务就会销毁,但如果后台任务有多个的话,onHandleIntent执行完最后一个任务时,服务才销毁。最后我们要知道每次执行一个后台任务就必须启动一次IntentService,而IntentService内部则是通过消息的方式发送给HandlerThread的,然后由Handler中的Looper来处理消息,而Looper是按顺序从消息队列中取任务的,也就是说IntentService的后台任务时顺序执行的,当有多个后台任务同时存在时,这些后台任务会按外部调用的顺序排队执行,我们前面的使用案例也很好说明了这点。最后贴一下到IntentService的全部源码,大家再次感受一下:


/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package android.app;

import android.annotation.WorkerThread;
import android.content.Intent;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;

/**
* IntentService is a base class for {@link Service}s that handle asynchronous
* requests (expressed as {@link Intent}s) on demand. Clients send requests
* through {@link android.content.Context#startService(Intent)} calls; the
* service is started as needed, handles each Intent in turn using a worker
* thread, and stops itself when it runs out of work.
*
* <p>This "work queue processor" pattern is commonly used to offload tasks
* from an application's main thread. The IntentService class exists to
* simplify this pattern and take care of the mechanics. To use it, extend
* IntentService and implement {@link #onHandleIntent(Intent)}. IntentService
* will receive the Intents, launch a worker thread, and stop the service as
* appropriate.
*
* <p>All requests are handled on a single worker thread -- they may take as
* long as necessary (and will not block the application's main loop), but
* only one request will be processed at a time.
*
* <div>
* <h3>Developer Guides</h3>
* <p>For a detailed discussion about how to create services, read the
* <a href="{@docRoot}guide/topics/fundamentals/services.html">Services</a> developer guide.</p>
* </div>
*
* @see android.os.AsyncTask
*/
public abstract class IntentService extends Service {
private volatile Looper mServiceLooper;

private volatile ServiceHandler mServiceHandler;
private String mName;
private boolean mRedelivery;

private final class ServiceHandler extends Handler {
public ServiceHandler(Looper looper) {
super(looper);
}

@Override
public void handleMessage(Message msg) {
onHandleIntent((Intent)msg.obj);
stopSelf(msg.arg1);
}
}

/**
* Creates an IntentService. Invoked by your subclass's constructor.
*
* @param name Used to name the worker thread, important only for debugging.
*/
public IntentService(String name) {
super();
mName = name;
}

/**
* Sets intent redelivery preferences. Usually called from the constructor
* with your preferred semantics.
*
* <p>If enabled is true,
* {@link #onStartCommand(Intent, int, int)} will return
* {@link Service#START_REDELIVER_INTENT}, so if this process dies before
* {@link #onHandleIntent(Intent)} returns, the process will be restarted
* and the intent redelivered. If multiple Intents have been sent, only
* the most recent one is guaranteed to be redelivered.
*
* <p>If enabled is false (the default),
* {@link #onStartCommand(Intent, int, int)} will return
* {@link Service#START_NOT_STICKY}, and if the process dies, the Intent
* dies along with it.
*/
public void setIntentRedelivery(boolean enabled) {
mRedelivery = enabled;
}

@Override
public void onCreate() {
// TODO: It would be nice to have an option to hold a partial wakelock
// during processing, and to have a static startService(Context, Intent)
// method that would launch the service & hand off a wakelock.

super.onCreate();
HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
thread.start();

mServiceLooper = thread.getLooper();
mServiceHandler = new ServiceHandler(mServiceLooper);
}

@Override
public void onStart(Intent intent, int startId) {
Message msg = mServiceHandler.obtainMessage();
msg.arg1 = startId;
msg.obj = intent;
mServiceHandler.sendMessage(msg);
}

/**
* You should not override this method for your IntentService. Instead,
* override {@link #onHandleIntent}, which the system calls when the IntentService
* receives a start request.
* @see android.app.Service#onStartCommand
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
onStart(intent, startId);
return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;
}

@Override
public void onDestroy() {
mServiceLooper.quit();
}

/**
* Unless you provide binding for your service, you don't need to implement this
* method, because the default implementation returns null.
* @see android.app.Service#onBind
*/
@Override
public IBinder onBind(Intent intent) {
return null;
}

/**
* This method is invoked on the worker thread with a request to process.
* Only one Intent is processed at a time, but the processing happens on a
* worker thread that runs independently from other application logic.
* So, if this code takes a long time, it will hold up other requests to
* the same IntentService, but it will not hold up anything else.
* When all requests have been handled, the IntentService stops itself,
* so you should not call {@link #stopSelf}.
*
* @param intent The value passed to {@link
* android.content.Context#startService(Intent)}.
*/
@WorkerThread
protected abstract void onHandleIntent(Intent intent);
}

此IntentService的源码就分析完了,嗯,本篇完结。


作者:如果声音不记得
链接:https://juejin.cn/post/7040651369497231374
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android onSaveInstanceState/onRestoreInstanceState 原来要这么理解

前些天,有位小伙伴兴匆匆地跑过来给我展示一个现象:Activity 里有个EditText,点击该EditText 输入一些文字。此时,转动手机方向,Activity 变成横屏了,而EditText 上的文字依然保留。 问我:为啥EditText上文字能够恢复...
继续阅读 »

前些天,有位小伙伴兴匆匆地跑过来给我展示一个现象:Activity 里有个EditText,点击该EditText 输入一些文字。此时,转动手机方向,Activity 变成横屏了,而EditText 上的文字依然保留。

问我:为啥EditText上文字能够恢复?

我说:你Activity 配置了横竖屏切换时不重建Activity。

他立马给我展示了:Activity 重建的日志。

我说:系统会在重建Activity 的时候恢复整个ViewTree吧。

他又给我展示了:ImageView 横竖屏时没有恢复之前的图像。

我:...

不服输的我开始了默默地研究,于是有了这篇总结以解心中困惑。

通过本篇文章,你将了解到:



1、onSaveInstanceState/onRestoreInstanceState 作用。

2、onSaveInstanceState/onRestoreInstanceState 原理分析

3、onSaveInstanceState/onRestoreInstanceState 触发场景。

4、onSaveInstanceState/onRestoreInstanceState 为啥不能存放大数据?

5、与Jetpack ViewModel 区别。



1、onSaveInstanceState/onRestoreInstanceState 作用


EditText/ImageView 横竖屏地表现



tt0.top-423136.gif


可以看出,从竖屏到横屏再恢复到竖屏,EditText 内容没有变化。而从竖屏到横屏时,ImageView 内容已经丢失了。

都是系统控件,咱们也没有进行其它的额外区别处理,为啥表现不一致呢?

View.java 里有两个方法:


#View.java
protected Parcelable onSaveInstanceState() {...}

protected void onRestoreInstanceState(Parcelable state){...}

官方注释上写的比较清楚了:



1、onSaveInstanceState 是个钩子方法,View.java 的子类可以重写该方法,在方法里面存储一些子类的内部状态,用以下次重建时恢复。

2、onRestoreInstanceState 也是个钩子方法,用以恢复在onSaveInstanceState 里保存的状态。



既然是View的方法,分别查看EditText 与ImageView 对它们的重写情况:


#TextView.java
public Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
...
if (freezesText || hasSelection) {
SavedState ss = new SavedState(superState);

if (freezesText) {
if (mText instanceof Spanned) {
final Spannable sp = new SpannableStringBuilder(mText);
...
ss.text = sp;
} else {
//将TextView 内容存储在SavedState里
ss.text = mText.toString();
}
}
...
return ss;
}

//返回存储的对象
return superState;
}

public void onRestoreInstanceState(Parcelable state) {
if (!(state instanceof SavedState)) {
super.onRestoreInstanceState(state);
return;
}

SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());

if (ss.text != null) {
//取出TextView 内容,并设置
setText(ss.text);
}
...
}

由此可见,TextView 重写这俩方法,先是在onSaveInstanceState 里存储文本内容,再在onRestoreInstanceState 里恢复文本内容。

而通过查看ImageView 发现它并没有重写这俩方法,当然就不能恢复了。其实这也比较容易理解,毕竟对于ImageView,Bitmap 是它的内容,暂存这个Bitmap 很耗内存。


需要注意的是:想要onSaveInstanceState 被调用,则需要给该控件设置id。因为系统是根据View id将状态存储在SparseArray 里


Activity 横竖屏的处理


现在的问题是:谁调用了View 的onSaveInstanceState/onRestoreInstanceState ? 在前一篇分析过Activity 和View的关系:Android Activity 与View 的互动思考

可知,Activity 通过Window 控制View,我们子类继承自EditText,并重写 onSaveInstanceState/onRestoreInstanceState,然后在横竖屏切换时查看这俩方法的调用栈:



image.png


第一个红色框表示EditText子类里的方法(onSaveInstanceState),而第二个红框表示Activity 子类里重写的方法(onSaveInstanceState)。

由此可知,当横竖屏切换时调用了Activity.onSaveInstanceState(xx) 方法。


#Activity.java
protected void onSaveInstanceState(@NonNull Bundle outState) {
//saveHierarchyState 调用整个ViewTree 的onSaveInstanceState 方法
outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState());
...
//告知生命周期回调方法状态已保存
dispatchActivitySaveInstanceState(outState);
}

同样的对于onRestoreInstanceState:


#Activity.java
protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
if (mWindow != null) {
Bundle windowState = savedInstanceState.getBundle(WINDOW_HIERARCHY_TAG);
if (windowState != null) {
//恢复整个ViewTree 状态
mWindow.restoreHierarchyState(windowState);
}
}
}


当横竖屏切换时,会调用到Activity onSaveInstanceState/onRestoreInstanceState 方法,进而会调用整个ViewTree onSaveInstanceState/onRestoreInstanceState 方法来保存与恢复必要的状态。



Activity 数据保存与恢复


Activity 的onSaveInstanceState/onRestoreInstanceState 方法 除了触发View 的状态保存与恢复外,还可以将Activity 用到的一些重要的数据保存下来,待下次Activity 重建时恢复。

重写两者:


    @Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString("say", "hello world");
}

@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
String restore = savedInstanceState.getString("say");
Log.d("fish", restore);
}

此时我们注意到onSaveInstanceState 的入参是Bundle 类型,往outState 写入数据,在onRestoreInstanceState 将数据取出,outState/savedInstanceState 必然不为空。


总结 onSaveInstanceState/onRestoreInstanceState 作用



1、保存与恢复View 的状态。

2、保存与恢复Activity 自定义数据。



2、onSaveInstanceState/onRestoreInstanceState 原理分析。


onSaveInstanceState 调用时机


之前 Android Activity 生命周期详解及监听 有详细分析了Activity 各个阶段的调用情况,现在结合生命周期来分析onSaveInstanceState(xx)在生命周期中的哪个阶段被调用的。

调用栈如下:



image.png


看看上图标黄色的方法,这方法很眼熟,在Activity 生命周期中分析过,它是Activity.onStop()方法的调用者:


#ActivityThread.java
private void callActivityOnStop(ActivityClientRecord r, boolean saveState, String reason) {
// Before P onSaveInstanceState was called before onStop, starting with P it's
// called after. Before Honeycomb state was always saved before onPause.
//这句话翻译过来:
//如果目标设备是Android 9之前,那么onSaveInstanceState 在onStop 之前调用
//如果在Android 9 之后,那么onSaveInstanceState 在onStop 之后调用
//Honeycomb 指的是Android 3.0 现在基本可以忽略了。
//r.activity.mFinished 表示Activity 是否即将被销毁
final boolean shouldSaveState = saveState && !r.activity.mFinished && r.state == null
&& !r.isPreHoneycomb();
final boolean isPreP = r.isPreP();
//Android p 之前先于onStop 之前执行
if (shouldSaveState && isPreP) {
callActivityOnSaveInstanceState(r);
}
try {
//最终执行到Activity.onStop()方法
r.activity.performStop(r.mPreserveWindow, reason);
} catch (SuperNotCalledException e) {
...
}
//标记Stop状态
r.setState(ON_STOP);

if (shouldSaveState && !isPreP) {
//调用onSave 保存
callActivityOnSaveInstanceState(r);
}
}

以上注释比较详细了,小结一下:



1、在Android 9之前,onSaveInstanceState 在onStop 之前调用(至于在onPause 之前还是之后调用,这个时机不确定);在Android 9(包含)之后,onSaveInstanceState 在onStop 之后调用。

2、如果Activity 即将被销毁,则onSaveInstanceState 不会被调用。



对于第二句的理解,举个简单例子:



Activity 在前台时,此时按Home键回到桌面,会执行onSaveInstanceState;若是按back键/主动finish,此时虽然会执行到onStop,但是不会执行onSaveInstanceState。



onRestoreInstanceState 调用时机


现在已经弄清楚onSaveInstanceState 调用时机,接着来分析 onRestoreInstanceState 什么时候执行。

调用栈如下:



image.png


黄色部分的方法也很眼熟,它是Activity.onStart()方法的调用者:


    public void handleStartActivity(ActivityClientRecord r,
PendingTransactionActions pendingActions) {
final Activity activity = r.activity;
...
//最终执行到Activity.onStart()
activity.performStart("handleStartActivity");
r.setState(ON_START);
...
if (pendingActions.shouldRestoreInstanceState()) {
if (r.isPersistable()) {
//从持久化存储里恢复数据
if (r.state != null || r.persistentState != null) {
mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state,
r.persistentState);
}
} else if (r.state != null) {
//从内存里恢复数据
mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state);
}
}
...
}

小结:



1、onRestoreInstanceState 在onStart()方法之后执行。

2、pendingActions.shouldRestoreInstanceState() 返回值是执行 onRestoreInstanceState()方法的关键,它是在哪赋值的呢?接下来会分析。

3、r.state 不能为空,毕竟没数据无法恢复。



通过以上分析,结合Activity生命周期,onSaveInstanceState /onRestoreInstanceState 调用时机如下:



image.png


onRestoreInstanceState 与onCreate 参数差异


onCreate参数也是Bundle类型,实际上这个参数就是onSaveInstanceState里保存的Bundle,这个Bundle分别传递给了onCreate和onRestoreInstanceState,而onCreate里的Bundle可能为空(新建非重建的情况下),onRestoreInstanceState 里的Bundle必然不为空。

官方注释也说了在onRestoreInstanceState里处理数据的恢复更灵活。


3、onSaveInstanceState/onRestoreInstanceState 触发场景


横竖屏触发的场景


在前面的分析中,与Activity 生命周期关联可能会让人有种印象:

onSaveInstanceState 调用之后onRestoreInstanceState 就会被调用。

而事实并非如此,举个简单例子:

Activity 处在前台时,此时退回到桌面,onSaveInstanceState 会被执行。而后再让Activity 回到前台,onStart()方法执行后,发现onRestoreInstanceState 并没有被调用。



也就是说onSaveInstanceState/onRestoreInstanceState 的调用不一定是成对出现的。



还记得在分析onRestoreInstanceState 遗留了个问题: pendingActions.shouldRestoreInstanceState() 返回值如何确定的 ?

在横竖屏切换时,onRestoreInstanceState 被调用了,说明 pendingActions.shouldRestoreInstanceState() 在横竖屏切换时返回了true,接着来看看其来龙去脉:


#PendingTransactionActions.java
//判断是否需要执行onRestoreInstanceState 方法
public boolean shouldRestoreInstanceState() {
return mRestoreInstanceState;
}

//设置标记
public void setRestoreInstanceState(boolean restoreInstanceState) {
mRestoreInstanceState = restoreInstanceState;
}

只需要找到setRestoreInstanceState()在何处调用即可。

直接说结论:



ActivityThread.handleLaunchActivity() 里设置了setRestoreInstanceState(true)



而handleLaunchActivity()在两种情况下被调用:



image.png


横竖屏时属于重建 Activity,因此onRestoreInstanceState 能被调用。

而从后台返回到前台,并没有新建Activity也没有重建Activity,因此onRestoreInstanceState 不会被调用。

又引申出另一个问题:为啥新建Activity 时onRestoreInstanceState 没被调用?

答案:因为新建Activity 时,ActivityClientRecord 是全新的对象,它所持有的Bundle state 对象为空,因此不会调用到onRestoreInstanceState。


其它配置项更改的场景


除了横竖屏切换时会重建Activity,还有以下配置项更改会重建Activity:



image.png


当然,还有一些不常涉及的配置项,比如所在地区更改等。


重建Activity 的细节



image.png


当需要重建Activity 时,AMS 发出指令,会执行到 ActivityThread.handleRelaunchActivity()方法。


#ActivityThread.java
public void handleRelaunchActivity(ActivityClientRecord tmp,
PendingTransactionActions pendingActions) {
...
//从Map 里获取缓存的ActivityClientRecord
ActivityClientRecord r = mActivities.get(tmp.token);
...
//将ActivityClientRecord 传递下去
handleRelaunchActivityInner(r, configChanges, tmp.pendingResults, tmp.pendingIntents,
pendingActions, tmp.startsNotResumed, tmp.overrideConfig, "handleRelaunchActivity");
...
}

mActivities 定义如下:


final ArrayMap<IBinder, ActivityClientRecord> mActivities = new ArrayMap<>();

以IBinder 为key,存储ActivityClientRecord。

当新建Activity 时,存入ActivityClientRecord,当销毁Activity 时,移除 ActivityClientRecord。


再来分析handleRelaunchActivityInner():


#ActivityThread.java
private void handleRelaunchActivityInner(...) {
...
if (!r.paused) {
//最终执行到onPause
performPauseActivity(r, false, reason, null /* pendingActions */);
}
if (!r.stopped) {
//最终执行到onStop
callActivityOnStop(r, true /* saveState */, reason);
}
//最终执行到onDestroy
handleDestroyActivity(r.token, false, configChanges, true, reason);
//创建新的Activity 实例
handleLaunchActivity(r, pendingActions, customIntent);
}

通过分析Activity 重建的细节,有以下结论:



1、Activity 重建过程中,先将原来的Activity 进行销毁(从onResume--onStop-->onDestroy 的生命周期)。

2、虽然是不同的Activity 对象,但重建时使用的ActivityClientRecord 却是相同的,而ActivityClientRecord 最终是被ActivityThread 持有,它是全局的。这也是 onSaveInstanceState/onRestoreInstanceState 能够存储与恢复数据的本质原因。



当然也可以通过配置告诉系统在配置项变更时不重建Activity:


<activity android:name=".viewmodel.ViewModelActivity" android:configChanges="orientation|screenSize"></activity>

比如以上配置,当横竖屏切换时,不会重建Activity,而配置项的变更会通过 Activity.onConfigurationChanged()方法回调。


4、onSaveInstanceState/onRestoreInstanceState 为啥不能存放大数据?


onSaveInstanceState/onRestoreInstanceState 的参数都是Bundle 类型,思考一下为什么需要定义为Bundle类型呢?

Android IPC 精讲系列 中有提到过,Android 进程间通信方式大多时候使用的是Binder,而要想自定义数据能够通过Binder传输则需要实现Parcelable 接口,Bundle 实现了Parcelable 接口。


由此我们推测,onSaveInstanceState/onRestoreInstanceState 可能涉及到进程间通信,才会用Bundle 来修饰形参。但之前说的ActivityClientRecord是存储在当前进程的啊,貌似和其它进程没有关联呢?

要分析这个问题,实际上只需要在onSaveInstanceState 存储一个比较大的数据,看看报错时的堆栈。


    protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString("say", "hello world");
//存储2M 数据
outState.putByteArray("big", new byte[1024*1024*2]);
}

保存2M 的数据,通常来说这是超出了Binder的限制,当调用onSaveInstanceState 时会有报错信息:



image.png


果然还是crash了。

找到 PendingTransactionActions ,它实现了Runnable 接口,在其run方法里:


#PendingTransactionActions.java
public void run() {
try {
//提交给ActivityTaskManagerService 处理,属于进程间通信
//mState 即是onSaveInstanceState 保存的数据
ActivityTaskManager.getService().activityStopped(
mActivity.token, mState, mPersistentState, mDescription);
} catch (RemoteException ex) {
...
}
}

而在ActivityThread.java 里有个方法:


    public void reportStop(PendingTransactionActions pendingActions) {
mH.post(pendingActions.getStopInfo());
}

该方法用于告知系统,咱们的Activity 已经变为Stop状态了,最终会执行到PendingTransactionActions.run()方法。

小结一下:



onSaveInstanceState 存储的数据,在onStop执行后,当前进程需要将Stop状态传递给ATM(ActivityTaskManagerService 运行在system_server进程),因为跨进程传递(Binder)有大小限制,因此onSaveInstanceState 不能传递大量数据。



5、与Jetpack ViewModel 区别


onSaveInstanceState 与 ViewModel 都是将数据放在ActivityClientRecord 的不同字段里。



image.png



1、onSaveInstanceState 用Bundle存储数据便于跨进程传递,而ViewModel 是Object存储数据,不需要跨进程,因此它没有大小限制。

2、onSaveInstanceState 在onStop 之后调用,比较频繁。而ViewModel 存储数据是onDestroy 之后。

3、onSaveInstanceState 可以选择是否持久化数据到文件里(该功能由ATM 实现,存储到xml里),而ViewModel 没有这功能。



更多的区别后续分析 ViewModel 时会提到。


本文基于Android 10.0。


作者:小鱼人爱编程
链接:https://juejin.cn/post/7040819115874844709
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Spring Boot + Redis 解决重复提交问题,还有谁不会??

前言 在实际的开发项目中,一个对外暴露的接口往往会面临很多次请求,我们来解释一下幂等的概念:任意多次执行所产生的影响均与一次执行的影响相同。按照这个含义,最终的含义就是 对数据库的影响只能是一次性的,不能重复处理。如何保证其幂等性,通常有以下手段: 1、数据库...
继续阅读 »

前言


在实际的开发项目中,一个对外暴露的接口往往会面临很多次请求,我们来解释一下幂等的概念:任意多次执行所产生的影响均与一次执行的影响相同。按照这个含义,最终的含义就是 对数据库的影响只能是一次性的,不能重复处理。如何保证其幂等性,通常有以下手段:


1、数据库建立唯一性索引,可以保证最终插入数据库的只有一条数据。


2、token机制,每次接口请求前先获取一个token,然后再下次请求的时候在请求的header体中加上这个token,后台进行验证,如果验证通过删除token,下次请求再次判断token。


3、悲观锁或者乐观锁,悲观锁可以保证每次for update的时候其他sql无法update数据(在数据库引擎是innodb的时候,select的条件必须是唯一索引,防止锁全表)


4、先查询后判断,首先通过查询数据库是否存在数据,如果存在证明已经请求过了,直接拒绝该请求,如果没有存在,就证明是第一次进来,直接放行。


redis 实现自动幂等的原理图:



搭建 Redis 服务 API


1、首先是搭建redis服务器。


2、引入springboot中到的redis的stater,或者Spring封装的jedis也可以,后面主要用到的api就是它的set方法和exists方法,这里我们使用springboot的封装好的redisTemplate。


推荐一个 Spring Boot 基础教程及实战示例:
github.com/javastacks/…


/**
* redis工具类
*/
@Component
public class RedisService {

@Autowired
private RedisTemplate redisTemplate;

/**
* 写入缓存
* @param key
* @param value
* @return
*/
public boolean set(finalString key, Object value) {
boolean result = false;
try {
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
operations.set(key, value);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}

/**
* 写入缓存设置时效时间
* @param key
* @param value
* @return
*/
public boolean setEx(finalString key, Object value, Long expireTime) {
boolean result = false;
try {
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
operations.set(key, value);
redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}

/**
* 判断缓存中是否有对应的value
* @param key
* @return
*/
public boolean exists(finalString key) {
return redisTemplate.hasKey(key);
}

/**
* 读取缓存
* @param key
* @return
*/
public Objectget(finalString key) {
Object result = null;
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
result = operations.get(key);
return result;
}

/**
* 删除对应的value
* @param key
*/
public boolean remove(finalString key) {
if (exists(key)) {
Boolean delete = redisTemplate.delete(key);
return delete;
}
returnfalse;

}

}

自定义注解 AutoIdempotent


自定义一个注解,定义此注解的主要目的是把它添加在需要实现幂等的方法上,凡是某个方法注解了它,都会实现自动幂等。后台利用反射如果扫描到这个注解,就会处理这个方法实现自动幂等,使用元注解ElementType.METHOD表示它只能放在方法上,etentionPolicy.RUNTIME表示它在运行时。


@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoIdempotent {

}

token 创建和检验


token服务接口:我们新建一个接口,创建token服务,里面主要是两个方法,一个用来创建token,一个用来验证token。创建token主要产生的是一个字符串,检验token的话主要是传达request对象,为什么要传request对象呢?主要作用就是获取header里面的token,然后检验,通过抛出的Exception来获取具体的报错信息返回给前端。


publicinterface TokenService {

/**
* 创建token
* @return
*/
public String createToken();

/**
* 检验token
* @param request
* @return
*/
public boolean checkToken(HttpServletRequest request) throws Exception;

}

token的服务实现类:token引用了redis服务,创建token采用随机算法工具类生成随机uuid字符串,然后放入到redis中(为了防止数据的冗余保留,这里设置过期时间为10000秒,具体可视业务而定),如果放入成功,最后返回这个token值。checkToken方法就是从header中获取token到值(如果header中拿不到,就从paramter中获取),如若不存在,直接抛出异常。这个异常信息可以被拦截器捕捉到,然后返回给前端。


@Service
publicclass TokenServiceImpl implements TokenService {

@Autowired
private RedisService redisService;

/**
* 创建token
*
* @return
*/
@Override
public String createToken() {
String str = RandomUtil.randomUUID();
StrBuilder token = new StrBuilder();
try {
token.append(Constant.Redis.TOKEN_PREFIX).append(str);
redisService.setEx(token.toString(), token.toString(),10000L);
boolean notEmpty = StrUtil.isNotEmpty(token.toString());
if (notEmpty) {
return token.toString();
}
}catch (Exception ex){
ex.printStackTrace();
}
returnnull;
}

/**
* 检验token
*
* @param request
* @return
*/
@Override
public boolean checkToken(HttpServletRequest request) throws Exception {

String token = request.getHeader(Constant.TOKEN_NAME);
if (StrUtil.isBlank(token)) {// header中不存在token
token = request.getParameter(Constant.TOKEN_NAME);
if (StrUtil.isBlank(token)) {// parameter中也不存在token
thrownew ServiceException(Constant.ResponseCode.ILLEGAL_ARGUMENT, 100);
}
}

if (!redisService.exists(token)) {
thrownew ServiceException(Constant.ResponseCode.REPETITIVE_OPERATION, 200);
}

boolean remove = redisService.remove(token);
if (!remove) {
thrownew ServiceException(Constant.ResponseCode.REPETITIVE_OPERATION, 200);
}
returntrue;
}
}

拦截器的配置


web配置类,实现WebMvcConfigurerAdapter,主要作用就是添加autoIdempotentInterceptor到配置类中,这样我们到拦截器才能生效,注意使用@Configuration注解,这样在容器启动是时候就可以添加进入context中。


@Configuration
publicclass WebConfiguration extends WebMvcConfigurerAdapter {

@Resource
private AutoIdempotentInterceptor autoIdempotentInterceptor;

/**
* 添加拦截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(autoIdempotentInterceptor);
super.addInterceptors(registry);
}
}

拦截处理器:主要的功能是拦截扫描到AutoIdempotent到注解到方法,然后调用tokenService的checkToken()方法校验token是否正确,如果捕捉到异常就将异常信息渲染成json返回给前端。


/**
* 拦截器
*/
@Component
publicclass AutoIdempotentInterceptor implements HandlerInterceptor {

@Autowired
private TokenService tokenService;

/**
* 预处理
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

if (!(handler instanceof HandlerMethod)) {
returntrue;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
//被ApiIdempotment标记的扫描
AutoIdempotent methodAnnotation = method.getAnnotation(AutoIdempotent.class);
if (methodAnnotation != null) {
try {
return tokenService.checkToken(request);// 幂等性校验, 校验通过则放行, 校验失败则抛出异常, 并通过统一异常处理返回友好提示
}catch (Exception ex){
ResultVo failedResult = ResultVo.getFailedResult(101, ex.getMessage());
writeReturnJson(response, JSONUtil.toJsonStr(failedResult));
throw ex;
}
}
//必须返回true,否则会被拦截一切请求
returntrue;
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

}

/**
* 返回的json值
* @param response
* @param json
* @throws Exception
*/
private void writeReturnJson(HttpServletResponse response, String json) throws Exception{
PrintWriter writer = null;
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html; charset=utf-8");
try {
writer = response.getWriter();
writer.print(json);

} catch (IOException e) {
} finally {
if (writer != null)
writer.close();
}
}

}

测试用例


模拟业务请求类,首先我们需要通过/get/token路径通过getToken()方法去获取具体的token,然后我们调用testIdempotence方法,这个方法上面注解了@AutoIdempotent,拦截器会拦截所有的请求,当判断到处理的方法上面有该注解的时候,就会调用TokenService中的checkToken()方法,如果捕获到异常会将异常抛出调用者,下面我们来模拟请求一下:


@RestController
publicclass BusinessController {

@Resource
private TokenService tokenService;

@Resource
private TestService testService;

@PostMapping("/get/token")
public String getToken(){
String token = tokenService.createToken();
if (StrUtil.isNotEmpty(token)) {
ResultVo resultVo = new ResultVo();
resultVo.setCode(Constant.code_success);
resultVo.setMessage(Constant.SUCCESS);
resultVo.setData(token);
return JSONUtil.toJsonStr(resultVo);
}
return StrUtil.EMPTY;
}

@AutoIdempotent
@PostMapping("/test/Idempotence")
public String testIdempotence() {
String businessResult = testService.testIdempotence();
if (StrUtil.isNotEmpty(businessResult)) {
ResultVo successResult = ResultVo.getSuccessResult(businessResult);
return JSONUtil.toJsonStr(successResult);
}
return StrUtil.EMPTY;
}
}

使用postman请求,首先访问get/token路径获取到具体到token:



利用获取到到token,然后放到具体请求到header中,可以看到第一次请求成功,接着我们请求第二次:



第二次请求,返回到是重复性操作,可见重复性验证通过,再多次请求到时候我们只让其第一次成功,第二次就是失败:



总结


本篇博客介绍了使用springboot和拦截器、redis来优雅的实现接口幂等,对于幂等在实际的开发过程中是十分重要的,因为一个接口可能会被无数的客户端调用,如何保证其不影响后台的业务处理,如何保证其只影响数据一次是非常重要的,它可以防止产生脏数据或者乱数据,也可以减少并发量,实乃十分有益的一件事。而传统的做法是每次判断数据,这种做法不够智能化和自动化,比较麻烦。而今天的这种自动化处理也可以提升程序的伸缩性。


作者:Java技术栈
链接:https://juejin.cn/post/7039856436762902565
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

FlutterWeb初体验

FlutterWeb初体验 [toc] 背景 因为最近业务需求的变动,在APP的某一部分页面会经常性发生变动,一般情况下来说,这种不稳定的页面不应该由原生来承担,修改发版的成本太大了,最合理的做法是由H5来承担,由原生提供必要的bridge来调用原生方法,但是...
继续阅读 »

FlutterWeb初体验


[toc]


背景


因为最近业务需求的变动,在APP的某一部分页面会经常性发生变动,一般情况下来说,这种不稳定的页面不应该由原生来承担,修改发版的成本太大了,最合理的做法是由H5来承担,由原生提供必要的bridge来调用原生方法,但是由于种种历史债务,还是没有如此实现,经历了痛苦的发版以及等待审核后,我在想flutterWeb是不是可以解决这个问题?


想法


页面进入流程


screenshot-20211210-211936.png


项目架构想法


整个项目转为支持FlutterWeb


整个项目转为flutterweb,可以打包成web文件直接部署在服务器,而app依旧打包成apk和ipa,但是在路由监听处留下开关,当有页面需要紧急修复或者紧急更改的情况下,下发配置,跳转的时候根据路由配置跳转WebView或者原生页面。


抽离出某个模块,单个模块支持web


抽离出一个module,由一个壳工程引用,这个壳工程用于把该module打包成web;同时该模块依然被app工程引用,作为一个功能模块,而部署的时候只部署了这个模块的web产物。


因为目前app集成了一定数量的原生端的第三方sdk,直接支持flutterweb工程量较大,所以先尝试第二个方法。


壳工程结构图

1924616-18f5d8ee85f0f330.png


其中


flutter_libs 是基础的lib库,封装了基础的网络请求,持久化存储,状态管理等基础,壳工程和app工程也会引用


ly_income是功能module,也是我们主要开发需求的模块,它会被壳工程引用作为web的打包内容,也会被app工程引用作为原生的页面展示。


实践


打包问题处理


因为是新建的项目工程,打包成flutterWeb并不会有那么多障碍。


开启web支持


执行 flutter config查看目前的配置信息,如果看到


Settings:
enable-web: true
enable-macos-desktop: true

那就是已经开启了,如果还没,可以使用flutter config --enable-web开启配置


打包模式选择

而flutterWeb打包也有两种模式可以选择:html模式和CanvasKit模式


它们两者各自的特别是:


html模式


flutter build web --web-renderer html


当我们采用html渲染模式时,flutter会采用HTML的custom element,CSS,Canvas和SVG来渲染UI元素


优点是:体积比较小


缺点是:渲染性能比较差,跨端一致性可能不受保障



CanvasKit模式


flutter build web --web-renderer canvaskit


当我们采用canvaskit渲染模式时,flutter将 Skia 编译成 WebAssembly 格式,并使用 WebGL 渲染。应用在移动和桌面端保持一致,有更好的性能,以及降低不同浏览器渲染效果不一致的风险。但是应用的大小会增加大约 2MB。


优点是:跨端一致性受保障,渲染性能更好


缺点是:体积比较大,load页面时间会更久



跨域问题处理


之前一直是做app开发,跨域这个词只听过,还没见识过。


了解跨域

跨域是指浏览器的不执行其他网站脚本的,由于浏览器的同源策略造成,是对JavaScript的一种安全限制


说白点理解,当你通过浏览器向其他服务器发送请求时,不是服务器不响应,而是服务器返回的结果被浏览器限制了。


而什么是同源策略的同源



同源指的是协议、域名、端口 都要保持一致


http://www.123.com:8080/index.html (http协议,http://www.123.com 域名、8080 端口 ,只要这三个有一项不一样的都是跨域,这里不一一举例子)


http://www.123.com:8080/matsh.html(…


http://www.123.com:8081/matsh.html(…


注意:localhost 和127.0.0.1 虽然都指向本机,但也属于跨域。



而跨域的解决方法也暂时不适用我:



  1. JSONP方式 (我们项目的请求都是post请求)

  2. 反向代理,ngixn (ngixn小白)

  3. 配置浏览器 (好像不太适用,应该,大概,也许,可能,或许)

  4. 项目配置跨域 (因为只是尝试项目,需要后台和运维支持的话,需要跨部门沟通,太麻烦了)


摘自网络 什么是跨域,侵删歉


常规做法



  1. 本地调试的时候修改代码,支持跨域请求


    在上图红框中添加代码--disable-web-security




1924616-e444ef62f7776b1e.png


1924616-fddf6a72c3a43965.png


然后删除以下两个文件,执行flutter doctor生成新的一份,再尝试run起来,你会发现浏览器已经支持跨域了,你可以很开心地在浏览器run接口了。但是仅支持本地调试!!!



  1. ngixn做转发,但是这个... 我没有怎么用过ngixn,而且需要在周末做完调研给出可行性报告,也没有时间去学习,先搁置,后续再拿起来看看

  2. 后端和运维同学帮忙调试跨域,因为是尝试而已,没有必要用到其他部门的资源,先搁置,后续如果可实际应用,再要求他们协助。


骚操作


保命前提:



  1. 这个其实就是配置转发的做法,但是这块我没什么经验,时间紧任务重所以就先这么尝试做了

  2. 其实这个就是类似于openfeign之类的想法,但是我并不知道后台开发的FeignClient,而且也有点危险,还是调用开发的接口更加稳妥

  3. 纯个人做法,肯定还会有更好的方法,但是这个是我当时最快的达成方案,勿喷。



如果说我要求不了后台服务做跨域,那可不可以我自己要求我自己做跨域呢?


比如:


我请求我的服务器,我的服务器再去请求后台服务,我访问后台服务跨域而已,我的服务器访问后台服务可不跨域,我的服务器跨域又咋样,自己的东西随便拿捏。



  1. 新建一个springboot项目

  2. 搭建一个controller,参数是url全路径以及参数json字符串,配置好header之后请求后台服务并返回信息


@CrossOrigin
@RestController
@RequestMapping("api/home")
public class GatewayController {

@PostMapping("/gatewayApi")
public String gatewayApi(@RequestParam("url") String url, @RequestParam("params") String json) {
try {
JSONObject jsonObject = JSONObject.parseObject(json);
JSONObject result = doPost(jsonObject, url);
if (result != null) {
return result.toString();
} else {
return errMsg().toString();
}
} catch (Exception e) {
return errMsg(e.getMessage()).toString();
}
}
}


  1. 配置跨域信息


@SpringBootConfiguration
public class WebGlobalConfig {

@Bean
public CorsFilter corsFilter() {

//创建CorsConfiguration对象后添加配置
CorsConfiguration config = new CorsConfiguration();
//设置放行哪些原始域
config.addAllowedOriginPattern("*");
//放行哪些原始请求头部信息
config.addAllowedHeader("*");
//暴露哪些头部信息
config.addExposedHeader("*");
//放行哪些请求方式
config.addAllowedMethod("GET"); //get
config.addAllowedMethod("PUT"); //put
config.addAllowedMethod("POST"); //post
config.addAllowedMethod("DELETE"); //delete
//corsConfig.addAllowedMethod("*"); //放行全部请求

//是否发送Cookie
config.setAllowCredentials(true);

//2. 添加映射路径
UrlBasedCorsConfigurationSource corsConfigurationSource =
new UrlBasedCorsConfigurationSource();
corsConfigurationSource.registerCorsConfiguration("/**", config);
//返回CorsFilter
return new CorsFilter(corsConfigurationSource);
}
}


  1. 打包后部署到服务器

  2. module里的接口不再请求后台服务,而是请求我的服务器,因为只是转发,所以没有改动任何数据结构,只需要请求地址改动下

  3. 可以跨域了


与原生交互问题


设想中web的页面可以有三种方式:



  1. 集成在app里面作为原生页面,这个的交互没什么好说的。

  2. 打包成web项目,通过webview进行加载,那需要额外处理持久化信息的获取与写入,以及与原生页面的跳转交互

  3. 只有url,测试人员可以通过url路径传参之类的切换账号,方便测试


针对业务来说,页面的加载流程应该是这样的:


screenshot-20211210-211914.png


不同场景做不同的操作

原生

通过持久化工具类获取用户基础信息,然后读取接口判断身份,根据身份去做不同展示,点击跳转时间也是直接的通过路由跳转


通过webview加载

通过js交互,从原生模块拿到用户基础信息(存疑,是否直接读接口?,这样避免对原生api的依赖,如果有需求修改的话可以尽量不依赖),然后读取接口判断身份,根据身份不同去做不同展示,如果是dialog之类的交互可以直接实现,如果是跳转页面之类的,可以通过js交互进行原生操作


通过url加载的

通过url的参数串获取到对应的用户id,读取接口获取用户信息,其他操作如上,但是页面没有跳转之类的交互


实现

从链接上面获取参数

比如url为:```xxx.yyy.zzz/value


要如何拿到value值?


因为项目里刚好使用了Get做状态管理,而刚好Get已经实现了这一块,世间上的事情就是这么刚好。(好像navigator2已经支持这个了,不过还没仔细看过)




  1. 配置路由表



    class RouterConf {
    static const String appIncomeArgs = '/app/inCome/:fromApp';
    static const String appIncome = '/app/inCome/';
    static List<GetPage> _getPages = [];
    static List<GetPage> get getPages {
    _getPages = [
    GetPage(name: appIncomeArgs, page: () => const StoreKeeperInComePage()),
    ];
    return _getPages;
    }
    }

    这里appIncome配置了两个路由名


    但是实际使用时以没带**:fromApp为准的,fromApp我觉得可以理解成一个占位符,也就是fromApp=value**




  2. 获取对应的value


    在base类里面定义一个bool值,在init的回调里面去做获取操作


      bool ifFromApp = false;
    Map<String, String?> _args = Get.parameters;
    if (_args.isNotEmpty && _args.containsKey('fromApp')) {
    String? _fromAppFlag = Get.parameters['fromApp'];
    if ((_fromAppFlag?.isNotEmpty ?? false)) {
    ifFromApp = _fromAppFlag == "1";
    }
    }



根据不同情景做操作

以在webview打开为例,在页面加载时通过js交互获取用户信息,拿到用户信息后替换cache类里缓存的id,token之类的,因为拦截器里面会读取这些值用于拼接通用参数


  @override
void onReady() {
if (ifFromApp) {
initUserInfo();
js.context['getUserInfoCallback'] = getUserInfoCallback;
}else{
_loadInterface();
}

super.onReady();
}

void initUserInfo() {
js.context.callMethod("callFlutterMethod", [
json.encode({
"api": "getUserInfo",
"data": {
"name": 'getUserInfo',
"needCallback": true,
"needToken": true,
"callbackName": 'getUserInfoCallback',
"callbackArgs": 'info'
},
})
]);
}

void getUserInfoCallback(msg, info) {
Map<String, dynamic> _args = {};
if (info != null) {
if (info is String) {
_args = jsonDecode(info);
} else {
_args = info;
}
if (_args.containsKey("info")) {
dynamic _realInfo = _args['info'];
if (_realInfo is String) {
_args = jsonDecode(_realInfo);
} else {
_args = _realInfo;
}
}
if (_args.containsKey('name')) {
debugPrint(' _args[name]---------${_args['name']}');
CacheManager.instance.oName = _args['name'];
}
if (_args.containsKey('uId')) {
debugPrint(' _args[uId]---------${_args['uId']}');

CacheManager.instance.userId = _args['uId'];
}
if (_args.containsKey('oId')) {
debugPrint(' _args[oId]---------${_args['oId']}');
CacheManager.instance.userOId = _args['oId'];
}
if (_args.containsKey('token')) {
debugPrint(' _args[token]---------${_args['token']}');

CacheManager.instance.userToken = _args['token'];
}
if (_args.containsKey('headImg')) {
debugPrint(' _args[headImg]---------${_args['headImg']}');
CacheManager.instance.headImgUrl = _args['headImg'];
}
state.userName = CacheManager.instance.oName;
state.userHeaderImg = CacheManager.instance.headImgUrl;
_loadInterface();
}
}

每次都做这个判断是真的恶心,应该把这些东西抽离出来,通过中间件去实现,避免页面上耦合了这个判断。


接下去就是正常的请求接口渲染页面的流程了。


与原生的交互

这里借鉴的是这位大佬的文章 flutterweb与flutter的交互 侵删歉


唯一需要注意的就是在web项目里面增加一个js


1924616-af650f09d9300f88.png
在app里面也要做一点操作:


class NativeBridge implements JavascriptChannel {
BuildContext context; //来源于当前widget, 便于操作UI
Future<WebViewController> _controller; //当前webView 的 controller

NativeBridge(this.context, this._controller);

// api 与具体函数的映射表,可通过 _functions[key](data) 调用函数
get _functions => <String, Function>{
"getUserInfo": _getUserInfo,
"incomeDetail": _incomeDetail,
"incomeHistory": _incomeHistory,
};

@override
String get name =>
"nativeBridge"; // js 通过 nativeBridge.postMessage(msg); 调用flutter

// 处理js请求
@override
get onMessageReceived => (msg) async {
// 将收到的string数据转为json
Map<String, dynamic> message = json.decode(msg.message);
// 异步是因为有些api函数实现可能为异步,如inputText,等待UI相应
// 根据 api 字段,调用具体函数
final data = await _functions[message["api"]](message["data"]);
};

//拿token
_getUserInfo(data) async {
handlerCallback(data);
} //拿token

_incomeDetail(data) async {
Get.toNamed(RouterConf.OLD_STOREKEEPER_INCOME_LIST);
}

_incomeHistory(data) async {
Get.toNamed(RouterConf.STORE_KEEPER_INCOME_HISTORY);
}

handlerCallback(data) async {
LoginModel? _login = await UserManager.getLoginModel();
UserInfoModel? _user = await UserManager.getUserInfo();
String? _name = _user?.resultData?.organization?.organizationName;
String? _uId = _user?.resultData?.user?.userId?.toString() ?? "";
String? _oId =
_user?.resultData?.organization?.organizationId?.toString() ?? "";
String? _token = _login?.resultData?.xAUTHTOKEN;
String? _img = _user?.resultData?.user?.portraitUrl;
_img = ImgSize.getImgUrlThumbnail(_img);
Map<String, dynamic> _infos = {
"name": _name,
"uId": _uId,
"oId": _oId,
"token": _token,
"headImg": _img,
};

if (data['needCallback']) {
var args = data['callbackArgs'];
if (data['needToken']) {
args = "'${data['callbackArgs']}','${jsonEncode(_infos)}'";
}
doCallback(data['callbackName'], args);
}
}

doCallback(name, args) {
_controller.then((value) => value.evaluateJavascript("$name($args)"));
}
}

在webview里面设置channels:


 javascriptChannels: <JavascriptChannel>[
NativeBridge(context, widget.controller!.future)
].toSet(),

结尾


目前来说好像这个方案是可行的,把一个app页面通过网页跑起来确实是挺爽的,但是慢也是真的慢,


也可能因为我的服务器是丐版中的丐版,加载起来是真的慢:


1924616-5860a92dde710996.png


1924616-71b2de0857dcbc1e.png


但是挺好玩的,虽然代码很烂,但是开心就是了。


作者:Mr_凌宇
链接:https://juejin.cn/post/7040061394313543710
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

一步一步完成Flutter应用开发-掘金App文章详情, 悬浮,标题动画

这边文章主要将掘金app的文章详情界面的内容构造和效果的实现,也是完结篇,或者有兴趣的同学们可以谈论想要去实现哪个页面的或者功能都可以谈论起来。一起进步。一个人终究没有一群人会走得远。 标题部分 看了一下掘金app文章详情的效果,我的思路是自定义一个appba...
继续阅读 »

这边文章主要将掘金app的文章详情界面的内容构造和效果的实现,也是完结篇,或者有兴趣的同学们可以谈论想要去实现哪个页面的或者功能都可以谈论起来。一起进步。一个人终究没有一群人会走得远。


标题部分


看了一下掘金app文章详情的效果,我的思路是自定义一个appbar然后,左半部分是一个返回按钮,右部分是点击弹出分享的悬浮窗口,中间部分根据内容列表的滑动进行改变,大体思路就是通过pageView构建中间部分,禁止手势滑动,使用主动触发滑动效果,触发机制是内容滑动改变的距离
效果如下:


tutieshi_640x1343_4s.gif


import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';

class DetailPage extends StatefulWidget {
DetailPage({Key key}) : super(key: key);

@override
_DetailPageState createState() => _DetailPageState();
}

class _DetailPageState extends State<DetailPage> {
PageController controller = new PageController();
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Container(
width: Get.width,
height: 80,
decoration: BoxDecoration(color: Colors.white),
padding: EdgeInsets.only(
top: Get.context.mediaQueryPadding.top, left: 10, right: 10),
child: Row(
children: [
InkWell(
child: Icon(
CupertinoIcons.back,
color: Color.fromRGBO(38, 38, 40, 1),
),
onTap: () {
print('点击了返回');
Get.back();
},
),
Expanded(
child: PageView.builder(
itemBuilder: (context, index) {
if (index == 0) {
return Container(
alignment: Alignment.center,
child: Text('一步一步完成Flutter应用开发-掘金文章详情页面'),
);
}
return Container(
child: Row(
children: [
Container(
height: 20,
width: 20,
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(10)),
)
],
),
);
},
itemCount: 2,
scrollDirection: Axis.vertical,
physics: NeverScrollableScrollPhysics(),
controller: controller,
)),
InkWell(
child: Icon(
Icons.list,
color: Color.fromRGBO(38, 38, 40, 1),
),
onTap: () {
controller.animateTo(40,
duration: Duration(milliseconds: 500),
curve: Curves.ease);
},
),
],
),
)
],
));
}
}

基于这个思想接下来完成下面的内容


内容部分的构建


这部分想要知道掘金内容的返回格式是什么,是markdown内容或者是html内容,如果是html内容传送门,可以参考一下。
效果:


tutieshi_640x1343_5s.gif
这块主要是通过markdown形式展示详情内容引入


flutter_markdown: ^0.5.2

在上述代码中加入详情内容代码


ScrollController _scrollController = new ScrollController();

@override
void initState() {
super.initState();
_scrollController
..addListener(() {
setState(() {
if (_scrollController.offset > 88 && _scrollController.offset < 100) {
controller.animateTo(30,
duration: Duration(milliseconds: 500), curve: Curves.ease);
} else if (_scrollController.offset <= 0) {
controller.animateTo(0,
duration: Duration(milliseconds: 500), curve: Curves.ease);
}
});
});
}

@override
Widget build(BuildContext context) {
...省略上述代码
//_markdownData为内容常量
Expanded(
child: Markdown(
data: _markdownData,
controller: _scrollController,
))
}


悬浮弹窗


使用getX的Get.dialog进行展示悬浮弹窗
效果:


tutieshi_640x1343_4s.gif


代码如下:


renderItem(title) {
return Column(
children: [
Container(
height: 40,
width: 40,
margin: EdgeInsets.only(top: 10),
decoration: BoxDecoration(
color: Colors.red, borderRadius: BorderRadius.circular(20)),
),
Padding(padding: EdgeInsets.only(top: 8)),
Material(
child: Text(title),
)
],
);
}

//调用方法
Get.dialog(
UnconstrainedBox(
alignment: Alignment.bottomCenter,
child: Container(
width: Get.width,
height: Get.height * 0.6,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20))),
child: Column(
children: [
Padding(padding: EdgeInsets.only(top: 40)),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
renderItem('卡片分享'),
renderItem('微信分享'),
renderItem('朋友圈分享'),
renderItem('微博分享'),
],
),
Padding(padding: EdgeInsets.only(top: 20)),
Divider(
height: 2,
color: Colors.grey,
),
Padding(padding: EdgeInsets.only(top: 20)),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
renderItem('卡片分享'),
renderItem('微信分享'),
renderItem('朋友圈分享'),
renderItem('微博分享'),
],
),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
renderItem('卡片分享'),
renderItem('微信分享'),
renderItem('朋友圈分享'),
renderItem('微信分享'),
],
),
Expanded(child: Container()),
GestureDetector(
onTap: () {
Get.back();
},
child: Container(
margin: EdgeInsets.only(
bottom: Get
.context.mediaQueryPadding.bottom),
width: Get.width,
height: 40,
alignment: Alignment.center,
child: Material(
child: Text(
'取消',
),
),
),
),
],
)),
),
useRootNavigator: false,
useSafeArea: false);

over ~~~~


作者:一天清晨
链接:https://juejin.cn/post/6943106056205631502
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

SwiftUI与Swift的区别

iOS
引言 SwiftUI 于 2019 年度 WWDC 全球开发者大会上发布,它是基于 Swift 建立的声明式框架。该框架可以用于 watchOS、tvOS、macOS、iOS 等平台的应用开发,等于说统一了苹果生态圈的开发工具。 本人最早开始 iOS 开发时选...
继续阅读 »

引言


SwiftUI 于 2019 年度 WWDC 全球开发者大会上发布,它是基于 Swift 建立的声明式框架。该框架可以用于 watchOS、tvOS、macOS、iOS 等平台的应用开发,等于说统一了苹果生态圈的开发工具。


本人最早开始 iOS 开发时选择了 OC(Objective-C,一种编程语言),当时 OC 不但拥有各种知名的第三方库和完善的社区支持,同时 Swift 语言本身都还在不断颠覆性改进中。但当我看了 2020 年 WWDC 关于 SwiftUI 一系列课程之后,便从 Swift 语言的学习开始,逐步了解并掌握 SwiftUI,并果断抛弃了OC,将新项目全部迁移到了 SwiftUI 框架。


SwiftUI 到底有没有苹果宣传的那么理想化?资深的 iOS 开发者是否有必要转型,以及如何转型?SwiftUI 在实际使用的过程中真实体验如何?这些问题就是这篇文章希望探讨的话题。


在最后,我会分享一些自己的学习心得和材料顺序,借开源的精神与大家共同进步。


什么是 SwiftUI


对于 Swift UI,官方的定义很好找到,也写得非常明确:



SwiftUI is a user interface toolkit that lets us design apps in a declarative way.

可以理解为 SwiftUI 就是⼀种描述式的构建 UI 的⽅式。



单单通过描述,大部分人其实很难对抽象的编程方法,和其中的改进有直观的认识。这篇文章也希望通过尽量口语化的叙述,减少专业词汇和代码的出现来降低阅读门槛,让更多人了解计算机科学,了解程序的世界。


下面是我手头正在做的一个项目,定位是一个原生全平台的电子阅读应用,正在使用 SwiftUI 构建用户界面。


为什么苹果要推出 SwiftUI


SwiftUI 的两个组成部分,Swift + UI,即是这个问题的答案。


Swift:编程语言和体验的一致性


Swift 代表苹果推出的一种现代编程语言


很多苹果用户之所以喜欢苹果的产品,其中一个原因,是不同产品之间由内而外的统一感和协调感。这一点在硬件层面的感知是最明显的,从早期开始苹果出的硬件就是「果味十足」的。即使是新品迭代或者是开发全新的品类,也一定会带有烙印很深的「果味」工业设计。


仅仅外观的统一还不够,苹果真正追求的是内外一致,也就是体验的统一。


然而工业设计可以交给自家精英团队,系统可以相互借鉴,但用户使用的软件是由广大的开发者自由创造的。让人意外的是,这一点苹果做的也很不错,与别家相比,质量精良是很多人对苹果系统上软件的印象。


为了实现这一目标苹果做了大量不为普通消费者所感知的工作。


在设计上,苹果提供了一整套不断在更新的 Human Interface Guidelines,详细规定了与视觉相关的各个方面。在完成开发,准备上架分发之前,苹果的审核团队会对每一款应用进行审核,根据 App Store Review Guidelines 的条款判断应用是否允许上架 App Store,即使是知名的应用违反规定也是说下架就下架,绝不含糊。对于不越狱的移动设备而言,App Store 是唯一可以安装应用的途径,控制了其中的准入也就等于替整个平台做了筛选。


除了控制终端以外,苹果也在想方设法增加开发者的数量,提升单个应用质量。方式也非常符合第一性思维原则——降低开发的难度。所以先有了Swift,紧接着又推出了 SwiftUI。苹果希望直接优化语言本身,并统一所有设备的开发体验,让开发者更容易上手,也更容易将心里的想法转化为运行的程序。


虽说在 2015 年推出 Swift2.0 的时候就进行了开源。但这些年 Swift 在后端或是跨平台的发展上并不是非常顺利。提起 Swift,圈内还是会被默认为特指苹果平台内使用的编程语言,地位有些类似 OC 的接班者。其实为了推广这门语言,苹果本身也做了非常多的工作,像是推出SwiftUI,基本就可以看做苹果在推广新语言的过程中一个里程碑式的节点。


SwiftUI 使用了大量 Swift 的语言特性,特别是 5.0 之后新增的特性。Swift 5.1 的很多特性几乎可以说都是为了 SwiftUI 量身定制的,比如 Opaque return types、Property Delegate 和 Function builder 等。


UI:开发的困局


在 SwiftUI 出现之前,苹果不同的设备之前的开发框架并不互通,移动端的⼯程师和桌⾯端的⼯程师需要掌握的知识,有很⼤⼀部分是差异化的。


从 iOS SDK2.0 开始,移动端的开发者⼀直使⽤ UIKit 进⾏⻚⾯部分的开发。UIKit 的思想继承了成熟的 AppKit 和MVC(Model-View-Controller)模式,作出了⼀些改进,但本质上改动不⼤。UI 包括了⽤⼾能看到的⼀切,包括静⽌的显⽰和动态的动画。


再到后来苹果推出了Apple Watch,在这块狭小屏幕上,又引入了一种新的布局方式。这种类似堆叠的逻辑,在某种程度上可以看做 SwiftUI 的未完全体。


截止此时,macOS 的开发需要使用 AppKit,iOS 的开发需要使用 UIKit,WatchOS 的开发需要使用堆叠,这种碎片化的开发体验无疑会大大增加开发者所需消耗的时间精力,也不利于构建跨平台的软件体验。


即使单看 iOS 平台,UIKit 也不是完美的开发⽅案。


UIKit 的基本思想要求 ViewController 承担绝⼤部分职责,它需要协调 model,view 以及⽤⼾交互。这带来了巨⼤的 sideeffect 以及⼤量的状态,如果没有妥善安置,它们将在 ViewController 中混杂在⼀起,同时作⽤于 view 或者逻辑,从⽽使状态管理愈发复杂,最后甚⾄不可维护⽽导致项⽬崩溃。换句话说,在不断增加新的功能和⻚⾯后,同⼀个ViewControlle r会越来越庞杂,很容易在意想不到的地⽅产⽣ bug。⽽且代码纠缠在⼀起后也会⼤⼤降低可读性,伤害到维护和协作的效率。


SwiftUI的特点


在很多地方都能看到 SwiftUI 针对现有问题的一些解决思路,而且现在的编程思想经过不断以来的演化,也一直就软件工程在开发过程中的各种问题在寻找答案。


近年来,随着编程技术和思想的进步,使用声明式或者函数式的方式来进行界面开发,已经越来越被接受并逐渐成为主流。SwiftUI 不是第一个,也不会是最后一个使用声明式界面开发的框架。


声明式的界面开发方式


在计算机科学的世界内,抽象是一个很重要的概念。从底层的二进制逻辑门,到人类可以阅读和理解的编程语言之间,是由很多层的抽象将它们关联起来的。所谓抽象,简单解释就是通过封装组件,将底层细节打包并隐藏起来,从而明确逻辑降低复杂度。就像把晶体管打包成逻辑门,以及软件工程中的函数对象。在软件开发的过程中,工程师只需负责某个具体功能的实现,而其他人则通过开放的 api 使用该功能。


与曾经的布局方式相比,声明式的页面开发无疑又加了一层抽象。


在 UIKit 框架中,界面上的每一个元素都需要开发者进行布置,期间有不少计算工作,例如长宽的改变或是屏幕可视面积的变化等。这种线性的方式被称为指令式 (imperative) 编程。以一行文字为例,放置在哪个坐标、宽度多少、在哪里换行、怎么断句、字形字号是多少、最终高度多少、是否需要缩小字号来完全显示等,这些都是开发者在制作界面时要考虑和计算妥当的问题。到了第二年,用户可能会换更大屏幕的手机,系统支持动态字体调节等新功能,此时原先的程序不进行适配就可能出现显示问题,开发者就需要回头进行程序的重新调试。


换做 SwiftUI 之后,上述的很多变量就被系统接管了。开发者要做的就是直观的告诉系统放置一个图像,上面加一行文字,右边加一个按钮。系统会根据屏幕大小、方向等自动渲染这个界面,开发者也不再需要像素级的进行计算。这被称为声明式 (declarative) 编程


对比同一个场景界面的实现


作为常用的列表视图,在UIKit中被称为 TableView,而在 SwiftUI 中被叫做 List 或 Form。同样是实现一个列表,在 SwiftUI 仅需要声明这里有一个 List 以及每个单元的内容即可。而在UIKit 中,需要使用委托代理的方式定制每个单元的内容,还需要事无巨细的设置行和组的数量、对触摸的反应、点击过程等各方面。


在我的另一个早期项目 Amos 时间志中就可以看到,为了绘制主页就需要几千行代码。


智能适配不同尺寸的屏幕


除了不同尺寸的屏幕,SwiftUI 还能根据运行平台的区别,将按钮、对话框、设置项等渲染成匹配的样式。由于声明的留白是很大的,当开发者不需要事无巨细的安排好每一个细节时,系统可操作的空间也会变大。


可以想象,假如苹果推出新品例如眼镜,或许同样的界面代码会被展示成与 iPhone 中完全不同的样式。


提高了解决问题时所需要着手的层级,这可以让开发者可以将更多的注意力集中到更重要的创意方面。


链式调用修改属性


链式调用是 Swift 语言的一种特性,就是用来使用函数方法的一种方式。可以像链条那样不断地调用函数,中间不需要断开。使用这种方式开发者可以给界面元素添加各种属性,只要愿意,同样能够事无巨细的安排页面元素的各种细节。


除了系统预制的属性可以调节外,开发者也可以进行自定义。例如将不同字体、字号、行间距、颜色等属性统合起来,可以组合成为一个叫「标题」的文字属性。之后凡是需要将某一行文字设置成标题,直接添加这个自定义的属性即可。


使用这种方式进行开发无疑能够极大的避免无意义的重复工作,更快的搭建应用界面框架。


界面元素组件化


理论上来讲,每一个复杂的视图,都是由大量简单的单元视图构成。但是函数方法可以包装起来,做到仅在有需要的时候进行调取使用。在 UIKit 框架下的页面元素解耦却不太容易,一般都是针对某种特定情境,很难进行移植。有时候可能手机横屏就会让页面元素混乱,就更别论页面元素的组件化了。


不过 SwiftUI 在布局上的特点,却可以便捷的拆分复杂的视图组件。单一的组件不仅可以自由组合,而且在苹果的任意平台上都可以使用该组件,达到跨平台的实现。


一般我个人会将视图组件区分为基础组件、布局组件和功能组件。因为 SwiftUI 的界面不再像 UIKit 那样,用 ViewController 承载各种 UIVew 控件,而是一切都是视图。这种视图的拼装方式提高了界面开发的灵活性和复用性。


响应式编程框架 Combine


在构建复杂界面的过程中,数据的流通一直是指令式编程中相当让人头疼的部分。


在 UIKit 框架下时,会配合 Target-Action 或者 protocol-delegate 模式来交换信息,使用 Key-Value Observing (KVO) 或者 Key-Value Coding (KVC) 来监测变化和读写属性。但即便开发者熟练地使用这些工具,面对日益增长的应用复杂性,掉坑里的可能性还是非常大。因为有太多需要开发者妥善处理的数据流动,例如数据改动后需要通知相关的页面进行刷新,或是让关联数据重新计算等。


像是 React Native 和 Flutter 这样的移动端跨平台方案,由于采用了声明式 UI 的编写方式和严格的数据流动方向,就能够大幅减轻开发者的思考负担。


SwiftUI 很明显也吸收了这些现代的编程思想,在另一个重量级系统框架 Combine 的协助下,实现了单一数据源的管理。


响应式编程的核心是将所有事件转化成为异步的数据流,这刚好就是 Combine 的主要功能。Combine 采用观察者模式,对应多个观察者,可以分别订阅感兴趣的内容。在 SwiftUI 的界面布局过程中,不同的 View 就是观察者,分别订阅了相关联的属性,并在数据发生变化之后就能够自动的重新渲染。


单一数据源


在 WWDC 的介绍视频中,「Source of truth」这个词反复出现,中文可以将这个词理解为单一数据源。


一直以来复杂的UI结构都会创造更为复杂的数据和逻辑管理需求,每次在用户交互,或是数据来源发生变化的时候,能否及时更新相关界面组件,不然就会引起显示问题。


但是在 SwiftUI 中,只要在属性声明时加上 @State 等关键词,就可以将该属性和界面元素联系起来,在每次数据改动后,都有机会决定是否更新视图。这样就可以将所有的属性都集中到一起进行管理和计算,也不再需要手写刷新的逻辑。因为在 SwiftUI 中,页面渲染前会将开发者描述的界面状态储存为结构体,更新界面就是将之前状态的结构体销毁,然后生成新的状态。而在绘制界面的过程中,会自动比较视图中各个属性是否有变化,如果发生变化,便会更新对应的视图,避免全局绘制和资源浪费。


使用这种方式,读和写都集中在一处,开发者就能够更好地设计数据结构,比较方便的增减类型和排查问题。而不用再考虑线程、原子状态、寻找最新数据等各种细节,再决定通知相关的界面进行刷新


与UIKit彼此相容


一般开发者学习新技术有一个最大的障碍就是原先的项目怎么办。但 SwiftUI 在这一点上做的相当不错。由于是一个新发布的框架,UI 组件并不齐全,当 SwiftUI 中并没有提供类似的功能时,就可以把 UIKit 中已有的部分进行封装,提供给 SwiftUI 使用。需要做的仅仅是遵循UIViewRepresentable协议即可。相反,在已有的项目中,也可以仅用 SwiftUI 制作一部分的 UI 界面。


当然两种代码的风格是截然不同的,但在使用上却基本没有性能的损失。到最终成品时,用户也无法分辨出两种界面框架的不同。


从开发者的⻆度看 SwiftUI


回到开头的问题:SwiftUI 到底有没有苹果宣传的那么理想化?


在 WWDC 发布 SwiftUI 时,有一句话让我印象深刻:「不论多复杂,原先布局的 99% 现在都可以使用 SwiftUI 进行构建」。当我查询 SwiftUI 是否可以承担大型项目开发时,又一次从资深开发者那里看到了这句话。


在我实际体验一段时间,并最终将一款全 SwiftUI 开发的应用上架后,认为这句话并没有什么问题,但前提是对编程这件事需要有比较基础且深入的理解。


这有点像我们学习一些优秀的第三方库时候的感受,同样是用 Xcode 写代码,有些人写出来就是白开水,而另一些人就是黑魔法。学习同样的语言特性,但由于理解的深刻程度不同,在使用时也会大不一样。仅仅依靠一些标准的自带组件无法做出一款出色的应用,即使如 UIKit 那样拥有如此丰富的组件也不行。很多时候还是要根据业务需要,或者是一些独特的脑洞做出最合适的界面。


对于个⼈开发者而言,意味着什么?


SwiftUI 的上限有多高,还要看未来一年一度的更新。但与之前的 UIKit 相比,下限被大大拉低已是不争的事实。这里的所谓下限,指的是学习的难度。由于描述性的布局方式与我们平时的阅读习惯非常接近,告诉系统在页面中间放一个图片就像告诉别人在桌子中间放一个苹果那么直观。我认识的好多 UI 设计师就通过短时间的自学掌握了 SwiftUI,并且搭建起可以直接在真机上使用的 Demo。


降低学习成本这件事是非常有意义的,不仅可以增加开发者数量,降低学习门槛,而且就学习本身而言,让初学者感受到成就感和明确学习方向,长久而言是比短时间的学习效率更重要的事情。只有开始的时候培养足够的兴趣,在后期才可能自主自发的研究更深入更困难的问题。


SwiftUI 和 Combine 大量借助了 Swift 的语法特性,尤其是 5.0 之后的几个更新,新特性就仿佛是为了这两个系统及框架量身定做的一般。虽然将 Swift 开源,但苹果无疑还是牢牢地把握着这门语言的发展。这两个框架和编程语言之间的配合默契,也仿佛让开发者体会到了软硬件一体带来的发展潜力。无论 Swift 出圈后的成果能有多少,在苹果的体系内,无疑是能够将各种消费层面的软件体验整合统一。


只要使用 SwiftUI,系统会默认支持白天和黑夜模式的自动切换;在各种尺寸的屏幕间自动适配;为任意控件添加 Haptic Touch 或是动画;在 Apple Watch 上带来独立而完整的体验;将iOS 的应用转换为 macOS 的原生应用,会以最快的速度支持第一方的各种新特性。这种对苹果硬件的深入支持是那些跨平台方案无论如何无法实现的。可以看一些采用第三方框架的知名应用,像是横屏、黑夜模式、小组件等基础的特性,到现在都迟迟没有适配。


所以 SwiftUI 对小工作室或是独立开发者来说是件好事,可以让新的想法快速落地并且接受市场验证,真正的做到敏捷开发。以这种方式在市场中的细分领域获得一席之地,也能让更多人体会到编写程序的感受,甚至是创造财富。


开源我的学习心得


在最后的部分我会分享一些自己学习 SwiftUI 的过程和介绍相关的资源,给一些也对开发感兴趣的小伙伴们做个参考。


首先要学习的是 Swift 编程语言,它与 OC 之间的差别还是挺大的,学习也没有什么捷径,直接阅读官方教程,对照着实例自己写一遍就行。国内有几个非常好的汉化网站,可以一起对照学习。基本上没有必要特意买书,反而不如直接电脑上看了就敲来的方便。



  1. 官网

  2. SwiftGG

  3. GitHub 汉化库


对语言有了大概的了解后,就可以开始对 SwiftUI 的学习,假如遇到问题可以反复回去查看之前的资料。很多被忽略的细节,或是当时初看没概念的部分,结合具体的案例就能够有比较透彻的理解。



  1. 斯坦福公开课 CS193P·2020 年春:该课程强推,我当年学习 OC 看的就是它,现在到SwiftUI了还是先看这个,系统且细致,结合案例和编程过程中的小技巧介绍,是很好的入门课程。

  2. 苹果官方 SwiftUI 课程:打开Xcode,照着官方的教学,从头到尾学着做一遍应用。

  3. Hacking with swift:这是国外一个程序员用业余时间搭建的分享网站,有大量的文章可以阅读,还有推荐初学者跟着做的「100 Days of SwiftUI」课程。

  4. 苹果官方文档:文档是必读的,虽然很多文档缺乏工程细节,但是文档涉及很多概念性的内容,你可以知道官方是怎么思考的,并且有很多具体的机制参数。我本人有一个习惯,要是工程涉及某个框架,会把相关的文档都翻译一遍。

  5. Stack Overflow:有问题查询专用,在谷歌中搜索错误代码或者关键词基本都会由该网站给答案。

  6. 阅读 SwiftUI 库的源代码。


基本到此假如能够顺利完成下来,就可以开启自己的项目。开发想要提高的关键就是亲自写代码和不断地阅读学习。初期学习的关键能力就是英语,而到后期需要的就是真正的兴趣和一些数学能力。


作者:洋仔
链接:https://juejin.cn/post/6997313521067229214
收起阅读 »

Swift:基石库——R.swift

iOS
这是我参与更文挑战的第4天,活动详情查看: 更文挑战何为基石库?做一个App无外乎两大要素:获取数据通过数据驱动页面也许你的App没有网络请求或者网络请求少,你可以不需要Alamofire。也许你的App的UI不是特别复杂,简单的xib和storyb...
继续阅读 »

这是我参与更文挑战的第4天,活动详情查看: 更文挑战

何为基石库?

做一个App无外乎两大要素:

  • 获取数据

  • 通过数据驱动页面

也许你的App没有网络请求或者网络请求少,你可以不需要Alamofire。

也许你的App的UI不是特别复杂,简单的xib和storyboard就可以胜任。

但是在当下一个App中,图片资源、字符串资源等,作为一个App开发者,你是不得不用的。

举个栗子,传统的获取一个image资源我们都是这么写:

let image = UImage(named: "saber")

这么写的最大弊端就是saber这是一个字符串硬编码,靠的的是纯手工敲打,一旦出错,界面就会出现异常。

在开发中,需要尽量避免这种硬编码,如何高效将这种硬编码的表达方式更换为高效安全的方式,就由本次的主角出场了--R.swift

统和所有的资源,以现代化的方式引用资源,项目中使用它,虽然不会让你的App上升一个层次,不过却给你的编码极度舒适。

let image = R.image.saber()

同样是Ex咖喱棒,味道却完全不同,哈哈。

基石库就是那些你无法避免不得不用的库,而R.swift恰恰就是。

R.swift

何为R,即Resource的缩写,我们先看看官方给出的一些例子:

使用R.swift函数前:

let icon = UIImage(named: "settings-icon")
let font = UIFont(name: "San Francisco", size: 42)
let color = UIColor(named: "indicator highlight")
let viewController = CustomViewController(nibName: "CustomView", bundle: nil)
let string = String(format: NSLocalizedString("welcome.withName", comment: ""), locale: NSLocale.current, "Arthur Dent")

使用R.swift函数后:

let icon = R.image.settingsIcon()
let font = R.font.sanFrancisco(size: 42)
let color = R.color.indicatorHighlight()
let viewController = CustomViewController(nib: R.nib.customView)
let string = R.string.localizable.welcomeWithName("Arthur Dent")

所有的资源都函数化后,编写过程想出错都难,特别需要注意的是最后一个涉及国际化的函数R.string.localizable.welcomeWithName("Arthur Dent"),Arthur Dent这个字符串需要自己具体制定,可以通过在做国际化时,通过info.strings进行处理。

R.swift目前支持下面这些资源文件管理:

  • Images
  • Fonts
  • Resource files
  • Colors
  • Localized strings
  • Storyboards
  • Segues
  • Nibs
  • Reusable cells

基本上覆盖了绝大多数的App中的资源管理。

安装和使用

安装

R.swift使用其他特别舒服,不过它的安装确实比其他的第三方库稍微麻烦一点,正所谓工欲善其事必先利其器,这一点麻烦是值得的。

1.添加'R.swift' 在工程的Podfile文件中,并运行pod install。 2.如下图所示。添加脚本:

image.png

3.如下图所示,移动脚本的位置,让它在Compile Sources phase和Check Pods Manifest.lock之间:

image.png

4.添加脚本:

image.png

在shell,下面这一栏添加"$PODS_ROOT/R.swift/rswift" generate "$SRCROOT/R.generated.swift"

在input Files通过+号添加$TEMP_DIR/rswift-lastrun

在Output Files通过+号添加$SRCROOT/R.generated.swift

5.运行添加R.generated.swift:

完成第4步后,进行command + B编译,然后在工程的根目录下面会找到R.generated.swift文件: image.png

将这个文件拖入到工程中,并且不要勾选Copy items if needed

image.png

这样,R.swift就安装完成啦。

使用

每一次添加了新的资源文件,就运行一次command + B一次,这样R.generated.swift文件就将新加入的资源文件更新,使用使用的时候只用通过R.来进行引用了。

更多用法,参考上面写的例子,以及官方文档

明天周末怎么破?

最怕周末更文,因为作为一个奶爸,休息都不是自己的,我争取做到不水文,至少讲一些知识点,明日继续,大家加油。


收起阅读 »

Swift:解包的正确姿势

iOS
嗯,先来一段感慨 在掘金里面看见iOS各路大神各种底层与runtime,看得就算工作了好几年的我也一脸蒙圈,于是只好从简单的入手。 文章最初发布在简书上面,有段时间了,考虑以后大部分时间都会在掘金学习,于是把文章搬过来了。稍微做了点润色与排版。 对于Swift...
继续阅读 »

嗯,先来一段感慨


在掘金里面看见iOS各路大神各种底层与runtime,看得就算工作了好几年的我也一脸蒙圈,于是只好从简单的入手。


文章最初发布在简书上面,有段时间了,考虑以后大部分时间都会在掘金学习,于是把文章搬过来了。稍微做了点润色与排版。


对于Swift学习而言,可选类型Optional是永远绕不过的坎,特别是从OC刚刚转Swift的时候,可能就会被代码行间的?与!,有的时候甚至是??搞得稀里糊涂的。


这篇文章会给各位带来我对于可选类型的一些认识以及如何进行解包,其中会涉及到Swift中if let以及guard let的使用以及思考,还有涉及OC部分的nullablenonnull两个关键字,以及一点点对两种语言的思考。


var num: Int? 它是什么类型?


在进行解包前,我们先来理解一个概念,这样可能更有利于对于解包。


首先我们来看看这样一段代码:



var num: Int?

num = 10

if num is Optional<Int> {

print("它是Optional类型")

}else {

print("它是Int类型")

}



请先暂时不要把这段代码复制到Xcode中,先自问自答,num是什么类型,是Int类型吗?


好了,你可以将这段代码复制到Xcode里去了,然后在Xcode中的if上一定会出现这样一段话:



'is' test is always true



num不是Int类,它是Optional类型


那么Optional类型是啥呢--可选类型,具体Optional是啥,Optional类型的本质实际上就是一个带有泛型参数的enum类型,各位去源码中仔细看看就能了解到,这个类型和Swift中的Result类有异曲同工之妙。


var num: Int?这是一个人Optional的声明,意思不是“我声明了一个Optional的Int值”,而是“我声明了一个Optional类型,它可能包含一个Int值,也可能什么都不包含”,也就是说实际上我们声明的是Optional类型,而不是声明了一个Int类型!


至于像Int!或者Int?这种写法,只是一种Optional类型的糖语法写法。


以此类推String?是什么类型,泛型T?是什么类型,答案各位心中已经明了吧。


正是因为num是一个可选类型。所以它才能赋值为nil, var num: Int = nil。这样是不可能赋值成功的。因为Int类型中没有nil这个概念!


这就是Swift与OC一个很大区别,在OC中我们的对象都可以赋值为nil,而在Swift中,能赋值为nil只有Optional类型!


解包的基本思路,使用if let或者guard let,而非强制解包


我们先来看一个简单的需求,虽然这个需求在实际开发中意义不太大:


我们需要从网络请求获取到的一个人的身高(cm为单位)以除以100倍,以获取m为单位的结果然后将其结果进行返回。


设计思路:


由于实际网络请求中,后台可能会返回我们的身高为空(即nil),所以在转模型的时候我们不能定义Float类型,而是定义Float?便于接受数据。


如果身高为nil,那么nil除以100是没有意义的,在编译器中Float?除以100会直接报错,那么其返回值也应该为nil,所以函数的返回值也是Float?类型


那么函数应该设计成为这个样子是这样的:



func getHeight(_ height: Float?) -> Float?



如果一般解包的话,我们的函数实现大概会写成这样:



func getHeight(_ height: Float?) -> Float? {

if height != nil {

return height! / 100

}

return nil

}



使用!进行强制解包,然后进行运算。


我想说的是使用强制解包固然没有错,不过如果在实际开发中这个height参数可能还要其他用途,那么是不是每使用一次都要进行强制解包?


强制解包是一种很危险的行为,一旦解包失败,就有崩溃的可能,也许你会说这不是有if判断,然而实际开发中,情况往往比想的复杂的多。所以安全的解包行为应该是通过if let 或者guard let来进行。



func getHeight(_ height: Float?) -> Float? {

if let unwrapedHeight = height {

return unwrapedHeight / 100

}

return nil

}



或者:



func getHeight(_ height: Float?) -> Float? {

guard let unwrapedHeight = height else {

return nil

}

return unwrapedHeight / 100

}



那么if let和guard let 你更倾向使用哪个呢?


在本例子中,其实感觉二者的差别不大,不过我个人更倾向于使用guard let。




原因如下:


在使用if let的时候其大括号类中的情况才是正常情况,而外部主体是非正常情况的返回的nil;


而在使用guard let的时候,guard let else中的大括号是异常情况,而外部主体返回的是正常情况。


对于一个以返回结果为目的的函数,函数主体展示正常返回值,而将异常抛出在判断中,这样不仅逻辑更清晰,而且更加易于代码阅读。




解包深入


有这么一个需求,从本地路径获取一个json文件,最终将其转为字典,准备进行转模型操作。


在这个过程中我们大概有这么几个步骤:


1. 获取本地路径 


func path(forResource name: String?, ofType ext: String?) -> String?


2. 将本地路径读取转为Data 


init(contentsOf url: URL, options: Data.ReadingOptions = default) throws


3. JSON序列化


class func jsonObject(with data: Data, options opt: JSONSerialization.ReadingOptions = []) throws -> Any


4. 是否可以转为字典类型


我们可以看到以上几个函数中,获取路径获取返回的路径结果是一个可选类型而转Data的方法是抛出异常,JSON序列化也是抛出异常,至于最后一步的类型转换是使用as? [Sting: Any]这样的操作


这个函数我是这来进行设计与步骤分解的:


函数的返回类型为可选类型,因为下面的4步中都有可能失败进而返回nil。


虽然有人会说第一步获取本地路径,一定是本地有的才会进行读取操作,但是作为一个严谨操作,凡事和字符串打交道的书写都是有隐患的,所以我这里还是用了guard let进行守护。


这个函数看起来很不简洁,每一个guard let 后面都跟着一个异常返回,甚至不如使用if let看着简洁


但是这么写的好处是:在调试过程中你可以明确的知道自己哪一步出错



func getDictFromLocal() -> [String: Any]? {

/// 1 获取路径

guard let path = Bundle.main.path(forResource: "test", ofType:"json") else {

return nil

}

/// 2 获取json文件里面的内容

guard let jsonData = try? Data.init(contentsOf: URL.init(fileURLWithPath: path)) else {

return nil

}

/// 3 解析json内容

guard let json = try? JSONSerialization.jsonObject(with: jsonData, options:[]) else {

return nil

}

/// 4 将Any转为Dict

guard let dict = json as? [String: Any] else {

return nil

}

return dict

}



当然,如果你要追求简洁,这么写也未尝不可,一波流带走



func getDictFromLocal() -> [String: Any]? {

guard let path = Bundle.main.path(forResource: "test", ofType:"json"),

let jsonData = try? Data.init(contentsOf: URL.init(fileURLWithPath: path)),

let json = try? JSONSerialization.jsonObject(with: jsonData, options:[]),

let dict = json as? [String: Any] else {

return nil

}

return dict

}



guard let与if let不仅可以判断一个值的解包,而且可以进行连续操作


像下面这种写法,更加追求的是结果,对于一般的调试与学习,多几个guard let进行拆分,未尝不是好事。


至于哪种用法更适合,因人而异。


可选链的解包


至于可选链的解包是完全可以一步到位,假设我们有以下这个模型。



class Person {

var phone: Phone?

}

class Phone {

var number: String?

}



Person类中有一个手机对象属性,手机类中有个手机号属性,现在我们有位小明同学,我们想知道他的手机号。


小明他不一定有手机,可能有手机而手机并没有上手机号码。



let xiaoming = Person()

guard let number = xiaoming.phone?.number else {

return

}

print(number)



这里只是抛砖引玉,更长的可选链也可以一步到位,而不必一层层进行判断,因为可选链中一旦有某个链为nil,那么就会返回nil。


nullable和nonnull


我们先来看这两个函数,PHImageManager在OC与Swift中通过PHAsset实例获取图片的例子



[[PHImageManager defaultManager] requestImageForAsset:asset targetSize:size contentMode:PHImageContentModeDefault options:options resultHandler:^(UIImage * _Nullable result, NSDictionary * _Nullable info) {

//、 非空才进行操作 注意_Nullable,Swift中即为nil,注意判断

if (result) {

}

}];




PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: .default, options: options, resultHandler: { (result: UIImage?, info: [AnyHashable : Any]?) in

guard let image = result else { return }

})



在Swift中闭包返回的是两个可选类型,result: UIImage?与info: [AnyHashable : Any]? 


而在OC中返回的类型是 UIImage * _Nullable result, NSDictionary * _Nullable info


注意观察OC中返回的类型UIImage * 后面使用了_Nullable来修饰,至于Nullable这个单词是什么意思,我想稍微有点英文基础的应该一看就懂--"可以为空",这不恰恰和Swift的可选类型呼应吗?


另外还有PHFetchResult遍历这个函数,我们再来看看在OC与Swift中的表达



PHFetchResult *fetchResult;

[fetchResult enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {

}];




let fetchResult: PHFetchResult

fetchResult.enumerateObjects({ (obj, index, stop) in

})



看见OC中Block中的回调使用了Nonnull来修饰,即不可能为空,不可能为nil,一定有值,对于使用这样的字符修饰的对象,我们就不必为其做健壮性判断了。


这也就是nullable与nonnull两个关键字出现的原因吧--与Swift做桥接使用以及显式的提醒对象的状态


一点点Swift与OC的语言思考


我之前写过一篇文章,是说有关于一个字符串拼接函数的


从Swift来反思OC的语法


OC函数是这样的:



- (NSString *)stringByAppendingString:(NSString *)aString;



Swift中函数是这样的:



public mutating func append(_ other: String)



仅从API来看,OC的入参是很危险的,因为类型是NSString *


那么nil也可以传入其中,而传入nil的后果就是崩掉,我觉得对于这种传入参数为nil会崩掉的函数需要特别提醒一下,应该写成这样:



- (NSString *)stringByAppendingString:(NSString * _Nonnull)aString;

/// 或者下面这样

- (NSString *)stringByAppendingString:(nonnull NSString *)aString;



以便告诉程序员,入参不能为空,不能为空,不能为空,重要的事情说三遍!!!


反观Swift就不会出现这种情况,other后面的类型为String,而不是String?,说明入参是一个非可选类型。


基于以上对于代码的严谨性,所以我才更喜欢使用Swift进行编程。


当然,Swift的严谨使得它失去部分的灵活性,OC在灵活性上比Swift卓越。


作者:season_zhu
链接:https://juejin.cn/post/6931154052776460302

收起阅读 »

iOS 无感知上拉

iOS
本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!RxSwift编写wanandroid客户端现已开源目前RxSwift编写wanandroid客户端已经开源了——项目链接。记得给个star喔!附上一张效果图片:本篇文章是从6月更...
继续阅读 »

本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

RxSwift编写wanandroid客户端现已开源

目前RxSwift编写wanandroid客户端已经开源了——项目链接。记得给个star喔!

附上一张效果图片:

RPReplay_Final1625472730.2021-07-05 16_13_58.gif

本篇文章是从6月更文中热心网友的留言中进行的开发与探索:

Snip20210709_1.png

6月确实因为日更的原因,这个功能没有实现,趁着7月的时候,解决了。

废话了这么多,那么我们进入主题吧。

什么是无感知上拉加载更多

什么是无感知,这个这样理解:在网络情况正常的情况下,用户对列表进行连续的上拉时,该列表可以无卡顿不停出现新的数据。

如果要体验话,Web端很多已经做到了,比如掘金的首页,还有比如掘金iOS的App,列表都是无感知上拉加载更多。

说来惭愧,写了这久的代码,还真的没有认真思考这个功能怎么实现。

如何实现无感知上拉加载更多

我在看见这位网友留言的时候,就开始思考了。

在我看来,有下面几个着手点:

  • 列表滑动时候的是如何知道具体滑动的位置以触发接口请求,添加更多数据?

  • 从UIScrollView的代理回调中去找和scrollView的位置(contentOffset)大小(contentSize)关系密切的回调。

  • 网络上有没有比较成熟的思路?

顺着这条线,我先跑去看了UIScrollViewDelegate的源码:

public protocol UIScrollViewDelegate : NSObjectProtocol {


@available(iOS 2.0, *)
optional func scrollViewDidScroll(_ scrollView: UIScrollView) // any offset changes

@available(iOS 3.2, *)
optional func scrollViewDidZoom(_ scrollView: UIScrollView) // any zoom scale changes

.
.
.
.
.
.
/// 代码很多,这里就不放上来,给大家压力了。
}

直接上结论吧:看了一圈,反正没有和contentSize或者位置相关的回调代理。scrollViewDidScroll这个回调里面虽然可以回参scrollView,但是对于我们需要的信息还不够具体。

思考:既然UIScrollViewDelegate的代理没有现成的代理回调,自己使用KVO去监听试试?

网上的思路(一)

就在我思考的同时,我也在网络上需求实现这个功能的答案,让后看到这样一个思路:

实现方法很简单,需要用到tableView的一个代理方法,就可轻松实现。- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath就是这个方法,自定义显示cell。这个方法不太常用。但是这个方法可在每个cell将要第一次出现的时候触发。然后我们可设置当前页面第几个cell将要出现时,触发请求加载更多数据。

我看了之后,心想着,多写一个TableView的代理,总比写KVO的代码少,先试试再说,于是代码撸起:

extension SwiftCoinRankListController: UITableViewDelegate {

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let row = indexPath.row
let distance = dataSource.count - 25
print("row: \(row), distance:\(distance) ")
if row == distance {
loadMore()
}
}
}

本代码可以在开源项目中的SwiftCoinRankListController.swift文件查看具体的逻辑,其主要就是通过cell显示的个数去提前请求加载数据,然后我们看看效果:

620A94AE4920C54C6E1B85E1776AC83C.2021-07-09 17_47_45.gif

Gif可能看起来还好,我说我调试的感受:

虽然做到了上拉无感知,但是当手滑的速度比较快的时候,到底了新的数据没有回来,就会在底部等一段时间。

功能达到了,但是感受却不理想,果然还是监听的细腻程度不够。

网上的思路(二)

然后在继续的搜索中,我看到了另外一个方案:

很多时候我们上拉刷新需要提前加载新数据,这时候利用MJRefreshAutoFooter的属性triggerAutomaticallyRefreshPercent就可以实现,该属性triggerAutomaticallyRefreshPercent默认值为1,然后改成0的话划到底部就会自动刷新,改成-1的话,在快划到底部44px的时候就会自动刷新。

MJRefresh?使用MJRefreshAutoFooter,这个简单,我直接把基类的footer给替换掉就可以了,本代码可以在开源项目中的BaseTableViewController.swift文件查看:

/// 设置尾部刷新控件,更新为无感知加载更多
let footer = MJRefreshAutoFooter()
footer.triggerAutomaticallyRefreshPercent = -1
tableView.mj_footer = footer

再来看看效果:

992BC78FBAC7B8CB36A6DC679897DA21.2021-07-09 18_04_09.gif

直接说感受:

代码改动性少,编写简单,达到预期效果,爽歪歪。比方案一更丝滑,体验好。

到此,功能就实现,难道就完了?

当然,不会,我们去看看源码吧。

MJRefresh代码的追根朔源

首先我们看看MJRefreshAutoFooter.h文件:

image.png

这里有个专门的属性triggerAutomaticallyRefreshPercent去做自动刷新,那么我们去MJRefreshAutoFooter.m中去看看吧:

image.png

注意看喔,这个.m文件有一个- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change方法,并且还调用了super,从这个方法名中我们可以明显的得到当scrollView的contentOffset变化的时候进行回调的监听。,我们顺藤摸瓜,看看super是什么,会不会有新的发现:

image.png

稍微跟着一下源代码,MJRefreshAutoFooter的继承关系如下:

MJRefreshAutoFooter => MJRefreshFooter => MJRefreshComponent

所以这个super的调用我们就去MJRefreshComponent.m里面去看看吧:

image.png

通过上面的截图我们可以得到下面的一些信息与结论:

  • MJRefreshComponent是通过KVO去监听scrollView的contentOffset变化,思路上我们对齐一致了。

  • 该类并没有实现其具体方法,而是将其交由其子类去实现,这一点通过看MJRefreshComponent.h的注释可以得到:

image.png

  • MJRefreshComponent从本质上更像虚基类。

总结

如果不是掘友提出这个问题,我可能都不会太仔细的去研究这个功能,也许继续普普通通的使用一般的上拉加载更多就够了。

这次的实践,其实是从思路到寻找方法,最后再到源码阅读。

思路也许不困难,但是真正一点点实现并完善功能,每一步都并不容易,这次我也仅仅是继续使用了MJRefresh这个轮子。

想起有一天,在群里吹水看见的一张图:

云程序员来了.jpeg

灵魂拷问,直击人心,大部分时间我们不也是云程序员呢?

知行合一方能开拓新的天地。


收起阅读 »

JVM整体结构

JVM结构图类加载子系统加载连接初始化使用卸载运行时数据区域栈帧数据结构动态链接内存管理Java内存区域从逻辑上分为堆区和非堆区,Java8以前,非堆区又称为永久区,Java8以后统一为原数据区,堆区按照分代模型分为新生代和老年代,其中新生代分为Eden、so...
继续阅读 »

Java虚拟机主要负责自动内存管理、类加载与执行、主要包括执行引擎、垃圾回收器、PC寄存器、方法区、堆区、直接内存、Java虚拟机栈、本地方法栈、及类加载子系统几个部分,其中方法区与Java堆区由所有线程共享、Java虚拟机栈、本地方法栈、PC寄存器线程私有,宏观的结构如下图所示:

JVM结构图

类加载子系统

从文件或网络中加载Class信息,类信息存放于方法区,类的加载包括加载->验证->准备->解析->初始化->使用->卸载几个阶段,详细流程后续文章会介绍。

  • 加载

从文件或网络中读取类的二进制数据、将字节流表示的静态存储结构转换为方法区运行时数据结构、并于堆中生成Java对象实例,类加载器既可以使用系统提供的加载器(默认),也可以自定义类加载器。

  • 连接

连接分为验证、准备、解析3个阶段,验证阶段确保类加载的正确性、准备阶段为类的静态变量分配内存,并将其初始化为默认值、解析阶段将类中的符号引用转换为直接引用

  • 初始化

初始化阶段负责类的初始化,Java中类变量初始化的方式有2种,声明类变量时指定初始值、静态代码块指定初始化,只有类被主动使用时才会触发类的初始化,类的初始化会先初始化父类,然后再初始化子类。

  • 使用

类访问方法区内的数据结构的接口,对象是堆区的数据

  • 卸载

程序执行了System.exit()、程序正常执行结束、JVM进程异常终止等

运行时数据区域

程序从静态的源代码到编译成为JVM执行引擎可执行的字节码,会经历类的加载过程,并于内存空间开辟逻辑上的运行时数据区域,便于JVM管理,其中各数据区域如下,其中垃圾回收JVM会自动自行管理

栈帧数据结构

Java中方法的执行在虚拟机栈中执行,为每个线程所私有,每次方法的调用和返回对应栈帧的压栈和出栈,其中栈帧中保存着局部变量表、方法返回地址、操作数栈及动态链接信息。

动态链接

Java中方法执行过程中,栈帧保存方法的符号引用,通过动态链接,将解析为符号引用。

内存管理

  • 内存划分(逻辑上)

Java内存区域从逻辑上分为堆区和非堆区,Java8以前,非堆区又称为永久区,Java8以后统一为原数据区,堆区按照分代模型分为新生代和老年代,其中新生代分为Eden、so、s1,so和s1是大小相同的2块区域,生产环境可以根据具体的场景调整虚拟机内存分配比例参数,达到性能调优的效果。

堆区是JVM管理的最大内存区域,由所有线程共享,采用分代模型,堆区主要用于存放对象实例,堆可以是物理上不连续的空间,逻辑上连续即可,其中堆内存大小可以通过虚拟机参数-Xmx、-Xms指定,当堆无法继续扩展时,将抛出OOM异常。

  • 运行时实例

假设存在如下的SimpleHeap测试类,则SimpleHeap在内存中的堆区、Java栈、方法区对应的映射关系如下图所示:

public class SimpleHeap {
   /**
    * 实例变量
    */
   private int id;

   /**
    * 构造器
    *
    * @param id
    */
   public SimpleHeap(int id) {
       this.id = id;
  }

   /**
    * 实例方法
    */
   private void displayId() {
       System.out.println("id:" + id);
  }

   public static void main(String[]args){
       SimpleHeap heap1=new SimpleHeap(1);
       SimpleHeap heap2=new SimpleHeap(2);
       heap1.displayId();
       heap2.displayId();
  }

}
复制代码


同理,建设存在Person类,则创建对象实例后,内存中堆区、Java方法栈、方法区三者关系如下图:

直接内存

Java NIO库允许Java程序使用直接内存,直接内存是独立于Java堆区的一块内存区域,访问内存的速度优于堆区,出于性能考虑,针对读写频繁的场景,可以直接操作直接内存,它的大小不受Xmx参数的限制,但堆区内存和直接内存总和必须小于系统内存。

PC寄存器

线程私有空间,又称之为程序计数器,任意时刻,Java线程总在执行一个方法,正在执行的方法,我们称之为:"当前方法”,如果当前方法不是本地方法,则PC寄存器指向当前正在被执行的指令;若当前方法是本地方法,则PC寄存器的值是undefined。

垃圾回收系统

垃圾回收系统是Java虚拟机中的重要组成部分,其主要对方法区、堆区、直接内存空间进行回收,与C/C++不同,Java中所有对象的空间回收是隐式进行的,其中垃圾回收会根据GC算法自动完成内存管理。


作者:洞幺幺洞
来源:https://juejin.cn/post/7040742081236566029

收起阅读 »

Apache Log4j 漏洞(JNDI注入 CVE-2021-44228)

本周最热的事件莫过于 Log4j 漏洞,攻击者仅需向目标输入一段代码,不需要用户执行任何多余操作即可触发该漏洞,使攻击者可以远程控制用户受害者服务器,90% 以上基于 java 开发的应用平台都会受到影响。通过本文特推项目 2 你也能近距离感受这个漏洞的“魅力...
继续阅读 »



本周最热的事件莫过于 Log4j 漏洞,攻击者仅需向目标输入一段代码,不需要用户执行任何多余操作即可触发该漏洞,使攻击者可以远程控制用户受害者服务器,90% 以上基于 java 开发的应用平台都会受到影响。通过本文特推项目 2 你也能近距离感受这个漏洞的“魅力”,而特推 1 则是一个漏洞检测工具,能预防类似漏洞的发生。

除了安全相关的 2 个特推项目之外,本周 GitHub 热门项目还有高性能的 Rust 运行时项目,在你不知道用何词时给你参考词的反向词典 WantWords,还有可玩性贼高的终端模拟器 Tabby。

以下内容摘录自微博@HelloGitHub 的 GitHub Trending 及 Hacker News 热帖(简称 HN 热帖),选项标准:新发布 | 实用 | 有趣,根据项目 release 时间分类,发布时间不超过 14 day 的项目会标注 New,无该标志则说明项目 release 超过半月。由于本文篇幅有限,还有部分项目未能在本文展示,望周知 🌝

  • 本文目录

      1. 本周特推

      • 1.1 JNDI 注入测试工具:JNDI-Injection-Exploit

      • 1.2 Apache Log4j 远程代码执行:CVE-2021-44228-Apache-Log4j-Rce

      1. GitHub Trending 周榜

      • 2.1 塞尔达传说·时之笛反编译:oot

      • 2.2 终端模拟器:Tabby

      • 2.3 反向词典:WantWords

      • 2.4 CPU 性能分析和调优:perf-book

      • 2.5 高性能 Rust Runtime:Monoio

      1. 往期回顾

1. 本周特推

1.1 JNDI 注入测试工具:JNDI-Injection-Exploit

本周 star 增长数: 200+

JNDI-Injection-Exploit 并非是一个新项目,它是一个可用于 Fastjson、Jackson 等相关漏洞的验证的工具,作为 JNDI 注入利用工具,它能生成 JNDI 链接并启动后端相关服务进而检测系统。

GitHub 地址→github.com/welk1n/JNDI…

1.2 Apache Log4j 远程代码执行:CVE-2021-44228-Apache-Log4j-Rce

本周 star 增长数: 1,500+

New CVE-2021-44228-Apache-Log4j-Rce 是 Apache Log4j 远程代码执行,受影响的版本 < 2.15.0。项目开源 1 天便标星 1.5k+ 可见本次 Log4j 漏洞受关注程度。

GitHub 地址→github.com/tangxiaofen…

2. GitHub Trending 周榜

2.1 塞尔达传说·时之笛反编译:oot

本周 star 增长数:900+

oot 是一个反编译游戏塞尔达传说·时之笛的项目,目前项目处于半成品状态,会有较大的代码变更,项目从 scratch 中重新构建代码,并用游戏中发现的信息以及静态、动态分析。如果你想通过这个项目了解反编译知识,建议先保存好个人的塞尔代资产。

GitHub 地址→github.com/zeldaret/oo…

2.2 终端模拟器:Tabby

本周 star 增长数:1,800+

Tabby(曾叫 Terminus)是一个可配置、自定义程度高的终端模拟器、SSH 和串行客户端,适用于 Windows、macOS 和 Linux。它是一种替代 Windows 标准终端 conhost、PowerShell ISE、PuTTY、macOS terminal 的存在,但它不是新的 shell 也不是 MinGW 和 Cygwin 的替代品。此外,它并非一个轻量级工具,如果你注重内存,可以考虑 ConemuAlacritty

GitHub 地址→github.com/Eugeny/tabb…

2.3 反向词典:WantWords

本周 star 增长数:1,000+

WantWords 是清华大学计算机系自然语言处理实验室(THUNLP)开源的反向字词查询工具,反向词典并非是查询反义词的词典,而是基于目前网络词官广泛使用导致部分场景下,我们表达某个意思未能找到精准的用词,所以它可以让你通过想要表达的意思来找寻符合语境的词汇。你可以在线体验反向词典:wantwords.thunlp.org/ 。下图分别为项目 workflow 以及查询结果。

GitHub 地址→github.com/thunlp/Want…

2.4 CPU 性能分析和调优:perf-book

本周 star 增长数:1,300+

perf-book 是书籍《现代 CPU 的性能分析和调优》开源版本,你可以通过 python.exe export_book.py && pdflatex book.tex && bibtex book && pdflatex book.tex && pdflatex book.tex 命令导出 pdf。

GitHub 地址→github.com/dendibakh/p…

2.5 高性能 Rust Runtime:Monoio

本周 star 增长数:1,250+

New Monoio 是字节开源基于 io-uring 的 thread-per-core 模型高性能 Rust Runtime,旨在为高性能网络中间件等场景提供必要的运行时。详细项目背景可以阅读团队的文章Monoio:基于 io-uring 的高性能 Rust Runtime

GitHub 地址→github.com/bytedance/m…

3. 往期回顾

以上为 2021 年第 50 个工作周的 GitHub Trending 🎉如果你 Pick 其他好玩、实用的 GitHub 项目,记得来 HelloGitHub issue 区和我们分享下哟 🌝

最后,记得你在本文留言区留下你想看的主题 Repo(限公众号),例如:AI 换头。👀 和之前的送书活动类似,留言点赞 Top5 的小伙伴(),小鱼干会努力去找 Repo 的^^

作者:HelloGitHub
来源:https://juejin.cn/post/7040980646423953439

收起阅读 »

又到公祭日,App快速实现“哀悼主题”方案

今天是南京大屠杀死难者国家公祭日,很多APP已经变成哀悼主题,怎么实现的呢?看看这个Android方案! 4月4日,今天是国家,为了纪念和哀悼为新冠疫情做出努力和牺牲的烈士以及在新冠中逝去的同胞“举行全国哀悼”的日子! 今天10时全国停止一切娱乐活动,并默...
继续阅读 »


今天是南京大屠杀死难者国家公祭日,很多APP已经变成哀悼主题,怎么实现的呢?看看这个Android方案!

原标题:App快速实现“哀悼主题”方案

4月4日,今天是国家,为了纪念和哀悼为新冠疫情做出努力和牺牲的烈士以及在新冠中逝去的同胞“举行全国哀悼”的日子!
今天10时全国停止一切娱乐活动,并默哀3分钟!至此,各大app(爱奇艺、腾讯、虎牙...)响应号召,统一将app主题更换至“哀悼色”...,本篇文章简谈Android端的一种实现方案。

系统api:saveLayer

saveLayer可以为canvas创建一个新的透明图层,在新的图层上绘制,并不会直接绘制到屏幕上,而会在restore之后,绘制到上一个图层或者屏幕上(如果没有上一个图层)。为什么会需要一个新的图层,例如在处理xfermode的时候,原canvas上的图(包括背景)会影响src和dst的合成,这个时候,使用一个新的透明图层是一个很好的选择

public int saveLayer(@Nullable RectF bounds, @Nullable Paint paint, @Saveflags int saveFlags) {
       if (bounds == null) {
           bounds = new RectF(getClipBounds());
      }
       checkValidSaveFlags(saveFlags);
       return saveLayer(bounds.left, bounds.top, bounds.right, bounds.bottom, paint,
               ALL_SAVE_FLAG);
  }

public int saveLayer(@Nullable RectF bounds, @Nullable Paint paint) {
       return saveLayer(bounds, paint, ALL_SAVE_FLAG);
  }

ColorMatrix中setSaturation设置饱和度,给布局去色(0为灰色,1为原图)

/**
    * Set the matrix to affect the saturation of colors.
    *
    * @param sat A value of 0 maps the color to gray-scale. 1 is identity.
    */
   public void setSaturation(float sat) {
       reset();
       float[] m = mArray;

       final float invSat = 1 - sat;
       final float R = 0.213f * invSat;
       final float G = 0.715f * invSat;
       final float B = 0.072f * invSat;

       m[0] = R + sat; m[1] = G;       m[2] = B;
       m[5] = R;       m[6] = G + sat; m[7] = B;
       m[10] = R;      m[11] = G;      m[12] = B + sat;
  }

1.在view上的实践

自定义MourningImageVIew

public class MourningImageView extends AppCompatImageView {
   private Paint mPaint;
   public MourningImageView(Context context) {
       this(context,null);
  }
   public MourningImageView(Context context, @Nullable AttributeSet attrs) {
       this(context, attrs,0);
  }
   public MourningImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
       super(context, attrs, defStyleAttr);
       createPaint();
  }
   private void createPaint(){
       if(mPaint == null) {
           mPaint = new Paint();
           ColorMatrix cm = new ColorMatrix();
           cm.setSaturation(0);
           mPaint.setColorFilter(new ColorMatrixColorFilter(cm));
      }
  }
   @Override
   public void draw(Canvas canvas) {
       createPaint();
       canvas.saveLayer(null, mPaint, Canvas.ALL_SAVE_FLAG);
       super.draw(canvas);
       canvas.restore();
  }
   @Override
   protected void dispatchDraw(Canvas canvas) {
       createPaint();
       canvas.saveLayer(null, mPaint, Canvas.ALL_SAVE_FLAG);
       super.dispatchDraw(canvas);
       canvas.restore();
  }
}

和普通的ImageView对比效果:

举一反三在其他的view上也是可以生效的,实际线上项目不可能去替换所有view,接下来我们在根布局上做文章。

自定义各种MourningViewGroup实际替换效果:

xml version="1.0" encoding="utf-8"?>
<com.example.sample.MourningLinearlayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity"
   android:orientation="vertical"
   >

   <ImageView
       android:layout_width="wrap_content"
       android:src="@mipmap/ic_launcher"
       android:layout_margin="20dp"
       android:layout_height="wrap_content"/>

   <com.example.sample.MourningImageView
       android:layout_width="wrap_content"
       android:src="@mipmap/ic_launcher"
       android:layout_margin="20dp"
       android:layout_height="wrap_content"/>
com.example.sample.MourningLinearlayout>

也是可以生效的,那接下来的问题就是如何用最少的代码替换所有的页面根布局的问题了。在Activity创建的时候同时会创建一个 Window,其中包含一个 DecoView,在我们Activity中调用setContentView(),其实就是把它添加到decoView中,可以统一拦截decoview并对其做文章,降低入侵同时减少代码量。

2.Hook Window DecoView

application中注册ActivityLifecycleCallbacks,创建hook点

public class MineApplication extends Application {

   @Override
   protected void attachBaseContext(Context base) {
       super.attachBaseContext(base);
       registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
           @Override
           public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
              //获取decoview
               ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
               if(decorView!= null && decorView.getChildCount() > 0) {
                   //获取设置的contentview
                   View child = decorView.getChildAt(0);
                   //从decoview中移除contentview
                   decorView.removeView(child);
                   //创建哀悼主题布局
                   MourningFramlayout mourningFramlayout = new MourningFramlayout(activity);
                   //将contentview添加到哀悼布局中
                   mourningFramlayout.addView(child);
                   //将哀悼布局添加到decoview中
                   decorView.addView(mourningFramlayout);
              }
...
}

找个之前做的项目试试效果:

效果还行,暂时没发现什么坑,但是可能存在坑....😄😄😄

作者:code_balance
来源:https://www.jianshu.com/p/abdebc2c508e


收起阅读 »

jsonp的原理是什么?它是怎么实现跨域的?

写在前面一说到javascript的跨域,很多人第一时间想到的就是jsonp(JSON with Padding),那么这种跨域方式的实现原理是什么? 我承认我使用了很长时间,但是还是现在才知道,原来是这样....问题,如果我在 本地 访问 api.com下面...
继续阅读 »



写在前面

一说到javascript的跨域,很多人第一时间想到的就是jsonp(JSON with Padding),那么这种跨域方式的实现原理是什么? 我承认我使用了很长时间,但是还是现在才知道,原来是这样....

问题,如果我在 本地 访问 api.com下面的接口,会出现跨域请求的问题,为什么jsonp能解决这个?

  • 1、script标签是用来加载什么的?

加载js脚本的,src写上一个脚本的地址,然后浏览器就能加载啊!

  • 2、那么本地jsonp.html的script标签可以加载api.com的域名下面的脚本文件吗?

可以啊!要不那些用CDN方式优化网页加载速度的,是不可能成功的如

<script type="text/javascript" src="https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script>
复制代码
  • 3、那么script能加载别的域名下面的脚本文件,与jsonp何干?

我们都知道,加载api.com的域名下面的js脚本是可以的,此时,api.com下面的js脚本文件为真实存在的静态资源。那么如果这个脚本文件是由后端语言生成的呢?实例使用 php ==>jsonp.php

<?php
echo 'alert("Hello world")';
?>
  • 4、那么问题来了,我们生成js脚本的文件为.php文件啊,怎么加载这个脚本?

答案是:我们的 script标签是能够加载.php文件的,也就是

<script type="text/javascript" src='http://localhost/jsonp.php'></script>

运行结果

以上证明,我们完全可以在服务器端生成一段脚本,然后html页面用script标签去加载然后执行脚本。

那么,我们可以在生成的脚本中执行html中定义的方法吗?我们来试一下

index.html

<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="utf-8">
   <title>jsonp</title>
</head>
<body>
</body>
<script type="text/javascript">
 function execWithJsonp(para){
   console.log('我被jsonp.php中的脚本执行了,传递过来的参数是==>'+para);
   console.log(para)
}
</script>
<script type="text/javascript" src='http://localhost/jsonp.php'></script>
</html>

jsonp.php

<?php
echo "execWithJsonp({status:'ok'})";
?>

运行结果

是的,我们发现完全没问题,我们平常调用接口就是要的后端返回的数据,上面的例子,后端生成脚本时已然给我们传递了参数,拿到数据之后,我们可以做任何我们想做的事。

问题:如果后端接口这么写,那么前端所有调用这个接口的地方,岂不是都要定义一个 execWithJsonp方法?

如果页面调用两次,处理逻辑还不一样,那么我们岂不是要区分是哪一次?我希望每次访问接口调用不同的处理数据函数,每次我来告诉后端用哪个函数来处理返回的数据。 当然可以,我们可以这么做

index.html

<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="utf-8">
   <title>jsonp</title>
</head>
<body>
</body>
<script type="text/javascript">
 function execWithJsonp(para){
   console.log('我被jsonp.php中的脚本执行了,我是execWithJsonp,传递过来的参数是==>'+para);
   console.log(para)
}
 function doExecJsonp(para){
   console.log('我被jsonp.php中的脚本执行了,我是doExecJsonp,传递过来的参数是==>'+para);
   console.log(para)
}
</script>
<script type="text/javascript" src='http://localhost/jsonp.php?callback=doExecJsonp'></script>
<script type="text/javascript" src='http://localhost/jsonp.php?callback=execWithJsonp'></script>
</html>

jsonp.php

<?php
 $callback=$_GET['callback'];
 echo $callback."({status:'ok'})";
?>

运行结果

说到这儿,我好像还是没说原理是啥,其实你看完上面的也就理解了

jsonp实际上就是

  • 1、前端调用后端时传递给后端数据的处理函数callback

  • 2、后端收到处理函数callback之后,进行数据库查询等操作,将后端要传递给前端的数据(一般为json格式)放入callback函数的()中并返回【实际上就是由后端动态生成一个前端可用的js脚本】,

  • 3、html页面在脚本文件加载后,自动执行脚本

  • 4、完成了整个jsonp请求。

优缺点

优点:它不像XMLHttpRequest对象实现的Ajax请求那样受到同源策略的限制;它的兼容性更好,在更加古老的浏览器中都 可以运行,不需要XMLHttpRequest或ActiveX的支持;并且在请求完毕后可以通过调用callback的方式回传结果。

缺点:它只支持GET请求而不支持POST等其它类型的HTTP请求;它只支持跨域HTTP请求这种情况,不能解决不同域的两个页面之间如何进行JavaScript调用的问题,切很明显的需要后端工程师配合才能完成。

后记,发挥自己的想象吧,看这东西该怎么操作好

 function execWithJsonp(para){
   console.log('我被jsonp.php中的脚本执行了,我是execWithJsonp,传递过来的参数是==>'+para);
   console.log(para)
}
 function doExecJsonp(para){
   console.log('我被jsonp.php中的脚本执行了,我是doExecJsonp,传递过来的参数是==>'+para);
   console.log(para)
}

doJsonp('doExecJsonp')

function doJsonp(callbackName){
 var script=document.createElement('script');
 script.src='http://localhost/jsonp.php?callback='+callbackName;
 document.body.appendChild(script);
}


作者:小枫学幽默
来源:https://juejin.cn/post/7040730836156547086

收起阅读 »

Vite为什么快呢?快在哪?说一下我自己的理解吧

前言大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心。由于这几个月使用了Vue3 + TS + Vite进行开发,并且是真的被Vite强力吸粉了!!!Vite最大的优点就是:快!!!非常快!!!说实话,使用Vite开发...
继续阅读 »



前言

大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心。

由于这几个月使用了Vue3 + TS + Vite进行开发,并且是真的被Vite强力吸粉了!!!Vite最大的优点就是:快!!!非常快!!!

说实话,使用Vite开发之后,我都有点不想回到以前Webpack的项目开发了,因为之前的项目启动项目需要30s以上,修改代码更新也需要2s以上,但是现在使用Vite,差不多启动项目只需要1s,而修改代码更新也是超级快!!!

那到底是为什么Vite可以做到这么快呢?官方给的解释,真的很官方。。所以今天我想用比较通俗易懂的话来讲讲,希望大家能看一遍就懂。

问题现状

ES模块化支持的问题

咱们都知道,以前的浏览器是不支持ES module的,比如:

// index.js

import { add } from './add.js'
import { sub } from './sub.js'
console.log(add(1, 2))
console.log(sub(1, 2))

// add.js
export const add = (a, b) => a + b

// sub.js
export const sub = (a, b) => a - b

你觉得这样的一段代码,放到浏览器能直接运行吗?答案是不行的哦。那怎么解决呢?这时候打包工具出场了,他将index.js、add.js、sub.js这三个文件打包在一个bundle.js文件里,然后在项目index.html中直接引入bundle.js,从而达到代码效果。一些打包工具,都是这么做的,例如webpack、Rollup、Parcel

项目启动与代码更新的问题

这个不用说,大家都懂:

  • 项目启动:随着项目越来越大,启动个项目可能要几分钟

  • 代码更新:随着项目越来越大,修改一小段代码,保存后都要等几秒才更新

解决问题

解决启动项目缓慢

Vite在打包的时候,将模块分成两个区域依赖源码

  • 依赖:一般是那种在开发中不会改变的JavaScript,比如组件库,或者一些较大的依赖(可能有上百个模块的库),这一部分使用esbuild来进行预构建依赖,esbuild使用的是 Go 进行编写,比 JavaScript 编写的打包器预构建依赖快 10-100倍

  • 源码:一般是哪种好修改几率比较大的文件,例如JSX、CSS、vue这些需要转换且时常会被修改编辑的文件。同时,这些文件并不是一股脑全部加载,而是可以按需加载(例如路由懒加载)。Vite会将文件转换后,以es module的方式直接交给浏览器,因为现在的浏览器大多数都直接支持es module,这使性能提高了很多,为什么呢?咱们看下面两张图:

第一张图,是以前的打包模式,就像之前举的index.js、add.js、sub.js的例子,项目启动时,需要先将所有文件打包成一个文件bundle.js,然后在html引入,这个多文件 -> bundle.js的过程是非常耗时间的。

第二张图,是Vite的打包方式,刚刚说了,Vite是直接把转换后的es module的JavaScript代码,扔给支持es module的浏览器,让浏览器自己去加载依赖,也就是把压力丢给了浏览器,从而达到了项目启动速度快的效果。

解决更新缓慢

刚刚说了,项目启动时,将模块分成依赖源码,当你更新代码时,依赖就不需要重新加载,只需要精准地找到是哪个源码的文件更新了,更新相对应的文件就行了。这样做使得更新速度非常快。

Vite 同时利用 HTTP 头来加速整个页面的重新加载(再次让浏览器为我们做更多事情):源码模块的请求会根据 304 Not Modified 进行协商缓存,而依赖模块请求则会通过 Cache-Control: max-age=31536000,immutable 进行强缓存,因此一旦被缓存它们将不需要再次请求。

生产环境

刚刚咱们说的都是开发环境,也说了,Vite在是直接把转化后的es module的JavaScript,扔给浏览器,让浏览器根据依赖关系,自己去加载依赖。

那有人就会说了,那放到生产环境时,是不是可以不打包,直接在开个Vite服务就行,反正浏览器会自己去根据依赖关系去自己加载依赖。答案是不行的,为啥呢:

  • 1、你代码是放在服务器的,过多的浏览器加载依赖肯定会引起更多的网络请求

  • 2、为了在生产环境中获得最佳的加载性能,最好还是将代码进行tree-shaking、懒加载和 chunk 分割、CSS处理,这些优化操作,目前esbuild还不怎么完善

所以Vite最后的打包是使用了Rollup


作者:Sunshine_Lin
来源:https://juejin.cn/post/7040750959764439048

收起阅读 »

轻量级安卓水印框架,支持隐形数字水印 AndroidWM

一个轻量级的 Android 图片水印框架,支持隐形水印和加密水印。 English version下载与安装Maven:<dependency>  <groupId>com.huangyz0918groupId> &n...
继续阅读 »



AndroidWM

一个轻量级的 Android 图片水印框架,支持隐形水印和加密水印。 English version

img

下载与安装

Gradle:

implementation 'com.huangyz0918:androidwm:0.1.9'

Maven:

<dependency>
 <groupId>com.huangyz0918groupId>
 <artifactId>androidwmartifactId>
 <version>0.1.9version>
 <type>pomtype>
dependency>

Lvy:

<dependency org='com.huangyz0918' name='androidwm' rev='0.1.9'>
 <artifact name='androidwm' ext='pom' >artifact>
dependency>

快速入门

新建一个水印图片

在下载并且配置好 androidwm 之后,你可以创建一个 WatermarkImage 或者是 WatermarkText 的实例,并且使用内置的诸多Set方法为创建一个水印做好准备。

    WatermarkText watermarkText = new WatermarkText(editText)
          .setPositionX(0.5)
          .setPositionY(0.5)
          .setTextColor(Color.WHITE)
          .setTextFont(R.font.champagne)
          .setTextShadow(0.1f, 5, 5, Color.BLUE)
          .setTextAlpha(150)
          .setRotation(30)
          .setTextSize(20);

对于具体定制一个文字水印或者是图片水印, 我们在接下来的文档中会仔细介绍。

当你的水印(文字或图片水印)已经准备就绪的时候,你需要一个 WatermarkBuilder来把水印画到你希望的背景图片上。 你可以通过 create 方法获取一个 WatermarkBuilder 的实例,注意,在创建这个实例的时候你需要先传入一个 Bitmap 或者是一个 Drawable 的资源 id 来获取背景图。

    WatermarkBuilder
          .create(context, backgroundBitmap)
          .loadWatermarkText(watermarkText) // use .loadWatermarkImage(watermarkImage) to load an image.
          .getWatermark()
          .setToImageView(imageView);

选择绘制模式

你可以在 WatermarkBuilder.setTileMode() 中选择是否使用铺满整图模式,默认情况下我们只会添加一个水印。

    WatermarkBuilder
          .create(this, backgroundBitmap)
          .loadWatermarkText(watermarkText)
          .setTileMode(true) // select different drawing mode.
          .getWatermark()
          .setToImageView(backgroundView);

咚! 带水印的图片已经绘制好啦:

img

获取输出图片

你可以在 WatermarkBuilder 中同时加载文字水印和图片水印。 如果你想在绘制完成之后获得带水印的结果图片,可以使用 Watermark.getOutputImage() 方法:

    Bitmap bitmap = WatermarkBuilder
          .create(this, backgroundBitmap)
          .getWatermark()
          .getOutputImage();

创建多个水印

你还可以一次性添加多个水印图片,通过创建一个WatermarkText 的列表 List<> 并且在水印构建器的方法 .loadWatermarkTexts(watermarkTexts)中把列表传入进去(图片类型水印同理):

    WatermarkBuilder
          .create(this, backgroundBitmap)
          .loadWatermarkTexts(watermarkTexts)
          .loadWatermarkImages(watermarkImages)
          .getWatermark();

加载资源

你还可以从系统的控件和资源中装载图片或者是文字资源,从而创建一个水印对象:

WatermarkText watermarkText = new WatermarkText(editText); // for a text from EditText.
WatermarkText watermarkText = new WatermarkText(textView); // for a text from TextView.
WatermarkImage watermarkImage = new WatermarkImage(imageView); // for an image from ImageView.
WatermarkImage watermarkImage = new WatermarkImage(this, R.drawable.image); // for an image from Resource.

WatermarkBuilder里面的背景图片同样可以从系统资源或者是 ImageView 中装载:

    WatermarkBuilder
          .create(this, backgroundImageView) // .create(this, R.drawable.background)
          .getWatermark()

如果在水印构建器中你既没有加载文字水印也没有加载图片水印,那么处理过后的图片将保持原样,毕竟你啥也没干 :)

隐形水印 (测试版)

androidwm 支持两种模式的隐形水印:

  • 空域 LSB 水印

  • 频域叠加水印

你可以通过WatermarkBuilder 直接构造一个隐形水印,为了选择不同的隐形方式,可以使用布尔参数 isLSB 来区分它们 (注:频域水印扔在开发中),而想要获取到构建成功的水印图片,你需要添加一个监听器:

     WatermarkBuilder
          .create(this, backgroundBitmap)
          .loadWatermarkImage(watermarkBitmap)
          .setInvisibleWMListener(true, 512, new BuildFinishListener<Bitmap>() {
               @Override
               public void onSuccess(Bitmap object) {
                   if (object != null) {
                      // do something...
                  }
              }

               @Override
               public void onFailure(String message) {
                  // do something...
              }
          });

setInvisibleWMListener 方法的第二个参数是一个整数,表示输入图片最大尺寸,有的时候,你输入的可能是一个巨大的图片,为了使计算算法更加快速,你可以选择在构建图片之前是否对图片进行缩放,如果你让这个参数为空,那么图片将以原图形式进行添加水印操作。无论如何,注意一定要保持背景图片的大小足以放得下水印图片中的信息,否则会抛出异常。

同理,检测隐形水印可以使用类WatermarkDetector,通过一个create方法获取到实例,同时传进去一张加过水印的图片,第一个布尔参数代表着水印的种类,true 代表着检测文字水印,反之则检测图形水印。

     WatermarkDetector
          .create(inputBitmap, true)
          .detect(false, new DetectFinishListener() {
               @Override
               public void onImage(Bitmap watermark) {
                   if (watermark != null) {
                        // do something...
                  }
              }

               @Override
               public void onText(String watermark) {
                   if (watermark != null) {
                       // do something...
                  }
              }

               @Override
               public void onFailure(String message) {
                      // do something...
              }
          });

LSB 隐形空域水印 Demo 动态图:

imgimg
隐形文字水印 (LSB)隐形图像水印 (LSB)

好啦!请尽情使用吧 😘

使用说明

水印位置

我们使用 WatermarkPosition 这个类的对象来控制具体水印出现的位置。

   WatermarkPosition watermarkPosition = new WatermarkPosition(double position_x, double position_y, double rotation);
  WatermarkPosition watermarkPosition = new WatermarkPosition(double position_x, double position_y);

在函数构造器中,我们可以设定水印图片的横纵坐标,如果你想在构造器中初始化一个水印旋转角度也是可以的, 水印的坐标系以背景图片的左上角为原点,横轴向右,纵轴向下。

WatermarkPosition 同时也支持动态调整水印的位置,这样你就不需要一次又一次地初始化新的位置对象了, androidwm 提供了一些方法:

     watermarkPosition
            .setPositionX(x)
            .setPositionY(y)
            .setRotation(rotation);

在全覆盖水印模式(Tile mode)下,关于水印位置的参数将会失效。

imgimg
x = y = 0, rotation = 15x = y = 0.5, rotation = -15

横纵坐标都是一个从 0 到 1 的浮点数,代表着和背景图片的相对比例。

字体水印的颜色

你可以在 WatermarkText 中设置字体水印的颜色或者是其背景颜色:

    WatermarkText watermarkText = new WatermarkText(editText)
          .setPositionX(0.5)
          .setPositionY(0.5)
          .setTextSize(30)
          .setTextAlpha(200)
          .setTextColor(Color.GREEN)
          .setBackgroundColor(Color.WHITE); // 默认背景颜色是透明的
imgimg
color = green, background color = whitecolor = green, background color = default

字体颜色的阴影和字体

你可以从软件资源中加载一种字体,也可以通过方法 setTextShadow 设置字体的阴影。

    WatermarkText watermarkText = new WatermarkText(editText)
          .setPositionX(0.5)
          .setPositionY(0.5)
          .setTextSize(40)
          .setTextAlpha(200)
          .setTextColor(Color.GREEN)
          .setTextFont(R.font.champagne)
          .setTextShadow(0.1f, 5, 5, Color.BLUE);
imgimg
font = champagneshadow = (0.1f, 5, 5, BLUE)

阴影的四个参数分别为: (blur radius, x offset, y offset, color).

字体大小和图片大小

水印字体和水印图片大小的单位是不同的:
- 字体大小和系统布局中字体大小是类似的,取决于屏幕的分辨率和背景图片的像素,您可能需要动态调整。
- 图片大小是一个从 0 到 1 的浮点数,是水印图片的宽度占背景图片宽度的比例。

imgimg
image size = 0.3text size = 40

方法列表

对于 WatermarkTextWatermarkImage 的定制化,我们提供了一些常用的方法:

方法名称备注默认值
setPosition水印的位置类 WatermarkPositionnull
setPositionX水印的横轴坐标,从背景图片左上角为(0,0)0
setPositionY水印的纵轴坐标,从背景图片左上角为(0,0)0
setRotation水印的旋转角度0
setTextColor (WatermarkText)WatermarkText 的文字颜色Color.BLACK
setTextStyle (WatermarkText)WatermarkText 的文字样式Paint.Style.FILL
setBackgroundColor (WatermarkText)WatermarkText 的背景颜色null
setTextAlpha (WatermarkText)WatermarkText 文字的透明度, 从 0 到 25550
setImageAlpha (WatermarkImage)WatermarkImage 图片的透明度, 从 0 到 25550
setTextSize (WatermarkText)WatermarkText 字体的大小,单位与系统 layout 相同20
setSize (WatermarkImage)WatermarkImage 水印图片的大小,从 0 到 1 (背景图片大小的比例)0.2
setTextFont (WatermarkText)WatermarkText 的字体default
setTextShadow (WatermarkText)WatermarkText 字体的阴影与圆角(0, 0, 0)
setImageDrawable (WatermarkImage)WatermarkImage的图片资源null

WatermarkImage 的一些基本属性和WatermarkText 的相同。

项目地址:https://github.com/huangyz0918/AndroidWM

作者:huangyz0918
来源:https://www.wanandroid.com/blog/show/2346

收起阅读 »

优秀的react框架的开源ui库 -- Pile.js

Pile.js是滴滴出行企业级前端组开发的一套基于 react 的移动端组件库,Pile.js组件库在滴滴企业级产品中极大提高了开发效率,也希望我们的产出能给广大前端开发者带来便捷。特性质量可靠 由滴滴企业级业务精简提炼而来,经历了一年多的考验,提供质量保障标...
继续阅读 »

Pile.js是滴滴出行企业级前端组开发的一套基于 react 的移动端组件库,Pile.js组件库在滴滴企业级产品中极大提高了开发效率,也希望我们的产出能给广大前端开发者带来便捷。

特性

质量可靠
由滴滴企业级业务精简提炼而来,经历了一年多的考验,提供质量保障

标准规范
代码规范严格按照eslint Airbnb编码规范,增加代码的可读性

优势

相对于同类型的移动端组件库,Pile.js有哪些优势?

组件数量多、体积小
Pile.js组件库包含52个组件,体积只有236k(未压缩),并且我们支持单个组件引用,除了常用的基础组件(比如:Button、Alert、Toast、Tip、Content等)外,我们还包含更为丰富的日期、时间、城市、车型组件,包括雷达图、环形加载、刻度尺组件等以及canvas动画图表等

样式定制
Pile.js设计规范上支持一定程度的样式定制,以满足业务和品牌上多样化的视觉需求

多语言
组件内文案提供统一的国际化支持,配置LocaleProvider组件,运用React的context特性,只需在应用外围包裹一次即可全局生效。

啰嗦一句,如果你有兴趣,不妨也参与到这个项目中来。

项目地址:https://github.com/didi/pile.js

Pile Issues:https://github.com/didi/pile.js/issues

文档: https://didi.github.io/pile.js/docs/

demo: https://didi.github.io/pile.js/demo/#/?_k=klfvmd

组件分类

作者:闫森
来源: https://www.cnblogs.com/yansen/p/9083173.html


收起阅读 »