注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

庖丁解牛:Android stuido中 git 操作详解

Git
前言 在开发flutter或android项目,选择用Android stuido是比较方便的,在git的可视化使用上,Android studio已经非常方便了,但是还是有很多的小伙伴,依旧用其他工具来管理git。那么今天我就来详细的介绍一下 Android...
继续阅读 »

前言


在开发flutter或android项目,选择用Android stuido是比较方便的,在git的可视化使用上,Android studio已经非常方便了,但是还是有很多的小伙伴,依旧用其他工具来管理git。那么今天我就来详细的介绍一下 Android stuido的git使用与操作。


一、基本认知


git是采用分布式版本库机制。


工作区


项目目录下的文件可以称之为工作区


暂存区


增加文件,执行add操作则是把文件添加到暂存区


基本操作


git add 是将文件放到暂缓区
git commit 则是把文件添加到本地仓库
git push 则是提交到远程仓库
git status 是查看现有版本库中的文件状态


head指针


head表示的是当前版本,并不是任何分支的最新版本


二、Android studio中的git


文件样式与对应关系


在这里插入图片描述
文件1是git忽略文件
文件2是与本地分支版本一致
文件3是咱为提交到本地分支,并做了修改


假如文件是红色的样子,表示并没提交到暂存区


界面与操作


在这里插入图片描述



  1. commit 提交到本地分支 (基本操作,不做说明)

  2. add 添加到暂存区 (基本操作,不做说明)

  3. .git/info/exclude (添加到 忽略文件,是为了让文件脱离git管理,不会上传到git仓库)

  4. Annotate with Git Blame (显示每行代码的作者,如下图)


在这里插入图片描述
5. show diff (故名思义differences 差别)
6. compare with reversion(与某个版本比较)
7. compare with branch(与某个分支比较)
8. show history (查看历史)
9. show current revision (显示当前行最新修订历史版本、提示)
10. Rollback.. 在没有提交到本地库之前,丢弃工作区内容。
11. push.. 推到线上分支
12. pull.. 线上拉到本地并合并
13. fetch 线上拉到本地
14. merge.. 选择分支进行合并
15. rebase.. 选择分支进行合并,如有有合并冲突,会提示整理为一条commit直线
16. branches.. 创建分支与查看分支(这块下面快捷操作具体介绍)
17. new branch 会按照当前本地提交版本,来创建新的分支
18. new tag 给某一次提交增加个可识别的名字
19. reset HEAD
注意:Git中,用HEAD表示当前版本
在这里插入图片描述
有三个选项
Mixed:参数为默认值,暂存区的内容和工作区的内容都在工作区,提交上的都会还原到工作区
Soft:工作区的内容依旧在工作区,暂存区的内容还在暂存区(不被git管理的文件会变为新增文件)
Hard:工作区和暂存区的内容全部丢失(需要谨慎操作,一步就啥也木有了)


to commit 选择你要还原的commit号
20. stash changes..
git中如果本地有文件改动未提交、且该文件和服务器最新版本有冲突,pull更新会提示错误,无法更新:要么先commit自己的改动然后再通过pull拉取代码,stash的好处是可以先将你的改动暂存到本地仓库中,随时可以取出来再用,但是不用担心下次push到服务器时,把不想提交的改动也push到服务器上,因为Stash Changes的内容不参与commit和push。
21. unstash changes
在这里插入图片描述
View:查看
Drop:删除
Clear:清理
pop stash:移除stash
reinstate index
22. manager remote
查看git remote内容


右下角快捷操作


在这里插入图片描述
在这里插入图片描述
merge into current 合并到当前分支
rebase current onto selected 合并到当前rebase模式
chechout and rebase onto current 切换分支,并将分支合并到当前切换的分支


git面板快捷操作


在这里插入图片描述
在这里插入图片描述


compact references view 简洁引用视图
简洁引用:
在这里插入图片描述
align references to left 将引用向左对齐
引用左右对齐配置:
在这里插入图片描述


Show tag names 显示标签名称
设置是否显示标签:


在这里插入图片描述


Show long Edges 显示长线
在这里插入图片描述


Turn Intellisort On 打开intelli 排序
incase of merge show incoming commits first (directly below merge commit)
在合并的情况下,首先显示传入的提交 直接合并在下边

收起阅读 »

排序算法的基础&进阶

类型平均情况下,时间复杂度最好情况下,时间复杂度最坏情况下,时间复杂度空间复杂度稳定性冒泡排序O(n²)O(n)有序情况O(n²)无序情况O(1)稳定快速排序O(nlogn)O(nlogn)O(n²)有序情况O(logn)不稳定插入排序O(n²)O(n)有序情...
继续阅读 »
类型平均情况下,时间复杂度最好情况下,时间复杂度最坏情况下,时间复杂度空间复杂度稳定性
冒泡排序O(n²)O(n)有序情况O(n²)无序情况O(1)稳定
快速排序O(nlogn)O(nlogn)O(n²)有序情况O(logn)不稳定
插入排序O(n²)O(n)有序情况O(n²)无序情况O(1)稳定
选择排序O(n²)O(n²)O(n²)O(1)不稳定
归并排序O(nlogn)O(nlogn)O(nlogn)O(n)稳定
希尔排序O(nlog²n)O(nlog²n)O(nlog²n)O(1)不稳定

关键词含义


n:数据规模


时间复杂度


算法运行过程中所耗费的时间。


空间复杂度


算法运行过程中临时占用存储空间的大小。例如:O(1)表示所需空间大小为常量,与数据量n无关。


稳定性含义



  • 稳定:在排序之前,如果两个数相等,那么排序之后,这两个数的先后顺序不变。如排序前,a=b,a在b的前面;那么排序后,a依旧在b的前面。

  • 不稳定:在排序之前,如果两个数相等,那么排序之后,这两个数的先后顺序改变。如排序前,a=b,a在b的前面;那么排序后,a在b的后面。


冒泡排序


原理步骤



  1. 比较相邻的两个数,如果前面的数大于后面的数,就交换这两个数。

  2. 相邻的最前一对数和最后一对数都要进行比较,这样最后一个数就是最大的数。

  3. 每个元素重复以上步骤,除了最后一个数。

  4. 重复1-3的步骤。


代码实现


private static int[] bubbleSort(int array[]) {
if (array.length == 0) {
return array;
}
// 第1个for循环相当于步骤4
for (int i = 0; i < array.length; i++) {
// 第2个for循环相当于步骤3
// array.length -1 是因为后面有j+1,先-1是为了避免数组越界
// array.length -1 - i,之所以减i(已经排过1遍,就减1;如果已经排过i遍,就减i),是为了不比较排在最后且已经排好序的数,相当于步骤3的最后一句话
for (int j = 0; j < array.length - 1 - i; j++) {
int temp;
// if判断语句相当于步骤1和步骤2
if (array[j] > array[j + 1]) {
temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
}
return array;
}

快速排序


原理步骤



  1. 取数组中的一个数作为key。

  2. 从后往前获取数组的数,并将其与key进行对比。

  3. 如果其中一个数小于key,那么就将这个数和key交换位置。

  4. 交换位置之后,从前往后获取数组的数,并将其与key对比。

  5. 如果其中一个数大于key,那么就将这个数和key交换位置。

  6. 重复2-5的过程,直到key前面的数都比key小,key后面的数都比key大,这样就完成一次排序。

  7. 以key为中心,对key前面的数组和后面的数组执行1-6的过程,直到数组完全有序。


代码实现


private static void quickSort(int[] array, int left, int right) {
if (left >= right) {
return;
}
int i, j, x;
i = left;
j = right;
x = array[i];
while (i < j) {
while (i < j && array[j] > x) {
j--;
}
if (i < j) {
array[i] = array[j];
i++;
}
while (i < j && array[i] < x) {
i++;
}
if (i < j) {
array[j] = array[i];
j--;
}
}
// j=i
array[j] = x;
quickSort(array, j + 1, right);
quickSort(array, left, j - 1);
}

插入排序


原理



  • 每一步将一个待排序的数插入到已经排好序的序列中,直到插完所有数据。


代码实现


private static int[] insertSort(int array[]) {          
if (array.length == 0) {
return array;
}    
int i, j, temp;
// 注释①
for (i = 1; i < array.length; i++) {
// 注释②
temp = array[i];
// 注释③
for (j = i - 1; j >= 0 && array[j] > temp; j--) {
// 注释④
array[j+1] = array[j];           
}                  
// 注释⑤                  
array[j+1] = temp;           
}                  
return array;
}

注释①



  • 默认数组第一个数(i=0的数)是有序的。


注释②



  • array[i]为待排序的数据。


注释③



  • array[i]前面的数与array[i]进行排序。


注释④



  • arr[j]相当于前数,arr[j+1]相当于后数。

  • 如果前数比后数大,交换位置,前数放到后数的位置。


注释⑤



  • 如果for循环内前数和后数交换了位置(即前数挪到了后数的位置),那么注释⑤处的代码,就是将后数挪到前数的位置,实现交换。

  • 如果缺少注释⑤处的代码,那么前数的位置就会“空缺”,或者说依旧是原来的数,并没有实现交换。

  • 如果for循环内两数并没有交换(即跳出了for循环),此时j=i-1,j+1=i,与tmp=a[i]效果是一样的。


选择排序


原理步骤:



  1. 从未排序的序列中取出最小(最大)的数,放入已排序序列的初始位置;

  2. 继续从未排序序列剩余的数中取最小(最大)的数,放在已排序序列的末尾。

  3. 持续执行②的步骤,直到整个序列有序。


代码实现


private static int[] selectionSort(int[] array) {    
if (array.length == 0) {
return array;
}
for (int i = 0; i < array.length; i++) {
int min = i;
for (int j = i; j < array.length; j++) {
// 从未排序序列中获取最小值
if (array[j] < array[min]) {
min = j;
}
}
// 把获取的最小值放入已排序序列的末尾(此时i代表末尾的索引)
int temp = array[i];
array[i] = array[min];
array[min] = temp;
}
return array;
}

总结1:



  • 插入排序和选择排序可以划为一类排序算法来理解和掌握。

  • 它们都具有相同点——将数组划分为已排序、未排序两个部分,然后将未排序的部分逐个迁移到已排序的部分,最终使整个数组实现完全有序。

  • 而不同点在于从未排序合入到已排序的方式。插入排序会将未排序的数据在已排序的数组中执行直接插入排序;而选择排序会先在未排序的数组中选出最小值,当这个值合入到已排序的数组中时,不需要再进行比较,直接放到已排序数组的末尾就可以了。


希尔排序


原理步骤



  • 把一个数组按增量进行分组。(增量指分组数量)

  • 每个分组采用直接插入排序进行排序。

  • 然后减小增量,每个分组的元素数目增加,直到增量为1,整个文件变为一组,算法结束。


代码实现


private static int[] shellSort(int[] array) {    
if (array.length == 0) {
return array;
}
// gap为分组数目
for (int gap = array.length / 2; gap > 0; gap = gap / 2) {
// i为索引,对每组进行排序
for (int i = gap; i < array.length; i++) {
// j为临时变量
int j = i;
// 分组内元素的个数可能大于2个,因此使用while循环
while (j - gap >= 0 && array[j] < array[j - gap]) {
// 在同一个分组中,如果后面的数(j)比前面的(j-gap)大,就交换它们的位置
int temp = array[j];
array[j] = array[j - gap];
array[j - gap] = temp;
j = j - gap;
}
}
}
return array;
}

归并排序


原理步骤



  • 将一个数组分为左子数组和右子数组,两个子数组的长度为n/2(n为数组的总长度)。

  • 在两个子数组间进行归并排序(即每个子数组划分为更小的左子数组和右子数组,直到无法再分时,对两个数组进行排序,详情见代码)。

  • 将两个有序的子数组合并为一个最终的有序数组。


代码实现


private static int[] mergeSort(int[] array) {    
// 数组只有一个元素或没有元素,直接返回。
// 脱离递归的条件
if (array.length < 2) {
return array;
}
// 将数组分为两半,分别进行排序
int[] left = Arrays.copyOfRange(array, 0, array.length / 2);
int[] right = Arrays.copyOfRange(array, array.length / 2, array.length);
return merge(mergeSort(left),mergeSort(right));
}

/**
* 将左数组与右数组合并为一个有序数组
* 注意:此时左数组、右数组已经有序
* @param left
* @param right
* @return
*/
private static int[] merge(int[] left, int[] right) {
// 合并后的有序数组
int[] result = new int[left.length + right.length];
for (int index = 0, i = 0, j = 0; index < result.length; index++) {
if (i >= left.length) {
// 如果左数组已经遍历结束,就插入右数组的值
result[index] = right[j];
j++;
} else if (j >= right.length) {
// 如果右数组已经遍历结束,就插入左数组的值
result[index] = left[i];
i++;
} else if (left[i] > right[j]) {
// 左数组与右数组的值同时存在时,就对两数进行比较
// 如果右数组的值比较小,就插入右数组的值。
result[index] = right[j];
j++;
} else {
// 左数组与右数组的值同时存在时,就对两数进行比较
// 如果左数组的值比较小,就插入左数组的值。
result[index] = left[i];
i++;
}
}
return result;
}

总结2:



  • 希尔排序和归并排序可以划为一类排序算法来理解和掌握。

  • 它们都具有相同点——先将整个大的数组分为不同的小组,然后对小组的数据进行排序,最终将所有小组合并为一个有序数组。

  • 而不同点在于分组后的排序方式不同。希尔排序会针对一个小组内的数据执行直接插入排序,而归并排序会直接将两个小组合并为一个有序数组。


排序算法进阶



  • 以上冒泡、快排、插入、选择、希尔、归并这六种排序算法都是基础的排序算法,很多中等、困难难度的算法题一般都是基于上述算法进行解决。(比如《合并两个有序数组》其实就是归并算法的某一部分)

  • 推荐《最小K个数》、《数组中的第K个最大元素》作为进阶学习。(它们都是基于快排实现,类似的变形有最大K个数等)

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

把EditText交给ViewModel管理

Android小萌新今天在做项目的时候遇到一个小问题,来记录一下~ 在做一个登录界面的时候,想使用DataBinding+ViewModel+LiveData 但是怎样让ViewModel拿到EditText控件的实例呢?一开始想到把DataBinding对象...
继续阅读 »

Android小萌新今天在做项目的时候遇到一个小问题,来记录一下~


在做一个登录界面的时候,想使用DataBinding+ViewModel+LiveData


但是怎样让ViewModel拿到EditText控件的实例呢?一开始想到把DataBinding对象从Activity传入ViewModel,后来发现不可行,因为DataBinding在初始化的时候需要传入owner参数,而这个owner参数传的是Activity本身,也就是说DataBinding持有了Activity的引用,这时候如果把DataBinding传给ViewModel不就成了ViewModel持有Activity的引用了吗?内存泄漏!不行!


image.png


解决办法是通过DataBinding双向绑定(View可以操作数据,数据变化时通知View),让EditText的内容直接对应到ViewModel中的LiveData上,这样的话在输入框输入的同时LiveData也在随时变化。


一些收获的经验:


1. @={}和@{}


我发现EditText的text属性要使用@={...}而不是像TextView直接使用@{...}来和Livedata绑定,多出来的这个"="我个人认为是TextView和LiveData绑定仅仅只是get数据,而EditText和数据绑定需要get和实时set数据,所以"="可以理解为赋值


<EditText
...
android:text="@={viewModel.inputAccount}"
... />

<EditText
...
android:text="@={viewModel.inputVerify}"
... />

<Button
...
android:onClick="@{(v)->viewModel.onLogin()}"
... />

2. 为什么在账号EditText输入一个数,getInputAccount()会被调用两次呢?


public class TemporaryLoginViewModel extends ViewModel {

private static final String TAG = "TemporaryLoginViewModel";
MutableLiveData<String> mInputAccount;
MutableLiveData<String> mInputVerify;

public MutableLiveData<String> getInputAccount() {
// TODO:为什么EditText输入一个数,getInputAccount()会调用两次?
Log.d(TAG, "getInputAccount: Entrance");
//双检锁
if (mInputAccount == null)
synchronized (TemporaryLoginViewModel.class) {
if (mInputAccount == null)
mInputAccount = new MutableLiveData<>();
}
//只是TextView展示的话可以返回不可变的LiveData,这里因为是EditText所以只能返回可变的MutableLivedata
return mInputAccount;
}

public MutableLiveData<String> getInputVerify() {
...
}

public void onLogin() {
Log.d(TAG, "onLogin: 账号:" + mInputAccount.getValue() + " 验证码:" + mInputVerify.getValue());
}
}

这就要进入源码去看一眼了,在getInputAccount()上选择findUsages
发现有两处地方调用了它


image.png
第一处在一个回调方法的onChange()中,我们打个断点查看虚拟机栈的栈帧,在第一次执行到断点的时候,虚拟机栈是这样的:


image.png


onChange()内部是这样的:


image.png


也就是说你在输入框里打字使得EditText数据改变的时候,首先回调到onChange()中,在这个onChange()中通过getInputAccount()得到LiveData再给它set一个字符串值


第二处是在executeBindings()中,这个方法是什么时候执行呢?我们让程序继续执行,在下一次执行到断点的时候,虚拟机栈是这样的:


image.png
可以看到在第二次执行到断点的时候,程序从executeBindings()方法中企图调用getInputAccount()


继续向下追踪,就可以看到这样的一个描述


image.png


意思是当View所绑定的数据发生变更的时候,执行此方法


总结


走到这里就很清晰了,整个流程是首先在输入框中输入,当监听到输入后先回调onChange(),在onChange()中通过getInputAccount()得到LiveData,然后修改了LiveData的值;LiveData一但修改,就会重新执行executeBindings(),所以又会调用一次getInputAccount()


到现在就明白了为什么ViewModel中的getInputAccount()会被执行两次啦~


3. getInputAccount()只能返回MutableLiveData


第三个问题也很好理解,为了安全嘛,我一开始试图让getInputAccount()返回一个不可修改的LiveData,然后报错了!


image.png
从第二个问题的分析不难看出,人家内部还要给get到的LiveData执行setValue()呢,所以返回的LiveData一定是可变的MutableLiveData啦~


4. 程序启动时会额外执行一次getInputAccount()


当我查看Activity中的setLifecycleOwner(this)方法时发现它设置了一个LifecycleObserver


image.png
进入这个Observer
image.png
它观察到Activity处于onStart状态的时候会调用executePendingBindings()


进入executePendingBindings()瞅瞅


image.png
又要去调用executeBindingsInternal(),这不就是我们上面在虚拟机栈中看到的调用步骤吗?也就是说在Activity在onStart状态时会执行一次getInputAccount()



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

Android卡顿优化思路

卡顿优化思路 卡顿原理分析 卡顿流程flow 卡顿概貌分析 卡顿实际数据收集 卡顿优化细节 卡顿原因 屏幕刷新频率高于帧率,帧率低于30 每帧执行流程 Choreographer中维护着四个队列callbacks 输入事件队列 动画队列 绘制队列 app...
继续阅读 »

卡顿优化思路



  • 卡顿原理分析

  • 卡顿流程flow

  • 卡顿概貌分析

  • 卡顿实际数据收集

  • 卡顿优化细节


卡顿原因


屏幕刷新频率高于帧率,帧率低于30


每帧执行流程


Choreographer中维护着四个队列callbacks



  • 输入事件队列

  • 动画队列

  • 绘制队列

  • app添加的frameCallback队列


vysnc信号由SurfaceFlinger中创建HWC触发,通过bitTube技术发送到目标进程,目标进程vsync信号到来时,执行Choreographer中的onVsync回调,最终触发doFrame顺序执行这四条队列中的消息。


bitTube


在linux/unix中,bitTube技术成为socketPair,它通过dup技术复制socket的句柄,传递到目标进程,开启socket的全双工通信。


句柄


在内核中,每一个进程都有一个私有的“打开文件表”,这个表是一个指针数组,每一个元素都指向一个内核的打开文件对象。而fd,就是这个表的下标。当用户打开一个文件时,内核会在内部生成一个打开文件对象,并在这个表里找到一个空项,让这一项指向生成的打开文件对象,并返回这一项的下标作为fd.


ui优化



  • 多余Bg移除

  • ui重叠区域优化 cancas.clipRect

  • 减少ui层级

  • 耗时方法分析与优化

  • 多样式布局采用单一rv处理


webview优化


webview的加载流程


image


webiew初始化



  • 目的是初始化并启动浏览器内核。

  • 提前初始化webview并隐藏 优化126ms


webview 单独进程



  • 单独进程 activity配置

  • 单独进程的交互 webview.addJavascriptInterface(),webview.evalute()


安全性



  • addJavaScriptInterface添加的java对象的方法,需要添加@addJavascriptInterface注解,避免xss攻击


卡顿收集策略


开发卡顿检测StrictMode


private void initStrictMode() {
if (isDebug()) {
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectCustomSlowCalls() //API等级11,使用StrictMode.noteSlowCode
.detectDiskReads()
.detectDiskWrites()
.detectNetwork() // or .detectAll() for all detectable problems
.penaltyDialog() //弹出违规提示对话框
.penaltyLog() //在Logcat 中打印违规异常信息
.penaltyFlashScreen() //API等级11
.build());
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects() //API等级11
.penaltyLog()
.penaltyDeath()
.build());
}
}

线下卡顿检测



  • adb shell dumpsys gfxinfo [packagename]


Applications Graphics Acceleration Info:
Uptime: 205237819 Realtime: 436545102

** Graphics info for pid 5842 [xxxx] **

Stats since: 198741999784549ns
Total frames rendered: 653
Janky frames: 157 (24.04%)
50th percentile: 9ms
90th percentile: 34ms
95th percentile: 53ms
99th percentile: 200ms
Number Missed Vsync: 46
Number High input latency: 268
Number Slow UI thread: 76
Number Slow bitmap uploads: 3
Number Slow issue draw commands: 8
Number Frame deadline missed: 92


  • 通过gpu绘制条形柱分析


条形柱共分为8种颜色,绿色和蓝色部分是异步应用能够优化的部分。包括其他处理 - 输入 - 动画 - travel
image


BlockCanary检测卡顿


在ActivityThread.main中的Looper大循环中,Looper.looponce会不断从消息队列中取出消息派发出去,并在前后通过logging打印了两个日志,我们通过设置自定义的logger,在两部分日志的时间差与30ms做对比,如果超过30ms,认为是卡顿。


logging.println(">>>>> Dispatching to " + msg.target + " "
+ msg.callback + ": " + msg.what);
msg.target.dispatchMessage(msg);
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);

卡顿分析信息收集



  • Debug.startMethodTracing 收集具体的卡顿方法

  • 查看trace文件 根据bottomup分析具体的耗时方法

  • 火焰图,横轴是调用方法耗时,纵轴是调用深度

  • 调用图,调用链以及方法耗时


线上卡顿分析与收集


在ActivityThread.main中的Looper大循环中,Looper.looponce会不断从消息队列中取出消息派发出去,并在前后通过logging打印了两个日志,我们通过设置自定义的logger,在两部分日志的时间差与30ms做对比,如果超过30ms,认为是卡顿。将主线程堆栈信息写入到缓存文件并异步发送到日志后台。


常见的卡顿问题


sharepreference



  • 首次读取写入会loadxml到内存

  • sp文件修改是全量读写的

  • commit异步写入,通过CountdownLatch阻塞等待结果

  • apply延迟100ms写入,无返回结果

  • 主线程ANR,sp的修改会先体现在内存中,然后往QueueWorker中加入磁盘异步写数据的任务,但是会在Activity.onResume以及Service.onstartCommand等方法中增加waitToFinish等待磁盘写入完成的代码。

  • 解决方案使用MMKV

  • 尽量拆分小的xml


主线程操作文件


主线程网络操作


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

Silhouette——更方便的Shape/Selector实现方案

写在前面 首先祝大家新年快乐,开工大吉。 最新刚换了工作,大部分精力还是放到新工作上面,所以这次还是先给大家带来一个小而实用的库:Silhouette。另外,考虑到Kotlin越来越普及,作者在开发过程中也切实感受到Kotlin相较于Java带来的便利,后续的...
继续阅读 »

写在前面


首先祝大家新年快乐,开工大吉。

最新刚换了工作,大部分精力还是放到新工作上面,所以这次还是先给大家带来一个小而实用的库:Silhouette。另外,考虑到Kotlin越来越普及,作者在开发过程中也切实感受到Kotlin相较于Java带来的便利,后续的IM系列文章及项目考虑用Kotlin重写,而且考虑到由于工作业务需求过多可能出现断更的情况,所以打算一次性写完再放出来,避免大家学习不方便。

废话不多说,直接开始吧。


Silhouette是什么?


Silhouette意为“剪影”,取名并没有特别的含义,只是单纯地觉得意境较美。例如上一篇文章Shine——更简单的Android网络请求库封装的网络请求库:Shine即意为“闪耀”,也没有特别的含义,只是作者认为开源库起名较难,特意找一些比较优美的单词。

Silhouette是一系列基于GradientDrawableStateListDrawable封装的组件集合,主要用于实现在Android Layout XML中直接支持Shape/Selector等功能。

我们都知道在Android开发中,不同的TextViewButton各种样式(形状、背景色、描边、圆角、渐变等)的传统实现方式是在drawable文件夹中编写各种shape/selector等文件,这种方式至少会存在以下几种弊端:



  1. shape/selector文件过多,项目体积增大;

  2. shape/selector文件命名困难,命名规范时往往会存在功能重复的文件;

  3. 功能存在局限性:例如gradient渐变色。传统shape方式只支持三种颜色过渡(startColor/centerColor/endColor),如果设计稿存在四种以上颜色渐变,shape gradient无能为力。再比如TextView在常态和按下态需要同时改变背景色及文字颜色时,传统方式只能在代码中动态设置等。

  4. 开发效率低;

  5. 难以维护等;


综上所述,我们迫切需要一个库来解决以上问题,Silhouette正具备这些能力。接下来,我们来具体看看Silhouette能做什么吧。


Silhouette能做什么?


上面说到Silhouette是一系列组件集合,具体包含以下组件:




  • SleTextButton

    基于AppCompatTextView封装;

    具备定义各种样式(形状、背景色、描边、圆角、渐变等)的能力 ;

    具备不同状态(常态、按下态、不可点击态)下文字颜色指定等。




  • SleImageButton

    基于ShapeableImageView封装;

    通过指定sle_ib_type属性使ImageView支持按下态遮罩层、透明度改变、自定义图片,同时支持CheckBox功能;

    通过指定sle_ib_style属性使ImageView支持Normal、圆角、圆形等形状。




  • SleConstraintLayout

    基于ConstraintLayout封装;

    具备定义各种样式(形状、背景色、描边、圆角、渐变等)的功能。




  • SleRelativeLayout

    基于RelativeLayout封装;

    具备定义各种样式(形状、背景色、描边、圆角、渐变等)的功能。




  • SleLinearLayout

    基于LinearLayout封装;

    具备定义各种样式(形状、背景色、描边、圆角、渐变等)的功能。




  • SleFrameLayout

    基于FrameLayout封装;

    具备定义各种样式(形状、背景色、描边、圆角、渐变等)的功能。




设计、封装思路及原理




  • 项目结构

    com.freddy.silhouette



    • config(配置相关,存放全局注解及公共常量、默认值等)

    • extkotlin扩展相关,可选择用或不用)

    • utils(工具类相关,可选择用或不用)

    • widget(控件相关)

      • button

      • layout




    由此可见,项目结构非常简单,所以Silhouette也是一个比较轻量级的库。




  • 封装思路及原理

    由于该库非常简单,实际上就是根据Shape/Selector进行自定义属性,从而利用GradientDrawableStateListDrawable提供的API进行封装,不存在什么难度,在此就不展开讲了。


    下面贴一下代码片段,基本上几个组件的实现原理都大同小异,都是利用GradientDrawableStateListDrawable实现组件的ShapeSelector功能:




private fun init() {
val normalDrawable =
getDrawable(normalBackgroundColor, normalStrokeColor, normalGradientColors)
var pressedDrawable: GradientDrawable? = null
var disabledDrawable: GradientDrawable? = null
var selectedDrawable: GradientDrawable? = null
when (type) {
TYPE_MASK -> {
pressedDrawable = getDrawable(
normalBackgroundColor,
normalStrokeColor,
normalGradientColors
).apply {
colorFilter =
PorterDuffColorFilter(maskBackgroundColor, PorterDuff.Mode.SRC_ATOP)
}
disabledDrawable =
getDrawable(disabledBackgroundColor, disabledBackgroundColor)
}
TYPE_SELECTOR -> {
pressedDrawable =
getDrawable(pressedBackgroundColor, pressedStrokeColor, pressedGradientColors)
disabledDrawable = getDrawable(
disabledBackgroundColor,
disabledStrokeColor,
disabledGradientColors
)
}
}
selectedDrawable = getDrawable(
selectedBackgroundColor,
selectedStrokeColor,
selectedGradientColors
)
setTextColor(normalTextColor)
background = StateListDrawable().apply {
if (type != TYPE_NONE) {
addState(intArrayOf(android.R.attr.state_pressed), pressedDrawable)
}
addState(intArrayOf(-android.R.attr.state_enabled), disabledDrawable)
addState(intArrayOf(android.R.attr.state_selected), selectedDrawable)
addState(intArrayOf(), normalDrawable)
}

setOnTouchListener(this)
}

private fun getDrawable(
backgroundColor: Int,
strokeColor: Int,
gradientColors: IntArray? = null
): GradientDrawable {
// 背景色相关
val drawable = GradientDrawable()
setupColor(drawable, backgroundColor)

// 形状相关
(drawable.mutate() as GradientDrawable).shape = shape
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
drawable.innerRadius = innerRadius
if (innerRadiusRatio > 0f) {
drawable.innerRadiusRatio = innerRadiusRatio
}
drawable.thickness = thickness
if (thicknessRatio > 0f) {
drawable.thicknessRatio = thicknessRatio
}
}

// 描边相关
if (strokeColor != 0) {
(drawable.mutate() as GradientDrawable).setStroke(
strokeWidth,
strokeColor,
dashWidth,
dashGap
)
}

// 圆角相关
setupCornersRadius(
drawable,
cornersRadius,
cornersTopLeftRadius,
cornersTopRightRadius,
cornersBottomRightRadius,
cornersBottomLeftRadius
)

// 渐变相关
(drawable.mutate() as GradientDrawable).gradientType = gradientType
if (gradientCenterX != 0.0f || gradientCenterY != 0.0f) {
(drawable.mutate() as GradientDrawable).setGradientCenter(
gradientCenterX,
gradientCenterY
)
}
gradientColors?.let { colors ->
(drawable.mutate() as GradientDrawable).colors = colors
}
var orientation: GradientDrawable.Orientation? = null
when (gradientOrientation) {
GRADIENT_ORIENTATION_TOP_BOTTOM -> {
orientation = GradientDrawable.Orientation.TOP_BOTTOM
}
GRADIENT_ORIENTATION_TR_BL -> {
orientation = GradientDrawable.Orientation.TR_BL
}
GRADIENT_ORIENTATION_RIGHT_LEFT -> {
orientation = GradientDrawable.Orientation.RIGHT_LEFT
}
GRADIENT_ORIENTATION_BR_TL -> {
orientation = GradientDrawable.Orientation.BR_TL
}
GRADIENT_ORIENTATION_BOTTOM_TOP -> {
orientation = GradientDrawable.Orientation.BOTTOM_TOP
}
GRADIENT_ORIENTATION_BL_TR -> {
orientation = GradientDrawable.Orientation.BL_TR
}
GRADIENT_ORIENTATION_LEFT_RIGHT -> {
orientation = GradientDrawable.Orientation.LEFT_RIGHT
}
GRADIENT_ORIENTATION_TL_BR -> {
drawable.orientation = GradientDrawable.Orientation.TL_BR
}
}
orientation?.apply {
(drawable.mutate() as GradientDrawable).orientation = this
}
return drawable
}

感兴趣的同学可以到官方文档了解GradientDrawableStateListDrawable的原理。


自定义属性列表


自定义属性分为通用属性特有属性




  • 通用属性



    • 类型



















    属性名称类型说明备注
    sle_typeenum类型
    mask:遮罩
    selector:自定义样式
    none:无
    默认值:mask
    默认的mask为90%透明度黑色,可通过sle_maskBackgroundColors属性设置
    若不指定为selector,则自定义样式无效


    • 形状相关











































    属性名称类型说明备注
    sle_shapeenum形状
    rectangle:矩形
    oval:椭圆形
    line:线性形状
    ring:环形
    默认值:rectangle
    sle_innerRadiusdimension|reference尺寸,内环的半径shape="ring"可用
    sle_innerRadiusRatiofloat以环的宽度比率来表示内环的半径shape="ring"可用
    sle_thicknessdimension|reference尺寸,环的厚度shape="ring"可用
    sle_thicknessRatiofloat以环的宽度比率来表示环的厚度shape="ring"可用


    • 背景色相关





































    属性名称类型说明备注
    sle_normalBackgroundColorcolor|reference常态背景颜色/
    sle_pressedBackgroundColorcolor|reference按下态背景颜色/
    sle_disabledBackgroundColorcolor|reference不可点击态背景颜色默认值:#CCCCCC
    sle_selectedBackgroundColorcolor|reference选中态背景颜色/


    • 描边相关























































    属性名称类型说明备注
    sle_normalStrokeColorcolor|reference常态描边颜色/
    sle_pressedStrokeColorcolor|reference按下态描边颜色/
    sle_disabledStrokeColorcolor|reference不可点击态描边颜色/
    sle_selectedStrokeColorcolor|reference选中态描边颜色/
    sle_strokeWidthdimension|reference描边宽度/
    sle_dashWidthdimension|reference虚线宽度/
    sle_dashGapdimension|reference虚线间隔/


    • 圆角相关











































    属性名称类型说明备注
    sle_cornersRadiusdimension|reference总圆角半径/
    sle_cornersTopLeftRadiusdimension|reference左上角圆角半径/
    sle_cornersTopRightRadiusdimension|reference右上角圆角半径/
    sle_cornersBottomLeftRadiusdimension|reference左下角圆角半径/
    sle_cornersBottomRightRadiusdimension|reference右下角圆角半径/


    • 渐变相关



































































    属性名称类型说明备注
    sle_normalGradientColorsreference常态渐变背景色支持在res/array下定义数组实现多个颜色渐变
    sle_pressedGradientColorsreference按下态渐变背景色支持在res/array下定义数组实现多个颜色渐变
    sle_disabledGradientColorsreference不可点击态渐变背景色支持在res/array下定义数组实现多个颜色渐变
    sle_selectedGradientColorsreference选中态渐变背景色支持在res/array下定义数组实现多个颜色渐变
    sle_gradientOrientationenum渐变方向
    TOP_BOTTOM:从上到下
    TR_BL:从右上到左下
    RIGHT_LEFT:从右到左
    BR_TL:从右下到左上
    BOTTOM_TOP:从下到上
    BL_TR:从左下到右上
    LEFT_RIGHT:从左到右
    TL_BR:从左上到右下
    /
    sle_gradientTypeenum渐变类型
    linear:线性渐变
    radial:圆形渐变,起始颜色从gradientCenterX、gradientCenterY点开始
    sweep:A sweeping line gradient
    /
    sle_gradientCenterXfloat渐变中心放射点x坐标注意,这里的坐标是整个背景的百分比的点,并不是确切点,0.2就是20%的点
    sle_gradientCenterYfloat渐变中心放射点y坐标注意,这里的坐标是整个背景的百分比的点,并不是确切点,0.2就是20%的点
    sle_gradientRadiusdimension|reference渐变半径需要配合gradientType=radial使用,如果设置gradientType=radial而没有设置gradientRadius,将会报错


    • 其它

























    属性名称类型说明备注
    sle_maskBackgroundColorcolor|reference当sle_type=mask时,按钮按下状态的遮罩颜色默认值:90%透明度黑色(#1A000000)
    sle_cancelOffsetdimension|reference用于解决手指移出控件区域判断为cancel的偏移量默认值:8dp



  • 特有属性



    • SleConstraintLayout/SleRelativeLayout/SleFrameLayout/SleLinearLayout



















    属性名称类型说明备注
    sle_interceptTypeenum事件拦截类型
    intercept_super:return super
    intercept_true:return true
    intercept_false:return false
    Layout组件设置此值,可实现是否拦截事件,如果设置为intercept_true,事件将不传递到子控件,在某些场景比较实用


    • SleTextButton





































    属性名称类型说明备注
    sle_normalTextColorcolor|reference常态文字颜色/
    sle_pressedTextColorcolor|reference按下态文字颜色/
    sle_disabledTextColorcolor|reference不可点击态文字颜色/
    sle_selectedTextColorcolor|reference选中态文字颜色/


    • SleImageButton









































































    属性名称类型说明备注
    sle_ib_typeenum类型
    mask:图片遮罩
    alpha:图片透明度改变
    selector:自定义图片
    checkBox:CheckBox场景
    none:无
    1.指定为mask时,自定义图片资源无效;
    2.指定为alpha时,sle_pressedAlpha/sle_disabledAlpha生效;
    3.指定为selector时,sle_normalResId/sle_pressedResId/sle_disabledResId生效;
    4.指定为checkBox时,sle_checkedResId/sle_uncheckedResId/sle_isChecked生效;
    5.指定为none时,图片资源均不生效,圆角相关配置有效
    sle_ib_styleenumImageView形状
    normal:普通形状
    rounded:圆角
    oval:圆形
    默认值:normal
    sle_normalResIdcolor|reference常态图片资源/
    sle_pressedResIdcolor|reference按下态图片资源/
    sle_disabledResIdcolor|reference不可点击态图片资源/
    sle_checkedResIdcolor|reference选中态checkBox图片资源/
    sle_uncheckedResIdcolor|reference非选中态checkBox图片资源/
    sle_isCheckedbooleanCheckBox是否选中默认值:false
    sle_pressedAlphafloat按下态图片透明度默认值:70%
    sle_disabledAlphafloat不可点击态图片透明度默认值:30%



使用方式



  1. 添加依赖


implementation "io.github.freddychen:silhouette:$lastest_version"

Note:最新版本可在maven central silhouette中找到。



  1. 使用


由于自定义属性太多,在此就不一一列举了。下面给出几种常见的场景示例,大家可以根据自定义属性表自行编写:



  • 常态


Silhouette Normal



  • 按下态


Silhouette Pressed


以上布局代码为:


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
android:gravity="center_horizontal"
android:orientation="vertical">

<com.freddy.silhouette.widget.button.SleTextButton
android:id="@+id/stb_1"
android:layout_width="match_parent"
android:layout_height="54dp"
android:layout_marginHorizontal="48dp"
android:layout_marginTop="14dp"
android:gravity="center"
android:text="SleTextButton1"
android:textSize="20sp"
app:sle_cornersRadius="28dp"
app:sle_normalBackgroundColor="#f88789"
app:sle_normalTextColor="@color/white"
app:sle_type="mask" />

<com.freddy.silhouette.widget.button.SleTextButton
android:id="@+id/stb_2"
android:layout_width="match_parent"
android:layout_height="54dp"
android:layout_marginHorizontal="48dp"
android:layout_marginTop="14dp"
android:gravity="center"
android:text="SleTextButton2"
android:textSize="20sp"
app:sle_cornersBottomRightRadius="24dp"
app:sle_cornersTopLeftRadius="14dp"
app:sle_normalBackgroundColor="#338899"
app:sle_normalTextColor="@color/white"
app:sle_pressedBackgroundColor="#aeeacd"
app:sle_type="selector" />

<com.freddy.silhouette.widget.button.SleTextButton
android:id="@+id/stb_3"
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_marginHorizontal="48dp"
android:layout_marginTop="14dp"
android:enabled="false"
android:gravity="center"
android:text="SleTextButton2"
android:textSize="14sp"
app:sle_cornersBottomRightRadius="24dp"
app:sle_cornersTopLeftRadius="14dp"
app:sle_normalBackgroundColor="#cc688e"
app:sle_normalTextColor="@color/white"
app:sle_pressedBackgroundColor="#34eeac"
app:sle_shape="oval"
app:sle_type="selector" />

<com.freddy.silhouette.widget.button.SleImageButton
android:id="@+id/sib_1"
android:layout_width="84dp"
android:layout_height="84dp"
android:layout_marginTop="14dp"
app:sle_ib_type="mask"
app:sle_normalResId="@drawable/ic_launcher_background" />

<com.freddy.silhouette.widget.button.SleImageButton
android:id="@+id/sib_2"
android:layout_width="128dp"
android:layout_height="128dp"
android:layout_marginTop="14dp"
app:sle_ib_type="alpha"
app:sle_normalResId="@drawable/ic_launcher_background" />

<com.freddy.silhouette.widget.button.SleImageButton
android:id="@+id/sib_3"
android:layout_width="72dp"
android:layout_height="72dp"
android:layout_marginTop="14dp"
app:sle_ib_type="selector"
app:sle_normalResId="@mipmap/ic_launcher"
app:sle_pressedResId="@drawable/ic_launcher_foreground" />

<com.freddy.silhouette.widget.layout.SleConstraintLayout
android:id="@+id/scl_1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="48dp"
android:layout_marginTop="14dp"
android:paddingHorizontal="14dp"
android:paddingVertical="8dp"
app:sle_cornersRadius="10dp"
app:sle_interceptType="intercept_super"
app:sle_normalBackgroundColor="@color/white">

<ImageView
android:layout_width="72dp"
android:layout_height="48dp"
android:scaleType="centerCrop"
android:src="@mipmap/ic_launcher_round" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="UserName"
android:textColor="@color/black"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</com.freddy.silhouette.widget.layout.SleConstraintLayout>

<com.freddy.silhouette.widget.layout.SleLinearLayout
android:id="@+id/sll_1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="48dp"
android:layout_marginTop="14dp"
android:gravity="center_vertical"
android:paddingHorizontal="14dp"
app:sle_type="selector"
android:paddingVertical="8dp"
app:sle_cornersTopRightRadius="24dp"
app:sle_cornersBottomRightRadius="18dp"
app:sle_interceptType="intercept_true"
app:sle_pressedBackgroundColor="#fe9e87"
app:sle_normalBackgroundColor="#aee949">

<ImageView
android:layout_width="72dp"
android:layout_height="48dp"
android:scaleType="centerCrop"
android:src="@mipmap/ic_launcher_round" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="14dp"
android:text="UserName"
android:textColor="@color/black"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</com.freddy.silhouette.widget.layout.SleLinearLayout>
</LinearLayout>

Note:需要给组件设置setOnClickListener才能看到效果。

至于更多的功能,就让大家去试试吧,篇幅有限,就不一一列举了。有任何疑问,欢迎通过QQ群微信公众号联系我。


版本记录






















版本号修改时间版本说明
0.0.12022.02.10首次提交
0.0.22022.02.12修改minSdk为19

写在最后


终于写完了,Shape/Selector在每个项目中基本都会用到,而且频率还不算低。Silhouette原理虽然简单,但确实能解决很多问题,这些都是平时开发中的积累,希望对大家能有所帮助。欢迎大家starfork,让我们为Android开发共同贡献一份力量。另外如果有疑问欢迎加入我的QQ群:1015178804,同时也欢迎大家关注我的公众号:FreddyChen,让我们共同进步和成长。


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

Android UI适配方案

大纲 使用dp而不是px 尽量使用自动适配布局,而不要指定分辨率 使用宽高限定符 values-1080x1920,以1080P为基准计算每种常见分辨率对应的尺寸。 需要尽可能全的添加各种设备的分辨率(有工具) 容错性不足,如果设备分辨率不能精确匹配对应限...
继续阅读 »

大纲



  1. 使用dp而不是px

  2. 尽量使用自动适配布局,而不要指定分辨率

  3. 使用宽高限定符

    1. values-1080x1920,以1080P为基准计算每种常见分辨率对应的尺寸。

    2. 需要尽可能全的添加各种设备的分辨率(有工具)

    3. 容错性不足,如果设备分辨率不能精确匹配对应限定符,会默认使用统一默认的dimens



  4. 第三方自动适配UI框架

    1. 原理:自定义RelativeLayout,在onMeasure中对控件分辨率做变换

    2. 第三方框架,维护性很成问题

    3. 一些自定义View,处理比较麻烦



  5. 最小宽度限定符,类似宽高限定符

    1. values-sw240dp,同样以某一dp宽度为基准计算其他宽度dp的值

    2. values-sw360dp、values-sw480dp

    3. 相比宽高限定符,最小宽度限定符不进行精确匹配,会遵循就近原则,可以较好的解决容错问题。

    4. 如:设备宽364dp,系统会自动就近配置values-sw360dp下的dimens,显示效果相差不会很大



  6. 今日头条——修改density值

    1. 原理:px = dp x (dpi/160) = dp x density

    2. 既然如此,将density

    3. 需要UI出设计图时以统一的dp为基准

    4. mp.weixin.qq.com/s/d9QCoBP6k…




基本概念



  • 像素——px

  • 密度独立像素——dp或dip

  • 像素密度——dpi,单位面积内的像素数。

    • 软件系统的概念。

    • 在系统出厂时,配置文件中的固定值。

    • 通常的取值有:160、240、360、480等。

    • 不同于物理概念上的屏幕密度ppi,如ppi为415、430和470时,dpi可能会统一设置为480。



  • density——当dpi=160时,1px = 1pd,此时denstiy的值为1,dpi=240时,1.5px = 1dp,density的值为1.5。

  • 上述值的关系:

    • denstiy = dpi / 160;

    • px = dp x density = dp x (dpi / 160)




Android设备的碎片化极为严重,各种尺寸和分辨率的设备无比繁多。使得在Android开发中,UI适配变成了开发过程中极为重要的一步。为此Google提出了密度独立像素dip或dp的概率,旨在更友好的处理Android UI适配问题。


但是效果嘛,只能说差强人意,可以解决大部分的业务场景,但是剩下的个别情况就搞死人了,原因在于Android设备碎片化实在太严重了,存在各种分辨率和dpi的设备。


比如两台设备A和B,分辨率是1920x1080,dpi分别为420和480,在布局中编写一个100dp宽的ImageView,按照上面的公式ImageView的显示宽度分别为:100dp x 420 / 160 = 262.5100dp x 480 / 160 = 300,ImageView在B设备上明显显示要大一些。差异可能还不明显,我们把宽度改为360dp呢,A设备显示宽度为:948px,B设备显示宽度为:1080px。这就扯淡了,一个宽度填充满屏幕,一个不满。这种情况肯定是需要开发来背锅解决的。


适配方案


虽然上面提到了使用dp无法解决全部业务场景,但是相对于直接使用px已经可以解决大部分场景下的适配问题了。


所以UI适配的第一条就是:


1. 使用dp代替px来编写布局。


又因为上面无法适配的个别场景,所以UI适配的第二条是:


2.尽量使用自动适配布局,而不要指定分辨率


这一条也很好理解,尽量使用ConstraintLayout 约束布局和LinearLayout等父布局,不要写死分辨率,比如上面的例子如果使用match_parent而不是360dp,也可以避免出现显示不一致问题(但是仅限于上列)。


限定符


Google同样意识到dp满足所以业务场景的需要,所以提供了宽度限定符的概念。



虽然您的布局应始终通过拉伸其视图内部和周围的空间来应对不同的屏幕尺寸,但这可能无法针对每种屏幕尺寸提供最佳用户体验。例如,您为手机设计的界面或许无法在平板电脑上提供良好的体验。因此,您的应用还应提供备用布局资源,以针对特定屏幕尺寸优化界面设计。



最小宽度限定符



使用“最小宽度”屏幕尺寸限定符,您可以为具有最小宽度(以密度无关像素 dp 或 dip 为度量单位)的屏幕提供备用布局。


通过将屏幕尺寸描述为密度无关像素的度量值,Android 允许您创建专为非常具体的屏幕尺寸而设计的布局,同时让您不必对不同的像素密度有任何担心。



通俗一点翻译就是:可用通过xxxx-swXXXdp的方式定义一些最小限定符的资源文件,比如:values-sw400dp、values-sw600dp,系统会自动匹配如屏幕宽度相近资源文件夹。


我们再来看上面的例子两台设备A和B,分辨率是1920x1080,dpi分别为360和400。我们简化下问题比如设计图给的是1920x1080 360dpi,包含一个22.5px * 22.5px = 10dp * 10dp的图片。按经验布局应该如下编写:


<ImageView
android:id="@+id/img_iv"
android:layout_width="10dp"
android:layout_height="10dp"
android:background="@mipmap/ic_launcher"/>

在不同设备上运行的结果:



  • 1280 x 720 240dpi的设备,图片显示为15px * 15px;

  • 1920 x1080 360dpi的A设备,图片显示为22.5px * 22.5px;

  • 1920 x1080 400dpi的B设备,图片显示为25px * 25px;


可以看到B设备图片显示是有问题的,为了解决这个问题,我们使用最小宽度限定符定义两个资源文件夹:values-sw360dp和values-sw400dp。


在values-sw360dp中添加dimen.xml内容如下:


<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="dp_1">1dp</dimen>
<dimen name="dp_2">2dp</dimen>
<dimen name="dp_3">3dp</dimen>
<dimen name="dp_4">4dp</dimen>
<dimen name="dp_5">5dp</dimen>
<dimen name="dp_6">6dp</dimen>
<dimen name="dp_7">7dp</dimen>
<!-- 省略其他值 -->
<dimen name="dp_360">360dp</dimen>
<!-- 因为设计图是360dpi,所以控件尺寸通常不会超过360dp,定义最大360dp的值足够使用 -->
</resources>

在values-sw420dp中添加dimen.xml,文件中的dimen值很容易换算出来:在360dpi中dp_1 = 1dp,那么在400dpi中dp_1 = 360 / 400 = 0.9dp,文件内容如下:


<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="dp_1">0.9dp</dimen>
<dimen name="dp_2">1.8dp</dimen>
<dimen name="dp_3">2.7dp</dimen>
<dimen name="dp_4">3.6dp</dimen>
<dimen name="dp_5">4.5dp</dimen>
<dimen name="dp_6">5.4dp</dimen>
<dimen name="dp_7">6.3dp</dimen>
<!-- 省略其他值 -->
</resources>

注意要在values文件夹下添加默认dimen.xml,文件内容与values-sw360dp中添加dimen.xml一致(因为设计图恰好是360dpi的)。


布局中的ImageView自然要改写为:


<ImageView
android:id="@+id/img_iv"
android:layout_width="@dimen/dp_10"
android:layout_height="@dimen/dp_10"
android:background="@mipmap/ic_launcher"/>

我们再来看一下不同设备运行结果:



  • 1280 x 720 240dpi的设备,未匹配到限定符使用values中的dimen,dp_10 = 10dp, px = 10 * 240 / 160 = 15px,图片显示尺寸为15px * 15px。

  • 1920 x1080 360dpi的A设备,匹配到sw360dp限定符,dp_10 = 10dp, px = 10 * 360 / 160 = 20px,图片显示尺寸为22.5px * 22.5px。

  • 1920 x1080 400dpi的B设备,匹配到sw420dp限定符,dp_10 = 9dp, px = 9 * 400 / 160 = 20px,图片显示尺寸为22.5px * 22.5px。


完美的解决了设备A和B的显示问题,所以UI适配的第三条是:


3. 使用最小(可用)宽度限定符,解决同样分辨率不同dpi的设备适配问题。


这种方案看似完美,但是也有一些隐含的问题:此方案只能解决同样分辨率不同dpi设备的适配问题:



  • 一旦出现不同分辨率相同dpi的情况就无效了(当然这种情况的可能性不高)。

  • 以上举例只是基于1920x1080这一种分辨率为例说明,试想一下如果1280x720的设备存在240dpi和280dpi的情况呢?我们只能针对特殊情况适配处理,无法解决全部场景适配问题。


宽高限定符


类似于上面说的最小宽度限定符,但是需要精确指定要匹配的设备宽高,values-1920x1080、values-1280x720等。配置与使用方式也与上面类似,如设计图尺寸为1920x1080 360dpi,那么只需要以1920x1080为基准计算所有分辨率对应的尺寸就可以了,布局编写时按照给的尺寸一一对应就可以,比如:给出的ImageView是20px*20px的,那在布局中同样指定width和height为@dimen/dp_20就可以了。


values-1920x1080中dimens.xml如下:


<resources>
<dimen name="dp_1">1px</dimen>
<dimen name="dp_2">2px</dimen>
<dimen name="dp_3">3px</dimen>
<dimen name="dp_4">4px</dimen>
<dimen name="dp_5">5px</dimen>
<dimen name="dp_6">6px</dimen>
<dimen name="dp_7">7px</dimen>
<dimen name="dp_8">8px</dimen>
<dimen name="dp_9">9px</dimen>
<!-- 省略其他 -->
<dimen name="dp_1920">1920px</dimen>
</resources>

values-1280x720中dimens.xml换算为:


<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="dp_1">0.66px</dimen>
<dimen name="dp_2">1.33px</dimen>
<dimen name="dp_3">2.0px</dimen>
<dimen name="dp_4">2.66px</dimen>
<dimen name="dp_5">3.33px</dimen>
<dimen name="dp_6">4.0px</dimen>
<dimen name="dp_7">4.66px</dimen>
<!-- 省略其他 -->
</resources>

同样需要在values添加默认尺寸dimen.xml,内容同基准分辨率文件。


因为不是所有设备屏幕都是16:9的,也可以按照宽高拆分成两个dimens.xml文件dimen_x.xml和dimen_y.xml,按照宽高:1920x1080分别换算得到x和y的值,但是页面设计通常是竖屏可滑动的,所以对高度不敏感,只需要根据一个维度计算统一值就可以了。


以上计算方式比较简单了,不需要自己编写换算可以通过代码工具或者自己写个类实现。(网上有好多,找一下应该可以找到)。


理论上只要尽可能多的枚举所有设备分辨率,就可以完美的解决屏幕适配问题,所以UI适配的第四条是:


4.使用宽高限定符,精确匹配屏幕分辨率。


这种方案已经近乎完美了,一度成为比较热门的解决方案,也有很多团队使用过此方案。但是之前也说过Android设备的碎片化太严重了,综合考虑基本不可能在项目中枚举所有的屏幕尺寸进行适配,如果设备没有匹配到对应尺寸会使用values下的默认尺寸文件,可能会出现严重的UI适配问题。


但是不可否认此种方案实现简单,对于编写布局也很友好(直接填入设计图的尺寸值就行,不需要换算),可以解决绝大多数的设备适配问题,是一种很友好的解决方案。


第三方UI适配框架


有很多第三方库的解决方案,是从ViewGroup入手的,要么重写常用的如:RelativeLayout、LinearLayout和FrameLayout等在控件内部做转换来适配不同尺寸的设备,要么提供新的Layout如:Google的PercentLayout布局。但是这些方案基本都不在维护了,这里就不详细展开了,感兴趣的可以自行搜索了解。


UI适配的第五条是:


5. 使用第三方自适配框架,解决UI适配问题。


感兴趣的可以参考以下文档:



其他适配方案


参考字节的实现方案:


一种极低成本的Android屏幕适配方式


这篇文章着实属于拾人牙慧了,起因是因为看到了这篇博客Android 目前最稳定和高效的UI适配方案。所以想着确实应该把这部分知识梳理一下,所以写了这篇文档加了一些自己的里面,主要也是为了梳理知识点加深理解。


文中列举的几种UI适配方案,没有严格的优劣之分,可以根据自己的业务需求选择,也可以选择几种搭配使用,比如笔者目前主要做智能电视(盒子)的应用开发,Android电视不同于手机,碎片化没有那么严重,电视分辨率种类屈指可数,所以在日常项目中基本选择使用宽高限定符的方案进行适配,效果也是极好的。


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

Cookbook for Programmers

学了 Python Cookbook、 Bash Cookbook、 MySQL Cookbook、 Redis Cookbook、 Regular Expressions Cookbook、 React Native Cookbook、 HTML5 Cook...
继续阅读 »
学了
Python Cookbook、
Bash Cookbook、
MySQL Cookbook、
Redis Cookbook、
Regular Expressions Cookbook、
React Native Cookbook、
HTML5 Cookbook、
LATEX Cookbook、
RESTful Web Services Cookbook
……
通过了各个大厂的面试,为什么就是达不到心仪妹子的要求?
你需要学一学这本Cookbook!

不是标题党!这是一份正经的菜谱,符合程序员思维的菜谱!

学起来,去征服她的胃吧!


程序员做饭指南

GitHub Workflow Status (branch) GitHub GitHub contributors

最近在家隔离,出不了门。只能宅在家做饭了。作为程序员,我偶尔在网上找找菜谱和做法。但是这些菜谱往往写法千奇百怪,经常中间莫名出来一些材料。对于习惯了形式语言的程序员来说极其不友好。

所以,我计划自己搜寻菜谱和并结合实际做菜的经验,准备用更清晰精准的描述来整理常见菜的做法,以方便程序员在家做饭。

同样,我希望它是一个由社区驱动和维护的开源项目,使更多人能够一起做一个有趣的仓库。所以非常欢迎大家贡献它~

如何贡献

针对发现的问题,直接修改并提交 Pull request 即可。

在写新菜谱时,请复制并修改已有的菜谱模板: 示例菜。 在提交 Pull Request 前更新一下 README.md 里的引用。

做菜之前

菜谱

家常菜

早餐

主食

半成品加工

红烧菜系

汤与粥

饮料

酱料和其它材料

甜品

进阶知识学习

如果你已经做了许多上面的菜,对于厨艺已经入门,并且想学习更加高深的烹饪技巧,请继续阅读下面的内容:

作者:Anduin2017

来源:https://github.com/Anduin2017/HowToCook

收起阅读 »

程序员有哪些含金量高的证书可以考?

近来IT行业成为了发展前景好高薪资的大热门,社会上也出现了“计算机考试热”。总体看来,越来越多的人选择参加各种各样的计算机考试,就是为了拿含金量高的证书,提升自己的职场竞争力。那么程序员有哪些含金量高的证书可以考?下面黑马程序员小编将详细介绍一下含金量高的IT...
继续阅读 »

近来IT行业成为了发展前景好高薪资的大热门,社会上也出现了“计算机考试热”。总体看来,越来越多的人选择参加各种各样的计算机考试,就是为了拿含金量高的证书,提升自己的职场竞争力。

那么程序员有哪些含金量高的证书可以考?下面黑马程序员小编将详细介绍一下含金量高的IT证书,避免大家在不需要的考试上浪费时间。

1、MCSE,MCDBA,MCAD/MCSD微软认证
包括系统管理方向,数据库方向和开发方向的证书。分别叫做微软的技术还是比较有用的,比如MCSE,维护、管理局域网非常有用。但到实际的网络公司工作,还应该学习CCNA、CCNP方面的技术,由局域网管理扩展到广域网管理,所学的路由、交换、远程接入、网络故障排除等技术更为实用。目前在用人单位的招聘信息里,CCNP是这类公司招聘的首要挑选因素。

2、IBM认证
国内常见的有考电子商务方向,数据库方向,大型机方向,开发方向等等。由于名目太多,这里不列出了,有兴趣可以到IBM的网站或Prometric或VUE网站(这两家是国家两大认证考试中心)上查看(其他国际公司的认证介绍也都可以在这两家考试中心的网站上查看)。

3、Sun认证
Sun认证主要包括两大方向,一个是Sun Solaris系统的管理方向,另一个是非常流行的Java认证方向。其中Java方向包括:SCJP,SCJD,SCWCD,SCMAD,SCWSD,SCEA等,最高级别是SCEA,名称为Sun认证企业应用架构师。
pic_457d968b.png

4、Cisco证书
传说中有"钱"途的证书。注重网络知识的普遍性及与实际操作的紧密结合,教材由浅入深,教授方式灵活生动。CCNA认证 课程内容:包括网络操作、不同联网产品间的区别、如何设计网络并排除故障及其它一般性知识等; 课程目的:就设计、建立和维护能够支持全国及全球性机构的网络原则和实践对学生进行培训,CCNA能够根据培训和显示世界经验的结合提供的解决方案例子。

5、Adobe认证
Adobe中国教育认证计划的核心内容之一。遵循"国际品质、中国制定"的一贯开发理念和原则,在品质控制和规范管理下,Adobe认证逐渐获得社会的认可并深入人心,已经成为中国数字艺术教育市场主流的行业认证标准。

6、 Linux认证
指获得专业Linux培训后通过考试得到的资格。2013年国际上广泛承认的Linux认证有?LinuxProfessionalInstitute(简称为LPI)、SairLinux和GNU、Linux+和RedHatCertifiedEngineer。

7、CIW证书
由以下三个国际性的互联网专家协会认可并签署:国际Webmaster协会(IWA)、互联网专家协会(AIP)及位于欧洲的国际互联网证书机构(ICII)。CIW认证是惟一针对互联网专业人员的国际权威认证,适合设计、开发、管理、安全防护、技术支持互联网企业网相关业务的人士。培训内容由美国五十余家专业机构制定,从而保证了网络知识的全面性和专业性,形成一种中立的、标准全面的培训课程。CIW培训注重网络管理的应用和基础理论,学员不仅可以学到网络知识,还能学到实用技术;不仅学到理论,还学到具体的操作技术,并且广泛适用于企业的各种相关产品。

8、RHCE
Red Hat公司是目前最大的Linux软件产品供应服务商。RHCE是Red Hat公司授权全球企业认同的认证,为学习Linux技术者提供多样选择。在各家国际性的技术认证制度当中,RHCE认证强调受测考生实际动手的测验方式。适合人员:没有Linux或UNIX命令使用经验,但是想进一步了解如何使用和优化计算机上的Red Hat Linux的学习者。就业分析:由于这项认证具有很高的测试应试者实际技能的水平,难度较高,它的持有者会对自己的就业前景充满信心。

9、华为认证
华为认证是深圳华为技术有限公司(简称"华为")凭借多年信息通信技术人才培养经验及对行业发展的理解,基于ICT产业链人才个人职业发展生命周期,以层次化的职业技术认证为指引,搭载华为"云-管-端"融合技术,推出的覆盖IP、IT、CT以及ICT融合技术领域的认证体系;是唯一的ICT全技术领域认证体系。

以上介绍的证书虽然含金量很高,但是都不是程序员必须要考的。毕竟企业公司在招聘时,更看重的是程序员的项目经验和操作能力。当然,大家可以“以证促学”,把证书考试当成是检验自己能力水平的奋斗努力方向。

作者:骨灰级收藏夹
来源:https://blog.csdn.net/JACK_SUJAVA/article/details/110188664

收起阅读 »

男生「理想女友」职业排名出炉!程序媛排第3,第1没有争议?

我们常说,到了该结婚的年纪,选择什么样的人度过一生,决定着一个人后半生的幸福指数。择偶的时候,需要考虑的因素有很多,性格、长相、工作、学历、家庭等等。在男生眼中,同样颜值和学历的女生,工作不同,魅力指数也不一样。工作好的人,即使其他方面表现一般,也很容易成为婚...
继续阅读 »

我们常说,到了该结婚的年纪,选择什么样的人度过一生,决定着一个人后半生的幸福指数。择偶的时候,需要考虑的因素有很多,性格、长相、工作、学历、家庭等等。

在男生眼中,同样颜值和学历的女生,工作不同,魅力指数也不一样。工作好的人,即使其他方面表现一般,也很容易成为婚恋市场中的“红人”。

今天,播妞和大家一起来看看男生眼中「理想女友」的工作排名情况,很可能与你以为的完全不同哦。

「理想女友」职业排行榜

第一名:大学老师
在男生眼中,担任“大学老师”的女生魅力指数极高,原因很简单,大学老师往往意味着高学历,起码是硕士研究生及以上的文凭了。再者,大学老师工作相对轻松,时间也比较自由,不是那么累,并且大学老师收入可观,自然就很受欢迎了。

pic_e9f05b2b.png
第二名:医生
排名第二的,是被称为“白衣天使”的医生,医生主要从事救死扶伤的工作,不仅崇高,收入也很多,是典型的越老越吃香的工作。此外,如果另一半是医生,家人有个头疼脑热的,更容易被解决,和医生组成家庭,能有效提高生活水平。

pic_dae19819.png

第三名:程序媛
因为互联网的不断发展,程序员岗位在如今非常火,但是在大家的印象中,这个工作一般是男生比较多,所以程序媛排在第三名,还是比较出人意料的。

从事这个职业的女生,无论是在学习能力还是逻辑思维上都比较优秀,并且,程序员高薪已经算是互联网人的共识,发展前景和薪资名列前茅。

根据《中国女性程序员职场力大数据报告》报告,越来越多女性正加入程序员的行列,其平均月薪也达到1.5万元,薪酬涨幅超男性程序员。

之前看到过一个帖子讨论说,如果夫妻两人都是大厂程序员,是不是在北京上海买房很简单?财富自由很简单?高收入群体结合在一起岂不是“王炸”组合?

原帖内容:

pic_b79e26f8.png
△ 图片来源于网络,如侵删

这条帖子发出之后,引起了不少网友的讨论。有网友表示,自己和女友就都是大厂程序员,俩人10年时间存下几百万很简单,但是不能让钱只躺在银行,需要做一些理财,让钱生钱,并且两个人一起存钱会更加容易。

也有网友表示,虽然说夫妻两人都是程序员家庭经济状况会没有压力,但两个程序员在一起会不会非常无聊,让生活变得平静如水,没有乐趣。(当然这点,播妞本人是不太赞同的,有的程序员也很浪漫的)对于夫妻两个都是大厂程序员的组合,你看好吗?

pic_308d2068.png

更多排名:

就不一一介绍了,大家可以自己看图哦↓
pic_aacdc8dd.png

作者:骨灰级收藏夹
来源:https://blog.csdn.net/JACK_SUJAVA/article/details/123051958

收起阅读 »

2022技术趋势预测:Python、Java占主导,Rust、Go增长迅速,元宇宙成为关注焦点

2021年是技术不断发展的一年,新技术层出不穷,从移动时代到云计算大数据再到人工智能、机器学习、云原生等逐渐为人们所知晓。技术更迭、日新月异,但万变不离其宗,许多核心技术依旧占据主导,新技术的到来在注入新鲜血液的同时,也促使核心技术的不断更新。2022年1月2...
继续阅读 »

2021年是技术不断发展的一年,新技术层出不穷,从移动时代到云计算大数据再到人工智能、机器学习、云原生等逐渐为人们所知晓。技术更迭、日新月异,但万变不离其宗,许多核心技术依旧占据主导,新技术的到来在注入新鲜血液的同时,也促使核心技术的不断更新。

2022年1月25日, O'Reilly发布了《2022年技术趋势》报告,该报告针对技术发展进行了全面分析,统计了2021年1月至2021年9月的数据,并与2020年同期数据进行了比较。其中涉及微服务、云服务、Web框架、Kubernetes、人工智能、机器学习、数据库、虚拟现实、增强现实和元宇宙等热点话题。

此次报告基于四种数据进行了分析,包括搜索查询、O' Reilly Answer中的提问、按标题分类的资源使用情况以及按主题分类的资源使用情况。其中平台上暂未收集的内容(如QUIC协议或HTTP/3)均未纳入统计范围

数据成搜索频率最高词汇,2022或将继续占主导

作为智能搜索引擎,O'Reilly Answers允许用户搜索特定问题或查找问题库中的示例问题。此次报告对中O'Reilly Answers出现的所有词汇进行统计,结果表明,出现频率最多的五个词汇分别是“数据”、“Python”、“Git”、“测试”和“Java”;而用户搜索频率最高的问题分别是“什么是动态编程?”以及“怎样写好单元测试?”。

由此我们可以得出,数据仍然是开发人员最为关注的话题之一。其中与数据相关的最常见的词对是“数据治理”,其次是“数据科学”。而“数据分析”和“数据工程”的排名较后。这表明,“数据治理”将是数据领域的研究重点。

在过去的数据统计中,PythonJava都是排名前两位的编程语言,今年同样如此。不同之处在于,Python和Java的搜索频率有所下降,而RustGo的频率在迅速增长。除此之外,“编程”也是最常用的关键词之一。位列第三的是Kubernetes,之后分别为Golang和Rust。其中Kubernetes的提问频率反映了对于容器编排的兴趣。

另外,“AWS”、“Azure”和“云”同样也是搜索频率非常高的词汇,这表明开发人员非常关注云平台的发展。GCP谷歌云的频率同样排在榜单前3%。

有关加密货币的词汇(如比特币、以太坊、加密货币、NFT等)频率仍位于前20%,但排名有所下滑。

网络安全成企业关注重点,今年将有何突破?

2021年,由于勒索软件的攻击,各大基础设施、医院以及企业等安全性受到前所未有的威胁。O'Reilly调查显示,有6%的受访者公司遭到攻击。2021年7月6日,美国软件商Kaseya遭到攻击,成千上万的客户受到此次攻击的影响。该公司首席执行官Fred Voccola表示,攻击者要求支付一笔高达7000万美元的赎金。

据O'Reilly研究表明,从这一年开始,网站上安全相关内容大幅增加,有关勒索软件的内容增加了270%,与此同时,隐私相关内容增加了90%。除此之外,有关应用软件安全性、恶意软件、威胁等内容分别有不同程度地增加。

除此之外,标题中带有“安全”或是“网络安全”字样的文章浏览量分别增加了17%和24%。尽管和上述内容相比,这些关键词的增长相对缓慢,但在总数上,提及“安全”的频率远远领先于其他所有词汇。


安全相关的浏览次数以及同比增长

软件架构、Kubernetes和微服务提及次数最多

软件开发是O'Reilly平台中的一大类别,其中涵盖许多内容,例如编程语言、云以及架构等等。数据表明,软件架构Kubernetes微服务是2021年提及次数最多的三个主题,它们的同比增长分别为19%、15%和13%。尽管与API网关(增长218%)等主题的增长趋势相比,这三个数据的增长显得微不足道。但这也反映了一个规律:规模较小的主题的增长趋势较为明显,而已经占据主导地位的主题则增长较为缓慢。API网关相关内容的数量大约是架构或是Kubernetes内容的1/250。

然而,尽管API网关的数量相对较少,但218%的增长仍然令人意外。云原生获得的54%的增长也是如此。如今企业正在大力投资Kubernetes和微服务,他们正借助云服务构建云原生应用程序,而API网关则是客户端和服务之间路由请求的重要工具。

在这种情况下,容器的内容提及次数的显著增长(137%)绝非偶然,容器是打包应用程序和服务的最佳方式。尽管将应用程序迁移到容器并使用Kubernetes生态系统中的工具进行管理的难度不小,但在几年前,企业的应用程序只能运行在少量服务器上,并且只能由人工进行管理。而如今许多企业的规模在不断扩大,拥有数千台服务器,并且提供数百项服务。这都归功于云技术的发展。

提到微服务,不得不提到分布式系统。有关分布式系统的内容在过去一年中增长了39%,相应的,复杂系统和复杂性的提及次数也在不断增长(157%和8%)。同样值得注意的是,几年前不受欢迎的设计模式再次卷土重来,并实现了19%的增长。

量子计算仍然是一个有趣的话题,尽管浏览量较少,但同比增长了39%。对于一个尚未成功的技术而言,这个成绩已经非常好了。尽管量子计算机已有所突破,但制造出能完成工作的量子计算机还需要不少时间。一旦量子计算机到来,势必能够带来新的变革。

除此之外,软件架构同样有着重要的作用,没有架构,我们无法重建遗留应用程序、无法使用云技术、也无法使用微服务等等。软件架构能够帮助维护不灵活的遗留应用程序使它们随着需求的变化而不断更新。因此软件架构的提及次数不断增加也在意料之中。


编程语言的浏览量和同比增长

云服务不断发展,云原生将为我们带来什么?

过去一年云技术不断发展,云服务的竞争越发激烈。调查显示,AWS的内容减少了3%,而Microsoft AzureGoogle Cloud的内容分别增长了54%,其中有关Azure的内容几乎与和AWS的数量相等,Google Cloud位列第三。除了云服务之外,有关云的内容在去年增长了15%,而云原生内容的增长幅度高达54%。

另一个趋势在于,有关混合云和多云的的内容基数依旧非常小(大约是Google Cloud的十分之一),但增长速度非常快(分别为145%和240%)。这反映了一个问题,企业无法仅仅通过单一的云服务器构建云战略。构建云战略就必须要意识到云本质上就是多(或混合的),最重要的不是选择哪一个云服务器,而是如何跨多个云服务器构建有效的云架构,这成为了云原生的一个重要内容。


云服务器的浏览量和同比增长

Web框架稳定发展,元框架是否会打破格局?

在过去两年中,Web编程技术一直稳定发展。有关核心组件HTML、CSS和JavaScript的内容几乎没有变化(分别上升1%、2%和下降3%)。如果Java和Python是企业和数据开发人员的核心语言,那么HTML、CSS和JavaScript对于前端开发人员来说更是如此。据统计,有关PHP的内容增加了6%,有关jQuery的内容增加了28%而有关网页设计的内容增加了23%。

在新兴框架和元框架中,Svelte似乎正在迅速发展(增长71%),Vue和Next.js的内容有所减少(均减少13%)。若这种情况持续下去,Svelte可能会在几年内成为流行框架之一。

而有关React框架的内容数量基本没有变化(增长2%),但Angular框架的内容显著减少(减少16%)。JavaScript的数量与React的几乎相同,Rails的内容则减少19%。


Web框架的数量和同比增长

薛定谔的人工智能、机器学习和数据

尽管网络上出现了许多有关人工智能的预测,有人认为人工智能将面临低谷,也有人说它将是未来的新秀。但据O'Reilly表明,在2021年,标题中带有“人工智能”的内容减少了23%,而有关人工智能的内容在2021年减少了11%。主导这一领域的主题是机器学习(ML),人工智能的内容数量仅为机器学习的四分之一。

现在让我们来看看部分具体的技术。深度学习的内容减少了14%,但神经网络的内容增加了13%,强化学习增加了37%,对抗性网络增加了51%。由此看来,开发者的关注点已经从一般内容转向了具体内容。

同样值得关注的是,有关数据治理(增加87%)和GDPR(增加61%)的内容显著增加。数据治理及其相关内容(如数据来源、数据完整性、审计、可解释性等)将越来越重要。未来对于数据的监管势必会更加严厉。数据治理将继续存在。


AI和ML等内容的数量和同比增长

NoSQL数据库出路何在?

没有数据和数据库,就不存在机器学习。数据表明,Oracle在数据库中占据主导地位,其内容增加了5%,开源MySQL数据库的内容增加了22%,NoSQL的内容减少了17%,其中包括Cassandra、HBase、Redis、MongoDB等等。NoSQL与其说是一种技术,不如说是一种理念——致力于为系统设计人员扩展储存选项的数量。

在NoSQL数据库中,MongoDB的内容增加了10%。Cassandra、Redis和HBase的内容大幅减少(分别为27%、8%和57%)。尽管自2020年以来,这四种数据库的内容总数减少了4%,但比MySQL的内容数量多40%。尽管趋势已经由NoSQL转向关系数据库,但这并非最终结果。

在去年,图形数据库受到越来越多人的关注,其内容增加了44%,但这仍然是一个较小的类别。同样,有关时序数据库的内容增加了21%。时序数据库指的是用来存储时序列数据并以时间(点或区间)建立索引的软件,对于关于监控、日志记录和可观察性的应用程序非常重要。

尽管图形数据库和关系数据库正迅速发展,但关系数据库仍然并且将持续主导着数据库世界,NoSQL没有机会取代关系数据库。


数据库内容数量及同比增长

虚拟现实or增强现实?元宇宙进入大众视野

虚拟现实(VR)和增强现实(AR)同样是O'Reilly中的热点话题。尽管它们几度成为热点,但从未持续多久。早在2013年,谷歌眼镜就成为热点,但从未得到广泛使用。而像Oculus这样的初创公司已经针对消费者制造了VR眼镜,但它们从未成功打入玩家市场。

然而在今年,我们仍然认为VR和AR具备极大的潜力。马克·扎克伯格早在去年7月份就提出了“元宇宙”,并将Facebook重新命名为Meta,从而引发了一场新变革。微软等其他公司也纷纷效仿,推出了自己的Metaverse版本。苹果一直保持低调,但该公司被曝出正在开发AR眼镜。

数据表明,虚拟现实、VR和AR相关内容在不断增加(分别增加了13%、28%和116%)。但由于O'Reilly的数据统计截止到去年9月,“metaverse”一词并未纳入统计,尽管它的搜索量急剧增加了489%。


VR和AR的内容数量和同比增长

2022年技术预测,哪些领域将再次登顶?

在总结了O'Reilly中超过50000个项目的信息之后,在查看了一百万个的搜索查询以及O'Reilly Answers中的结果之后,对于2022年我们将有哪些期望呢?

在这其中,许多事件引起了人们的注意:GPT-3 利用深度学习产生类似人类编写的文本,网络犯罪分子在发起软件攻击后索要数百万美元等等。许多技术事件得到了广泛报道,尽管还没有出现在数据统计中,例如机器人流程自动化(RPA)、数字孪生、边缘计算和5G等。这些技术可能会具有重要意义,这取决于未来会把我们带到哪里。

【参考资料】

https://www.oreilly.com/radar/technology-trends-for-2022/

作者 | 郭露 责编 | 张红月
出品 | CSDN(ID:CSDNnews)

收起阅读 »

sleep()为什么要 try catch

前言 当我们在 Java 中使用 sleep() 让线程休眠的时候,总是需要使用 try catch 去包含它: try { sleep(1000); } catch (InterruptedExcept...
继续阅读 »

前言


当我们在 Java 中使用 sleep() 让线程休眠的时候,总是需要使用 try catch 去包含它:


        try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

但是,我们却很少在 catch 中执行其它代码,仿佛这个 try catch 是理所当然一样,但其实,存在即合理,不可能无缘无故地多出一个 try catch。


在给出原因之前,我先要讲讲另外一个知识点,那就是如何去停止线程。


如何停止线程


stop()


最直接了断的方法就是调用 stop(),它能直接结束线程的执行,就如:


        // 开启线程循环输出当前的时间
Thread thread = new Thread(){
@Override
public void run() {
while (true){
System.out.println(System.currentTimeMillis());
}
}
};
thread.start();
// 睡眠两秒后停止线程
try {
sleep(2000);
thread.stop();
} catch (InterruptedException e) {
e.printStackTrace();
}

输出结果:


···
1643730391073
1643730391073
1643730391073
1643730391073
1643730391073

Process finished with exit code 0

很明显是能够把线程暂停掉的。但是,该方法现在被标记遗弃状态。



大概意思就是:


这个方法原本是设计用来停止线程并抛出线程死亡的异常,但是实际上它是不安全的,因为它是直接终止线程解放锁,这很难正确地抛出线程死亡的异常进行处理,所以,更好的方式是设置一个判断条件,然后在线程执行的过程中去判断该条件以去决定是否要进行停止线程,这样就能进行处理并有序地退出这个线程。假如一个线程等待很久的话,那才会直接中断线程并抛出异常。


按照上面所说,我们可以设置一个变量来进行控制,当然,我们可以声明一个 bool 类型进行判断,但是更好的方式是使用 interrupt()。


interrupt()


源码:


    public void interrupt() {
if (this != Thread.currentThread())
checkAccess();

synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0();
}

Just to set the interrupt flag 这里很明显就是说明只是进行了一个中断标记而已,并不会直接中断线程,所以,需要使用 interrupt() 的时候,我们还要在线程中进行 interrupt 的状态判断:



// 开启线程循环输出当前的时间
// 每次循环前都去判断该线程是否被中断了
Thread thread = new Thread(){
@Override
public void run() {
while (true){
if(isInterrupted()){
return;
}
System.out.println(System.currentTimeMillis());
}
}
};
thread.start();
// 睡眠两秒后标记中断线程
try {
sleep(2000);
thread.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}

输出结果:


···
1643731839919
1643731839919
1643731839919

Process finished with exit code 0

这样也能正常的中断线程。



好了,如何中断线程到这里讲完了,大家拜拜~~


喂!等下,这文章是不是还有东西没讲 (#`O′),sleep()为什么要 try catch 还没说。



好像是这样。


中断等待


我们再看看 stop() 被标记为遗弃的说明:


 If the target thread waits for long periods (on a condition variable, for example),
the interrupt method should be used to interrupt the wait.

也就是说,当线程在等待过久的时候,interrupt() 应该去中断这个等待。


所以,原因就找到了,要加 try catch,是因为当线程在 sleep() 的时候,调用线程的 interrupt() 方法,就会直接中断线程的 sleep 状态,抛出 InterruptedException。


因为调用 interrupt() 的时候,其实就是想尽快地结束线程,所以,继续的 sleep 是没有意义的,应该尽快结束。


        // 开启线程
// 线程睡眠 10 秒
Thread thread = new Thread(){
@Override
public void run() {
try {
sleep(10000);
} catch (InterruptedException e) {
System.out.println("sleep 状态被中断了!");
e.printStackTrace();
}
}
};
thread.start();
// 睡眠两秒后标记中断线程
try {
sleep(2000);
thread.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}

输出结果:


sleep 状态被中断了!
java.lang.InterruptedException: sleep interrupted
at java.base/java.lang.Thread.sleep(Native Method)
at com.magic.vstyle.TestMain$1.run(TestMain.java:16)

Process finished with exit code 0

这时,我们就可以 catch 到这个异常后进行额外操作,例如回收资源等。这时,停止线程就是一种可控的行为了。


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

【解惑】App处于前台,Activity就不会被回收了?

昨天在康 KunMinX 大佬的:《重学安卓:Activity 生命周期的 3 个辟谣》,在加餐处看到这段:转换后的理解:单进程场景,Activity被回收只可能是因为进程被系统回收了。感觉不太对?因为在很久以前,遇到过这样一个场景:App...
继续阅读 »

昨天在康 KunMinX 大佬的:《重学安卓:Activity 生命周期的 3 个辟谣》,在加餐处看到这段:

转换后的理解:单进程场景,Activity被回收只可能是因为进程被系统回收了

感觉不太对?因为在很久以前,遇到过这样一个场景:

App打开多个Activity,然后手机晾一边,过一段时间后(屏幕常亮),点击回退,之前的Activity空白,然后重新加载了。

App在前台,不在栈顶的Activity却被干掉,但进程还健在,如果真是这样,就和上面的理解有些出入了。

立马写个代码验证下,大概流程如下:

写个父类Activity,生命周期回调加日志打印,接着打开一个Activity,包含一个按钮,点击后依次打开多个Activity,最后一个加个按钮,点一下就申请一个大一点的ByteArray来模拟内存分配,看内存不足时是否会回收Activity。

测试结果如下:

App宁愿OOM,也不愿意回收Activity,鬼使神差地加上 android:largeHeap="true" ,结果一样。

em...难道是我记错了???

等等!!!我好像混淆了两个东西:系统可用内存不足 和 应用可用内存不足

0x1、系统可用内存不足

LMK机制

Android系统中,进程的生命周期由系统控制,处于体验和性能考虑,在APP中点击Home键或Back回退操作,并不会真的杀掉APP,进程依旧存在于内存中,这样下次启动此APP时就能更加快速。随着系统运行时间增长,打开APP越来越多,内存中的进程随着增多,系统的可用内存会越来越少。咋办,总不能让用户自己去杀进程吧,所以系统内置一套 回收机制,当系统可用内存达到一个 阈值,系统会根据 进程优先级 来杀掉一部分进程,释放内存供后续启动APP使用。

Android的这套回收机制,是基于Linux内核的OOM规则改进而来的,叫 Low Memory Killer,简称 LMK

阈值 & 杀谁

通过下述两个文件配合完成,不同手机数值可能不同,以我的老爷机 魅蓝E2 为例 (Android 11的Mix2S一直说没权限打开此文件):

# /sys/module/lowmemorykiller/parameters/minfree
# 单位:Page页,1Page = 4KB
18432,23040,27648,46080,66560,97280

# /sys/module/lowmemorykiller/parameters/adj
0,58,117,176,529,1000

Android系统会为每个进程维护一个 adj(优先级)

  • Android 6及以前称为:oom_adj,值范围:[-17,16],LMK要换算*1000/17
  • Android 7后称为:oom_score_adj,值范围:[-1000,1000]

然后,上面两个文件的值,其实是以一一对应的,比如:

66560  * 4 / 1024 = 260MB → 当系统可用内存减少到260MB时,会杀掉adj值大于529的进程;
18432 * 4 / 1024 = 72MB → 当系统可用内存减少到72MB,杀掉ajd值大于0的进程;

adj怎么看

直接通过命令行查看:

可以看到,adj是动态变化的,当App状态及四大组件生命周期发生改变时,都会改变它的值。常见ADJ级别如下:

  • NATIVE_ADJ → -1000,init进程fork出来的native进程,不受system管控
  • SYSTEM_ADJ → -900,system_server进程
  • PERSISTENT_PROC_ADJ → -800,系统persistent进程,一般不会被杀,杀了或者Carsh系统也会重新拉起
  • PERSISTENT_SERVICE_ADJ → -700,关联着系统或persistent进程
  • FOREGROUND_APP_ADJ → 0,前台进程
  • VISIBLE_APP_ADJ → 100,可见进程
  • PERCEPTIBLE_APP_ADJ → 200,可感知进程,比如后台音乐播放
  • BACKUP_APP_ADJ → 300,执行bindBackupAgent()过程的备份进程
  • HEAVY_WEIGHT_APP_ADJ → 400,重量级进程,system/rootdir/init.rc文件中设置
  • SERVICE_ADJ → 500,服务进程
  • HOME_APP_ADJ → 600,Home进程,类型为ACTIVITY_TYPE_HOME的应用,如Launcher
  • PREVIOUS_APP_ADJ → 700,用户上一个使用的App进程
  • SERVICE_B_ADJ → 800,B List中的Service
  • CACHED_APP_MIN_ADJ → 900,不可见进程 的adj最小值
  • CACHED_APP_MAX_ADJ → 906,不可见进程的adj最大值
  • UNKNOWN_ADJ → 1001,一般指将要会缓存进程,无法获取确定值

关于ADJ计算的详细算法分析可见Gityuan大佬的:《解读Android进程优先级ADJ算法》,干货多多,顺带从总结处捞一波进程保活伎俩:

  • UI进程与Service进程分离,包含Activity的Service进程,一进后台ADJ>=900,随时可能被系统回收,分离的话ADJ=500,被杀的可能性降低,尤其是系统允许自启动的服务进程,必须做UI分离,避免消耗较大内存;
  • 真正需要用户可感知的应用,调用startForegroundService()启用前台服务,ADJ=200;
  • 进程中的Service工作完,务必主动调用stopService或stopSelf来停止服务,避免占用内存,浪费系统资源;
  • 不要长时间绑定其他进程的service或者provider,每次使用完成后应立刻释放,避免其他进程常驻于内存;
  • APP应该实现接口onTrimMemory()和onLowMemory(),根据TrimLevel适当地将非必须内存在回调方法中加以释放,当系统内存紧张时会回调该接口,减少系统卡顿与杀进程频次;
  • 更应在优化内存上下功夫,相同ADJ级别,系统会优先杀内存占用的进程;

:能否把自己的App的ADJ值设置为-1000,让其杀不死? :不可以,要有root权限才能修改adj,而且改了重启手机还是恢复的。

扯得有点远了,回到问题上:

系统内存不足时,会在内核层直接查杀进程,不会在Framework层还跟你叨逼叨看回收哪个Activity。

所以在系统这个层面,单进程场景,Activity被回收只可能是因为进程被系统回收了,这句话是没毛病的,但在应用层面就不一定了。


0x2、应用可用内存不足

APP进程(虚拟机)的内存分配实际上是对 堆的分配和释放,为了整个系统的内存控制需要,会为每个应用程序设置一个 堆的限制阈值,如果应用使用内存接近阈值还尝试分配内存,就很容易引起OOM。

当然,不会那么蠢,还要开发仔自己在APP里回收内存,虚拟机自带 GC,这里就不向去卷具体的回收算法了

假设应用内存不足真的会回收Activity,那该怎么设计?一种解法如下:

应用启动时,开一个子线程,定时轮询当前可用内存是否超过阈值,超过的话干掉Activity

那就来跟下Android是不是也是这样设计的?

Activity回收机制

跟下应用启动入口:ActivityThread → main()

跟下 attach()

这里就非常像,run()中计算:已用内存 > 3/4最大内存,就执行 releaseSomeActivities(),跟下:

所以 getService() 是获取了 IActivityTaskManager.aidl接口,具体的实现类是 ActivityTaskManangerService

继续往下跟: RootActivityContainer → releaseSomeActivitiesLocked()

跟下:WindowProcessController → getReleaseSomeActivitiesTasks()

然后再往下走就是释放Activity的代码了:ActivityStack → releaseSomeActivitiesLocked()

具体咋释放,就不往下跟了哈,接着跟下是怎么监控的~

内存监控机制

跟回:BinderInternal.addGcWatcher()

这里可能看得你有点迷,但是当你理解了就会觉得很妙了:

虚拟机GC会干掉 WeakReference 的对象,在释放内存前,会调用对象的 finalize(),而这里有创建了一个新的 WeakReference 实例。下次GC,又会走一遍这里的代码,啧啧啧,相比起轮询高效多了

到此,应用内存不足回收Activity的流程就大概缕清了,接着可以写个代码验证下是否真的这样。

Demo验证

先试下两个Task的:

模拟内存分配的页面,然后一直点~

宁愿OOM,也不回收,试试三个~

好家伙,onDestory()了,此时按Back回退这些页面,发现走了onCreate(),即回收了,接着试试四个的情况:

可以,每次只回收一个Task,到此验证完毕了~

0x3、结论

  • 系统内存不足时,直接在内核层查杀(回收)进程,并不会考虑回收哪个Activity;
  • 进程内存不足时,如果此进程 Activity Task数 >= 3 且 使用内存超过3/4,会对 不可见 Task进行回收,每次回收 1个 Task,回收时机为每次gc;

参考文献


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

收起阅读 »

Androd Gradle 使用技巧之模块依赖替换

背景 我们在多模块项目开发过程中,会遇到这样的场景,工程里依赖了一个自己的或者其他同事的 aar 模块,有时候为了开发调试方便,经常会把 aar 改为本地源码依赖,开发完毕并提交的时候,会再修改回 aar 依赖,这样就会很不方便,开发流程图示如下: 解决 一...
继续阅读 »

背景


我们在多模块项目开发过程中,会遇到这样的场景,工程里依赖了一个自己的或者其他同事的 aar 模块,有时候为了开发调试方便,经常会把 aar 改为本地源码依赖,开发完毕并提交的时候,会再修改回 aar 依赖,这样就会很不方便,开发流程图示如下:


截屏2022-02-09 下午4.56.16.png


解决


一开始我们通过在 appbuild.gradle 里的 dependency 判断如果是需要本地依赖的 aar,就替换为 implementation project 依赖,伪代码如下:


dependencies {
if(enableLocalModule) {
implementation 'custom:test:0.0.1'
} else {
implementation project(path: ':test')
}
}

这样就可以不用每次提交代码还要修改回 aar 依赖,但是如果其他模块如果也依赖了该 aar 模块,就会出现问题,虽然可以继续修改其他模块里的依赖方式,但是这样就会有侵入性,而且不能彻底解决问题,仍然有可能出现本地依赖和 aar 依赖的代码不一致问题。


Gradle 官方针对这种场景提供了更好的解决方式 DependencySubstitution,使用方式如下:


步骤1:在 settting.gradle,添加如下代码:


// 加载本地 module
if (file("local.properties").exists()) {
def properties = new Properties()
def inputStream = file("local.properties").newDataInputStream()
properties.load( inputStream )
def moduleName = properties.getProperty("moduleName")
def modulePath = properties.getProperty("modulePath")
if (moduleName != null && modulePath != null) {
include moduleName
project(moduleName).projectDir = file(modulePath)
}
}

步骤2:在 appbuild.gradle 添加以下代码


configurations.all {
resolutionStrategy.dependencySubstitution.all { DependencySubstitution dependency ->
// use local module
if (dependency.requested instanceof ModuleComponentSelector && dependency.requested.group == "custom") {
def targetProject = findProject(":test")
if (targetProject != null) {
dependency.useTarget targetProject
}
}
}
}

步骤3::在 local.properties


moduleName=:test
modulePath=../AndroidStudioProjects/TestProject/testModule

到这里就大功告成了,后续只需要在 local.properties 里开启和关闭,即可实现 aar 模块本地依赖调试,提交代码也不用去手动修改回 aar 依赖。


参考



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

Android 使用 Retrofit 发送网络请求

简介 在Android应用中,如果不是单机的话,应该都有请求后端接口API的情况,本篇文章就介绍下Retrofit在Android中如何进行使用的 相关代码 我们以一个简单的登录接口为例 完整代码GitHub上有:github.com/lw124392545…...
继续阅读 »

简介


在Android应用中,如果不是单机的话,应该都有请求后端接口API的情况,本篇文章就介绍下Retrofit在Android中如何进行使用的


相关代码


我们以一个简单的登录接口为例


完整代码GitHub上有:github.com/lw124392545…


仅做代码参考,目前数据监控上传是有了,但界面这些还很粗糙,没有完善


相关的依赖引入


首先我们在工程中引入相关的依赖:


    implementation 'com.squareup.okhttp3:okhttp:4.5.0'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'

相关的手机权限开启


需要在文件中开启网络权限:


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.selfgrowth">

<uses-permission android:name="android.permission.INTERNET"/>

<application
......
</application>

</manifest>

配置Retrofit Client


Client的相关配置:单例,配置基于OKHTTP,Gson序列化;OKHTTP中添加了请求拦截器


@Data
public class RetrofitClient {

private static final RetrofitClient instance = new RetrofitClient();

private final Retrofit retrofit = new Retrofit.Builder()
.baseUrl(HttpConfig.ADDRESS) //基础url,其他部分在GetRequestInterface里
.client(httpClient())
.addConverterFactory(GsonConverterFactory.create()) //Gson数据转换器
.build();

public static RetrofitClient getInstance() {
return instance;
}

private OkHttpClient httpClient() {
return new OkHttpClient.Builder()
.addInterceptor(new AccessTokenInterceptor())
.connectTimeout(20, TimeUnit.SECONDS)
.build();
}
}

配置通用的请求拦截器


比如在请求中,带上Authorization等


public class AccessTokenInterceptor implements Interceptor {

@NonNull
@Override
public Response intercept(@NonNull Chain chain) throws IOException {
if (UserCache.getInstance().getToken() == null) {
return chain.proceed(chain.request());
}

Request original = chain.request();
Request.Builder requestBuilder = original.newBuilder()
.addHeader("Authorization", UserCache.getInstance().getToken());
Request request = requestBuilder.build();
return chain.proceed(request);
}
}

Retrofit接口定义


登录请求的接口定义:


public interface UserApi {

/**
* 用户登录
**/
@POST("auth/user/login")
Call<ApiResponse> login(@Body LoginUser user);
}

Retrofit Request具体请求编写


我们首先定义一个抽象类,在其中持有我们的RetrofitClient全局类,在其中发起请求,由于Android UI的形式,请求是异步的


public abstract class Request {

final Retrofit retrofit;

public Request() {
this.retrofit = RetrofitClient.getInstance().getRetrofit();
}

/**
* 发送网络请求(异步)
* @param call call
*/
void sendRequest(Call<ApiResponse> call, Consumer<? super Object> success, Consumer<? super Object> failed) {
call.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(Call<ApiResponse> call, Response<ApiResponse> response) {
if (response.code() != 200) {
Log.w("Http Response", "请求响应错误");
failed.accept(response.raw().message());
return;
}
if (response.body() == null || response.body().getData() == null) {
success.accept(null);
return;
}
Object res = response.body().getData();
if (String.valueOf(res).isEmpty()) {
success.accept(null);
return;
}
success.accept(res);
}

@Override
public void onFailure(Call<ApiResponse> call, Throwable t) {
System.out.println("GetOutWarehouseList->onFailure(MainActivity.java): "+t.toString() );
}
});
}
}

如上所示,请求成功就执行success相关的逻辑,失败则执行failed相关的逻辑


登录请求的具体逻辑如下:构造Retrofit Interface,发起请求


public class UserRequest extends Request {

public void login(LoginUser user, Consumer<? super Object> success, Consumer<? super Object> failed) {
UserApi request = retrofit.create(UserApi.class);
Call<ApiResponse> call = request.login(user);
sendRequest(call, success, failed);
}
}

Android UI中进行调动


使用示例如下,点击一个登录按钮后触发


public class LoginFragment extends Fragment {

private final UserRequest userRequest = new UserRequest();

@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_login, container, false);
Button loginButton = rootView.findViewById(R.id.login_button);
loginButton.setOnClickListener(view -> {
EditText email = rootView.findViewById(R.id.login_email_edit);
EditText password = rootView.findViewById(R.id.login_password_edit);

final LoginUser user = LoginUser.builder()
.email(email.getText().toString())
.password(password.getText().toString())
.build();

// 获取相关的用户名和密码后,调用登录接口
userRequest.login(user, (token) -> {
UserCache.getInstance().initUser(email.getText().toString(), token.toString());
final SharedPreferences preferences = requireContext().getSharedPreferences("userInfo", Context.MODE_PRIVATE);
final SharedPreferences.Editor edit = preferences.edit();
edit.putString("username", email.getText().toString());
edit.putString("password", password.getText().toString());
edit.apply();
Snackbar.make(view, "登录成功:" + token.toString(), Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
}, failedMessage -> {
Snackbar.make(view, "登录失败:" + failedMessage, Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
});
});
return rootView;
}

@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
final SharedPreferences preferences = getActivity().getSharedPreferences("userInfo", Activity.MODE_PRIVATE);
final String userName = preferences.getString("username", "");
final String password = preferences.getString("password", "");
final EditText emailEdit = getView().findViewById(R.id.login_email_edit);
final EditText passwordEdit = getView().findViewById(R.id.login_password_edit);
emailEdit.setText(userName);
passwordEdit.setText(password);
}
}

总结


本篇文章中介绍了如Android学习中如何使用Retrofit发起网络请求


但由于吃初学,虽然感觉能用,但有点繁琐,不知道在实际的Android开发中,网络请求的最近实践是怎么样的,如果有的话,大佬可以在评论区告知下,感谢


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

Flutter之GetX依赖注入使用详解

put 为了验证依赖注入的功能,首先创建两个测试页面:PageA 和 PageB ,PageA 添加两个按钮 toB 和 find ,分别为跳转 PageB 和获取依赖;在 PageB 中通过 put 方法注入依赖对象,然后调用按钮触发 find 获取依赖。关...
继续阅读 »

put


为了验证依赖注入的功能,首先创建两个测试页面:PageA 和 PageB ,PageA 添加两个按钮 toBfind ,分别为跳转 PageB 和获取依赖;在 PageB 中通过 put 方法注入依赖对象,然后调用按钮触发 find 获取依赖。关键源码如下:


PageA


TextButton(
child: const Text("toB"),
onPressed: (){
/// Navigator.push(context, MaterialPageRoute(builder: (context) => const PageB()));
/// Get.to(const PageB());
},
),

TextButton(
child: const Text("find"),
onPressed: () async {
User user = Get.find();
print("page a username : ${user.name} id: ${user.id}");
})


PageB:


Get.put(User.create("张三", DateTime.now().millisecondsSinceEpoch)));

User user = Get.find();

TextButton(
child: const Text("find"),
onPressed: (){
User user = Get.find();
print("${DateTime.now()} page b username : ${user.name} id: ${user.id}");
})

其中 User 为自定义对象,用于测试注入,源码如下:


User:


class User{
final String? name;
final int? id;

factory User.create(String name, int id){
print("${DateTime.now()} create User");
return User(name, id);
}
}

Navigator 路由跳转


首先使用 Flutter 自带的路由管理从 PageA 跳转 PageB, 然后返回 PageA 再点击 find 按钮获取 User 依赖:


Navigator.push(context, MaterialPageRoute(builder: (context) => const PageB()));

流程:PageA -> PageB -> put -> find -> PageA -> find


输出结果:


/// put
I/flutter (31878): 2022-01-27 19:18:20.851800 create User
[GETX] Instance "User" has been created

/// page b find
I/flutter (31878): 2022-01-27 19:18:22.170133 page b username : 张三 id: 1643282300139

/// page a find
I/flutter (31878): 2022-01-27 19:18:25.554667 page a username : 张三 id: 1643282300139

通过输出结果发现,在 PageB 注入的依赖 User,在返回 PageA 后通过 find 依然能获取,并且是同一个对象。通过 Flutter 通过源码一步一步剖析 Getx 依赖管理的实现 这篇文章知道,在页面销毁的时候会回收依赖,但是这里为什么返回 PageA 后还能获取到依赖对象呢?是因为在页面销毁时回收有个前提是使用 GetX 的路由管理页面,使用官方的 Navigator 进行路由跳转时页面销毁不会触发回收依赖。


GetX 路由跳转


接下来换成使用 GetX 进行路由跳转进行同样的操作,再看看输出结果:


Get.to(const PageB());

流程:PageA -> PageB -> put -> find -> PageA -> find


输出结果:


[GETX] GOING TO ROUTE /PageB

/// put
I/flutter (31878): 2022-01-27 19:16:32.014530 create User
[GETX] Instance "User" has been created

/// page b find
I/flutter (31878): 2022-01-27 19:16:34.043144 page b username : 张三 id: 1643282192014
[GETX] CLOSE TO ROUTE /PageB
[GETX] "User" deleted from memory

/// page a find error
E/flutter (31878): [ERROR:flutter/lib/ui/ui_dart_state.cc(209)] Unhandled Exception: "User" not found. You need to call "Get.put(User())" or "Get.lazyPut(()=>User())"

发现在 PageB 中获取是正常,关闭 PageB 时输出了一句 "User" deleted from memory 即在 PageB 注入的 User 被删除了,此时在 PageA 再通过 find 获取 User 就报错了,提示需要先调用 put 或者 lazyPut 先注入依赖对象。这就验证了使用 GetX 路由跳转时,使用 put 默认注入依赖时,当页面销毁依赖也会被回收。


permanent


put 还有一个 permanent 参数,在 Flutter应用框架搭建(一)GetX集成及使用详解 这篇文章里介绍过,permanent 的作用是永久保留,默认为 false,接下来在 put 时设置 permanent 为 true,并同样使用 GetX 的路由跳转重复上面的流程。


关键代码:


Get.put(User.create("张三", DateTime.now().millisecondsSinceEpoch), permanent: true);

流程:PageA -> PageB -> put -> find -> PageA -> find


输出结果:


[GETX] GOING TO ROUTE /PageB
/// put
I/flutter (31878): 2022-01-27 19:15:16.110403 create User
[GETX] Instance "User" has been created

/// page b find
I/flutter (31878): 2022-01-27 19:15:18.667360 page b username : 张三 id: 1643282116109
[GETX] CLOSE TO ROUTE /PageB
[GETX] "User" has been marked as permanent, SmartManagement is not authorized to delete it.

/// page a find success
I/flutter (31878): page a username : 张三 id: 1643282116109

设置 permanent 为 true 后,返回 PageA 同样能获取到依赖对象,说明依赖并没有因为页面销毁而回收,GetX 的日志输出也说明了 User 被标记为 permanent 而不会被删除:"User" has been marked as permanent, SmartManagement is not authorized to delete it.


lazyPut


lazyPut 为延迟初始化依赖对象 :


Get.lazyPut(() => User.create("张三", DateTime.now().millisecondsSinceEpoch));

TextButton(
child: const Text("find"),
onPressed: () async {
User user = Get.find();
print("${DateTime.now()} page b username : ${user.name} id: ${user.id}");
})

流程:PageA -> PageB -> put -> find -> find -> PageA -> find, 从 PageA 跳转 PageB,先通过 lazyPut 注入依赖,然后点击 find 获取依赖,过 3 秒再点击一次,然后返回 PageA 点击 find 获取一次。


输出结果:


[GETX] GOING TO ROUTE /PageB
/// lazyPut

/// page b find 1
I/flutter (31878): 2022-01-27 17:38:49.590295 create User
[GETX] Instance "User" has been created
I/flutter (31878): 2022-01-27 17:38:49.603063 page b username : 张三 id: 1643276329589

/// page b find 2
I/flutter (31878): 2022-01-27 17:38:52.297049 page b username : 张三 id: 1643276329589
[GETX] CLOSE TO ROUTE /PageB
[GETX] "User" deleted from memory

/// page a find error
E/flutter (31878): [ERROR:flutter/lib/ui/ui_dart_state.cc(209)] Unhandled Exception: "User" not found. You need to call "Get.put(User())" or "Get.lazyPut(()=>User())"

通过日志发现 User 对象是在第一次调用 find 时进行初始化话的,第二次 find 时不会再次初始化 User;同样的 PageB 销毁时依赖也会被回收,导致在 PageA 中获取会报错。


fenix


lazyPut 还有一个 fenix 参数默认为 false,作用是当销毁时,会将依赖移除,但是下次 find 时又会重新创建依赖对象。


lazyPut 添加 fenix 参数 :


 Get.lazyPut(() => User.create("张三", DateTime.now().millisecondsSinceEpoch), fenix: true);

流程:PageA -> PageB -> put -> find -> find -> PageA -> find,与上面流程一致。


输出结果:


[GETX] GOING TO ROUTE /PageB
/// lazyPut

/// page b find 1
[GETX] Instance "User" has been created
I/flutter (31878): 2022-01-27 17:58:58.321564 create User
I/flutter (31878): 2022-01-27 17:58:58.333369 page b username : 张三 id: 1643277538321

/// page b find 2
I/flutter (31878): 2022-01-27 17:59:01.647629 page b username : 张三 id: 1643277538321
[GETX] CLOSE TO ROUTE /PageB

/// page a find success
I/flutter (31878): 2022-01-27 17:59:07.666929 create User
[GETX] Instance "User" has been created
I/flutter (31878): page a username : 张三 id: 1643277547666

通过输出日志分析,在 PageB 中的表现与不加 fenix 表现一致,但是返回 PageA 后获取依赖并没有报错,而是重新创建了依赖对象。这就是 fenix 的作用。


putAsync


putAsyncput 基本一致,不同的是传入依赖可以异步初始化。测试代码修改如下:


print("${DateTime.now()} : page b putAsync User");
Get.putAsync(() async {
await Future.delayed(const Duration(seconds: 3));
return User.create("张三", DateTime.now().millisecondsSinceEpoch);
});

使用 Future.delayed 模拟耗时操作。


流程:PageA -> PageB -> put -> find -> PageA -> find


输出结果:


[GETX] GOING TO ROUTE /PageB

/// putAsync
I/flutter (31878): 2022-01-27 18:48:34.280337 : page b putAsync User

/// create user
I/flutter (31878): 2022-01-27 18:48:37.306073 create User
[GETX] Instance "User" has been created

/// page b find
I/flutter (31878): 2022-01-27 18:48:40.264854 page b username : 张三 id: 1643280517305
[GETX] CLOSE TO ROUTE /PageB
[GETX] "User" deleted from memory

/// page a find error
E/flutter (31878): [ERROR:flutter/lib/ui/ui_dart_state.cc(209)] Unhandled Exception: "User" not found. You need to call "Get.put(User())" or "Get.lazyPut(()=>User())"

通过日志发现,put 后确实是过了 3s 才创建 User。


create


createpermanent 参数默认为 true,即永久保留,但是通过 Flutter应用框架搭建(一)GetX集成及使用详解 这篇源码分析知道,create 内部调用时 isSingleton 设置为 false,即每次 find 时都会重新创建依赖对象。


Get.create(() => User.create("张三", DateTime.now().millisecondsSinceEpoch));

流程:PageA -> PageB -> put -> find -> find -> PageA -> find


输出结果:


[GETX] GOING TO ROUTE /PageB
/// create

/// page b find 1
I/flutter (31878): 2022-01-27 18:56:10.520961 create User
I/flutter (31878): 2022-01-27 18:56:10.532465 page b username : 张三 id: 1643280970520

/// page b find 2
I/flutter (31878): 2022-01-27 18:56:18.933750 create User
I/flutter (31878): 2022-01-27 18:56:18.934188 page b username : 张三 id: 1643280978933

[GETX] CLOSE TO ROUTE /PageB

/// page a find success
I/flutter (31878): 2022-01-27 18:56:25.319224 create User
I/flutter (31878): page a username : 张三 id: 1643280985319

通过日志发现,确实是每次 find 时都会重新创建 User 对象,并且退出 PageB 后还能通过 find 获取依赖对象。


总结


通过代码调用不同的注入方法,设置不同的参数,分析输出日志,详细的介绍了 putlazyPutputAsynccreate 以及 permanentfenix 参数的具体作用,开发中可根据实际业务场景灵活使用不同注入方式。关于注入的 tag 参数将在后续文章中详细介绍。


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

晒一波程序员的工位,你中意哪一款?

程序员的圈子啊那是十分神秘,又令人着迷的。每天的工作就是对着电脑,那他们的工作是如何的呢?我们来品一品(PS:后面奉上各位大佬的桌面,别走开哦)↓↓↓最最常见的普通版:升级版:算不得体贴版:逼退人升级版:舒适版:超人性版:独立版:高级版:友谊版:高级程序员版:...
继续阅读 »

程序员的圈子啊

那是十分神秘,

又令人着迷的。

每天的工作就是对着电脑,

那他们的工作是如何的呢?

我们来品一品

(PS:后面奉上各位大佬的桌面,别走开哦)


↓↓↓


最最常见的普通版:



升级版:





算不得体贴版:



逼退人升级版:

图片

舒适版:


超人性版:

图片

独立版:


高级版:


友谊版:


高级程序员版:


干净的其他普通版:





一位程序员的暑期办公室:


看完以上这些,

终于到我们大佬的工位啦!


↓↓↓


扎克·伯格:


史蒂夫·乔布斯:


比尔·盖茨:


史蒂夫·鲍尔默:


杰夫·贝佐斯:


马克斯·莱文奇恩:


迈克尔·戴尔:


最后,

大家如果有补充,

欢迎留言呀!

来源:公众号 Python开发 

收起阅读 »

女生最后悔读的专业,在工科

“工科是辍学也能就业的学科”,曾有人这样调侃。然而,对工科女生来说,事情似乎并不是这么简单。泡实验室、写代码、修电路、搭模型……明明上了一样的大学,但在找工作时,工科女生们却只等来一句句“这个岗位不适合女生”“女孩怎么学这个专业啊”。都说“学好数理化,走遍天下...
继续阅读 »

“工科是辍学也能就业的学科”,曾有人这样调侃。
然而,对工科女生来说,事情似乎并不是这么简单。泡实验室、写代码、修电路、搭模型……明明上了一样的大学,但在找工作时,工科女生们却只等来一句句“这个岗位不适合女生”“女孩怎么学这个专业啊”。
都说“学好数理化,走遍天下都不怕”,对女生就恐怕不是这样了。好不容易跨过千军万马的独木桥考上了大学,谁知道这只是第一步。

01 进了大学才发现,学工科好难

高中时代,很多人面临过选择文理科的纠结,家长都会劝学生,最好还是选理科。毕竟选理科更容易考上大学,报志愿选择多,也更容易找工作。
事实上,在中国的千万高考大军中,大多数都是理科生。例如,在高考大省河南,2021 年理科类考生要比文科类考生多了 15 万 [1]。
从本科上线率来看,理科确实要高于文科。 2021 年,各省份理科的本科上线率普遍比文科高 20% - 40% 左右, 安徽、甘肃等省甚至相差 40% 以上。
从理科生与文科生本科上线率来看,只要还有文理分科的地区,这一比值都大于 1,也就是说,理科生更容易上本科,这不是存在于少数省市的个别现象,而是广泛存在于中国大江南北。
pic_4fc2b6ae.png
除了上线率更高,理科生的高考志愿专业选择也更为宽泛。其中, 他们中的很大部分,都去了工科门类。 根据教育部统计数据,工学门类的在校生数要远大于其他学科门类。
2020 年,全国有 1825.74 万本科在校生, 53.70% 都是女生。考虑到基数,就读工科专业的女生并不在少数。
虽然在传统印象中,工科是“男性学科”, 但在现在的理工类院校中,可以看到越来越多的女生身影。
例如,在浙江理工大学,男女新生比例已经基本均衡,在男生人数最多的机械与自动控制学院,男女比例也从 2019 年的 8.9 : 1 下降到了 2021 年 6.79 : 1 [2][3]。
只不过,考上大学或许只是她们人生闯关路的开端。填志愿时的一腔热血,很快就被现实浇上了冷水。
我们统计分析了知乎和豆瓣上有关“女生学工科是种什么体验”的帖子后发现, “跨考”一词的热度最高,相关内容多集中在生化环材、建工交通等传统工科上。
pic_c6b97c2d.png
要不要转专业?要不要考公?要不要转人文社科?一些女生在工科类专业没读多久,就已经开始盘算要脱离工科的苦海了。
后悔,也是她们吐槽的高频词。 后悔的背后,是报考时未曾想到的女生学工科的难处。
实验里动辄要待上一整天,建筑工地上晒大太阳和通宵赶报告是家常便饭,必须承认,工科的很多专业对女生来说是很大的挑战——工科太需要体力了。
还在大学,工科女生就已经感受到了歧视,参加课题、实验或是竞赛会被认为是受男同学或者师兄关照,毕竟“女生就是学不来工科”:

工科女生不是时时刻刻受到周围男生的关照吗(手动狗头)

工科不适合女生并不是说女生学习不行或者动手能力不行,而是就业屡遭歧视,考研究生时导师更偏向招男生。

但实际上,一项发表在《科学》期刊上的研究表明,女性在科学、技术、工程和数学(STEM)学科的能力一直以来都被低估。
在对 30 余个理工科开展大规模调查后发现,阻碍女性进入相关行业的主要原因, 是对她们缺乏这些领域天赋的刻板印象,而非能力差异 [4]。

02 敲得开大学门,却叩不开职场门

学习能力并不比男生差,但在找工作时,她们却遇到了不少“有色眼镜”。
有人吐槽,虽然自己毕业于北京一所老牌 985,成绩也不错,研究所来招人时,带走的却都是成绩不如她的男生的简历。
在投出简历时,男女就已处于不同的赛道,这种现象普遍存在。
根据中国科学院心理研究所的一项研究, 拥有男性化定向名字的女性求职者获得的面试机会较多 [5]。 也就是说,即使简历的性别栏是“女”,只要名字听起来像男生,你也会更容易获得橄榄枝 。
只不过,工科类行业是女生求职被区别对待的重灾区。
报告显示, 在采掘、冶炼、石油石化等工作体力强度较高的行业,男性求职者被招聘者主动沟通的次数是女性的 2 倍以上。
在计算机、交通运输等需要经常加班、出差的行业中,男性被沟通的次数也明显更高。
pic_08502e08.png
与之相反,女性通常需要主动联系招聘者,才有可能获得面试机会。在工程、制造等工科行业中,女性求职者与企业主动沟通次数是男性的 1.04 倍。
比找工作更难的,是工科女找工作。就算顺利进到了面试,用人单位还会明里暗里以各种理由将她们拒之门外。
我们统计了工科女生们在社交平台上分享的求职经历, 生育、体力、加班、出差是她们认为用人单位不青睐她们的主要因素。
根据智联招聘《2020 中国女性职场现状调查报告》,将近六成的女性表示自己曾在应聘过程中被问及生育情况,而男性这个比例仅为 19.59%。
生育对女性的影响,从求职就已开始。 而工科类工作,常常需要加班、出差、驻工地,“女生一毕业就结婚生孩子,拴住了。”有工科女生提及到了应聘单位这样的顾虑。
pic_1a8b415b.png
生育只是一个因素, 很多用人单位不认为女生有足够的体力能跑工地、下车间,能适应经常出差, 在招人时自然倾向于男生。

本人机械女,本科,在制造业工作过5年。工科的就业环境你们真的了解吗?虽然是做技术,到去车间是避免不了的。况且工厂觉得女生下车间不方便,不能加班,人家也不愿意招聘女生。

必须承认,工科的一些工作对体力有着较高要求,也有工科男生发表看法,认为经常在环境肮脏,施工噪音嘈杂的工地熬夜加班,就算是男生也会跑路转行。
只是,男生尚且能有跑路转行的机会,工科女连工作的“号码牌”都排不到。让不少人戏谑道“学得再好,不如你是个男的”。如果说职场是闯关游戏,那工科女们一开始便选择了 hard 模式。

03 兜兜转转,工科女归宿还是老师

就算筛过了简历、通过了面试、找到了工作,工科女们遭遇的性别歧视并没有结束。入职以后,薪酬差异大、机会不平等、晋升天花板……还有一道道难关在等着她们。
某招聘公司统计了 2020 年男女薪酬差异最高的行业, 其中医药、交通、建筑、互联网、化工、机械等理工科行业占据了大半, 大部分行业男女薪酬差异为 30% - 40%。
同工不同酬,正是很多工科女面临的困境。
分二级行业看更为直接,2019 年性别薪资差异最高的行业主要为采掘冶炼、工程施工、装修装饰等工程制造类行业。 在高薪技术岗位,也仅有 6% 左右的女性处于生产、技术总监级别 [6]。
今天,也有一些工科女凭实力在行业内得到了认可,她们动可跑工地、修电脑,静可画图纸、编程序。但挣的钱,却还是不如同行男性。
从面试到进入职场,工科女生要面临的是“九九八十一难”。这些种种都造成了工科女生从事对口行业的比例低,纷纷寻找其他出路。
麦可思的就业报告显示, 中小学教育及培训行业是工科女生们毕业后的第一选择,专业相关度仅 32%。 政府及公共管理的行政工作也名列前矛,专业相关度约 40%。
pic_649ea653.png
确实,有一些工科女读大学后才发现不喜欢自己的专业,要么考研时转专业,要么毕业后转行。
但也仍有许多工科女对专业满怀热爱,我们统计了社交平台上关于女生就读工科等相关内容, 有约 14% 提到了“喜欢”或“热爱”。
很多工科女本希望能进入喜欢的行业发光发热,但却在找工作时处处碰壁。“女生当老师好,稳定”“女孩子就该做安安静静的工作”,诸如此类的话工科女们听了太多。
深深的热爱,却换来了狠狠的伤害:

喜欢是很喜欢自己学的这个专业,大学四年各种奖学金也是拿了不少,但是吧,校招的时候,递简历一看是女的就不收,而有些男同学四级没过都可以,就很受打击。

她们听从家长和社会的规劝,高中时选了“就业前景广”的理科,毕业后,面试难、薪资低、被歧视……一道道难关又迫使她们去了和专业不相关的教育业。
造成工科女就业难的不是天赋、不是能力,而是偏见。即使当初上大学时选择的机会比文科女要多,但最后还是殊途同归,都去当了老师。

来源:网易数读

收起阅读 »

为了脱单,我在交友软件上看尽了奇葩

结语线上交友到底好不好?每个人对这个问题见仁见智,毕竟有些人用它拓宽了交友圈,有些却因为它差点失去了对人类的信心。但对于很多人来说,线上交友都是不可或缺的社交途径,而且此处“社交”不仅限于找对象。不能在软件上找到共度情人节的人,哪怕能找到共同吐槽情人节的人也不...
继续阅读 »

pic_90caa4be.png

pic_9742b690.png

pic_4597a7ce.png

pic_72f15f7c.png

pic_00af29dd.png

pic_f1b8c6fc.png

pic_526d68be.png

pic_b42c5c51.png

pic_26a55a50.png

pic_ae634815.png

pic_63fc9b85.png

pic_42d912cd.png

pic_c39f89dd.png

pic_c7e29d81.png

pic_ddb361aa.png

pic_c611bb78.png

pic_a131c0e5.png

pic_12358bce.png

pic_9153fe02.png

pic_5959a6f5.png

pic_00fb7551.png

结语
线上交友到底好不好?每个人对这个问题见仁见智,毕竟有些人用它拓宽了交友圈,有些却因为它差点失去了对人类的信心。
但对于很多人来说,线上交友都是不可或缺的社交途径,而且此处“社交”不仅限于找对象。不能在软件上找到共度情人节的人,哪怕能找到共同吐槽情人节的人也不赖。
重要的是,我们使用社交软件的时候,做最真实的自己!展示自己漂亮帅气、优雅可爱、自信有特色的外表,但不要照骗;别遮掩风趣认真、善良渊博的内在,同时也别“查户口”式聊天。等到线下面基的那天,也要好好打扮、认真地与对方交流。
“社交圈窄”终是许多人无法脱单的缘由。各位,打开思路,还有不少人在周围的朋友、朋友的朋友、朋友的朋友的同事里面翻出了对象呢。
总之,条条大路通罗马——祝大家有情人终成眷属。
以下是本次调查中,我们发现的一些关于交友的有趣现象:
1、大约三分之二的受访者用过线上交友找对象,其中最多人用的软件是微信。
2、除了约会 APP,人们还会在各种社交 APP,甚至是在音乐类 APP、游戏类 APP 里脱单。
3、至少有三分之一的受访者认为,用约会软件也不是只奔着恋爱去的。
4、约会软件上,大约三分之二的受访者会主要通过照片和档案筛选聊天对象。
5、将近七成的受访者会因为“聊天无趣”而放弃网络情谊。
6、线下见面时,男性更在乎对方是否照骗,而女性更在乎对方聊天有没有意思。
7、近一半使用过线上交友的女性遇到过图色的网友,超过三成的男性遇到过图钱的网友。
8、超七成的受访者会因网友质量太差而抛弃一款交友 APP。
9、超过一半的人是在同学、同事、朋友等熟人圈里发展出了对象。
10、不管是线上还是线下找对象,八成的受访者认为最重要的是“让人舒服的聊天”。

数据说明 本次调查收到了很多读者的反馈,感谢所有参与调查的读者朋友们,大家的留言我们都有读过。
针对本次的样本数据,我们做一些简单说明:
本次共回收有效问卷数量 6862 份,其中男生占比 32.5%,女生占比 67.5%;00 后占比 25.1%,95 后占比 42.5%,90 后占比 21.0%,85 后占比 8.0%。
样本中,目前脱单的占比 34.4%;谈过恋爱但目前单身的占比 32.9%,目前从未谈过恋爱的占比 32.7%。
另外,来自一线城市的受访者占比 45.4%,来自二线城市的占比 28.7%,来自三线城市的占比 14.3%,来自四线城市及以下的占比 11.7%。

来源:网易数读

收起阅读 »

麻辣烫都要上市了,我呆了。

近日,中国证监会的官网公布了杨国福麻辣烫的申请,如果获得受理,杨国福就有机会去香港上市,成为「麻辣烫第一股」。几乎在同一时间,绝味食品宣布旗下餐饮品牌和府捞面和中式快餐乡村基宣布将到境外上市;老乡鸡、西贝等餐饮企业,也传出了上市的消息。中国连锁餐饮耕耘多年,但...
继续阅读 »

pic_290bd818.png

近日,中国证监会的官网公布了杨国福麻辣烫的申请,如果获得受理,杨国福就有机会去香港上市,成为「麻辣烫第一股」。
几乎在同一时间,绝味食品宣布旗下餐饮品牌和府捞面和中式快餐乡村基宣布将到境外上市;老乡鸡、西贝等餐饮企业,也传出了上市的消息。
中国连锁餐饮耕耘多年,但是一直不温不火,为什么今年就爆发了呢?相信很多人都有这样的疑惑,我也去研究了大量的数据报告,探索研究了其中的原因,下面就是我看了相关报告和数据以后得出的观点。
也欢迎大家一起来讨论。
首先企业上市的目的是为了从资本市场获得更多资金来助力业务发展,但是想要保住股价,一个健康的商业模式是必须具备的。乡村基就曾因为扩张太快导致利润下跌,最后以退市收场,这次要上市的企业肯定不会忽视这一点。
而宣布上市的这三家企业有一个共同的特征,就是在近两年高速扩张的同时,依然保持了盈利。
乡村基旗下乡村基餐厅和大米先生两个品牌,由截至2019年1月1日的638家增至截至2021年9月30日的1145家,增加了507家店,增长率达到79.5%。并且在扩张的同时,乡村基餐厅的营业利润率分别为10.7%、9.2%和12.6%;大米先生餐厅的营业利润率分别为6.5%、3.3%和10.4%。
和府捞面则是连续多年保持了50%以上的营收增长,截至2021年6月底,和府捞面全国门店总数超340家。其中,2021年新增门店数较2020年翻番,全国约2天新开一家店。而且2021年上半年,和府捞面的营收为8.46亿元,并实现了1385万元的净利润,和2020全年2.15亿的亏损相比,目前单店营业额已全面恢复至疫情前水平。
杨国福2017年门店数量突破5000家,又在2020年突破6000,考虑到庞大的基数,这个数字已经非常惊人。并且加盟+供应链的营收模式,使杨国福只要扩张速度够快,营收就会相应增长。
这说明三家企业各自摸索出了成熟的模式,具备可快速复制并且盈利的能力,因此想要借助资本市场帮助自己完成扩张。
但是这依然解释不了他们2022集中上市,拿和府捞面为例,在其一开始就选择了中央工厂+门店的模式,早就具备复制能力,并且21年刚刚获得巨额融资,为什么2022年初就急匆匆地上市。
pic_7ebe1fff.png
这就不得不提疫情的影响。我们都知道疫情对餐饮行业的打击是致命的,但是疫情之后,给成熟的连锁餐饮带来的却是利好。

复工这几天我有个明显的感觉,就是公司楼下的小餐馆生意更好了,原本没几个人的馆子现在都座无虚席。这是因为打工人都回来上班了,但是有些餐馆老板还在过年。在消费需求没变的情况下,供应少了,那么剩下的餐馆生意自然就好了。

疫情也是同理。原本每个地区都有各式各样的小馆子,有些还是多年老店,周边顾客已经养成了消费习惯,而这些小馆子大多是个体经营。
但是疫情一来,这些个体餐馆几个月没法营业或者收入暴跌,很有可能就撑不住而倒闭。
复工后,这些餐馆不在了,可是消费者还是要吃饭,这个空窗期就是机遇。这是因为大品牌的食品安全更受消费者信任,在疫情以后订单量受影响相对较小,恢复更快。并且在租金上,大品牌的议价能力更强,恢复期开店反而能获得更多优惠。
乡村基等多个连锁品牌就是利用这种思路,把疫情恢复期当做抄底的好机会,借助投资完成了逆势扩张。
所以在整体上,疫情给餐饮行业带来了巨大的冲击,用了两年时间都没完全恢复。但是在局部,反而因为大洗牌造就了连锁餐饮的繁荣。
这种情况下,谁能早一步抢占市场,就能在后面的竞争里占据主动。并且这个空窗期很短,目前餐饮行业已经处在恢复期的尾声,餐饮企业想要占位,必须借助更大的杠杆。这么一看,上市似乎是最好的办法。
当然了,疫情并不是对所有连锁餐饮都是利好,如果没有完备的后端体系和充足的现金,疫情的打击依然是致命的,但是疫情确实给部分企业带来了机遇。
意识到这一点的资本也纷纷入场,给餐饮企业上市增加了助力。据有关统计数据显示,截至2021年8月,我国餐饮业投融资金额为439.11亿元,已经达到了2020年的2倍,大大小小的细分领域都拿到了融资,甚至出现了拉面馆单店估值千万的怪现象,这让今年餐饮企业的上市变得顺理成章。
除此以外,疫情带来的冲击也让餐饮老板的观念发生变化,越来越多餐饮老板拥抱资本市场。
四年前接受采访时,杨国福曾坦言和上市相比,餐饮还是自己做一把手更踏实。
现在来看,真香。

作者:墩墩 来源:路人甲TM(ID:smcode2016)

收起阅读 »

中国男足的收入是女足的N倍,职场男女收入差距在哪?

中国女足亚洲杯夺冠,举国振奋欢呼,女足姑娘们用两场荡气回肠的逆转胜,连克日本韩国两大亚洲劲敌,第9次登上亚洲之巅,为这个被大年初一男同胞们搞臭的“国足”称号,大大挣回了脸面。赢球了,不能总是虚的,必须来点实际的。有消息称,中国足协此次将重奖女足,奖金总额超过1...
继续阅读 »

中国女足亚洲杯夺冠,举国振奋欢呼,女足姑娘们用两场荡气回肠的逆转胜,连克日本韩国两大亚洲劲敌,第9次登上亚洲之巅,为这个被大年初一男同胞们搞臭的“国足”称号,大大挣回了脸面。

pic_fd442c71.png

赢球了,不能总是虚的,必须来点实际的。

有消息称,中国足协此次将重奖女足,奖金总额超过1000万元。男足在世界杯预选赛上的赢球奖金是一场600万元,女足拿了亚洲冠军,奖金超千万完全配得上。

pic_28919314.png

就连知名足球解说员黄健翔也呼吁:“请按照男足奖金标准双倍给女足发奖金!”

pic_343c00ec.png

除了大赛奖金,姑娘们平时的收入待遇问题,也被网友们热议:相对男足动辄近千万的年薪,中超女足队员最高不到百万……

后来又有媒体出来说了,如果进行横向比较,中国女足队员的收入自2018年以来已是世界第一。

所以在全球范围内,男足女足同工不同酬,绝对是普遍现象。

一般的解释是,这是由于男足运动背后巨大的商业价值所决定的,短时间很难改变。

且不去论证其中的合理性,我们回到普通的就业职场,男性和女性在收入差距上又是怎么样的状况呢?这背后有哪些深刻的原因呢?

pic_7e57afbb.png

pic_c02436f0.png

“男女同工不同酬”是个假议题?

从整个职业生涯来看,女性赚得比男性少,目前来看,不管在哪个国家哪个时代都是如此。女性的平均收入是男性的多少?

在美国这个数字是81%;在日本是73%;在丹麦这样的北欧国家,也只有85%。

放眼世界,你找不到一个男女平均收入差不多的国家。

有一种解释是,女性薪酬低于男性,除了管理层占比偏低,还在于女性主要从事的岗位普遍缺少高薪属性。

比如女性主要分布在财务、行政、人力资源和销售岗,现在普遍高薪的技术、产品岗位上男性的占比优势明显,这也影响女性的平均薪酬水平。

根据人才咨询服务公司“Korn Ferry”针对欧洲870万员工的一份调查显示:

虽然以平均值来说,女性薪资确实比男性少了18%,但若以在相同职位从事相同工作的男女做比较,薪资差距就下降到7%。

若再进一步以同公司的同职位来比较,差距则又会缩小到只剩2.6%,若是连工作内容也相同,则差距只剩不到2%。

这样看上去,同样公司的同样职位上来看,基本上实现了男女同工同酬。

那为什么男女收入整体来说还存在比较大的差距呢?

我们来看看这个现象,那就是男女薪酬差别,在刚入职场的时候比较小。

据史丹福大学经济学教授Claudia Goldin的数据统计,刚毕业的大学生,女性平均薪资是男性的92%,但到了四十多岁,降到了73%。

pic_6f1e15a8.png

类似的研究调查MBA毕业生,刚毕业时,女性约为男性薪酬的92%,但十年后,数字狂降到57%。

她的研究指出,的确在雇用和职场环境上,对男女有不平等待遇,这不平等待遇也可能造成男女薪酬不同。但许多工作对时间要求的不同,可以解释绝大多数的薪酬不同。

也就是说,歧视也许有,但男女对工作能够投入的时间,却是主要男女薪酬不同的原因。

为什么男女对工作投入的时间差别会随着年龄增长,越来越大呢?

其实这并不难解释,那就是女性为了承担家庭责任比如育儿(无偿劳动),主动或被动地选择了灵活性更高,但工资更低的工作。

在传统家庭分工中,女性承担了更多的家庭责任,这导致工作时间更短并且不连续,此她们的工作经验将短于男性,而这会降低她们的相对工资。

pic_0844cf66.png

女性的职业惩罚

女性收入不如男性,主要是生育带来的后果。

这个研究是由普林斯顿大学的经济学家亨里克·克莱文(Henrik Kleven)完成的。

他使用的数据来自全世界社会福利最好的国家之一:丹麦。

丹麦为新生儿童的父母提供一整年的带薪假期。政府为3岁以下的儿童提供公立的婴幼儿照护服务。

克莱文发现,女性的收入在第一个孩子出生以后出现了断崖式的下跌,而男性却没有类似的薪酬下降。

这种下跌带来的影响是深远的:在职业生涯中,女性最终会比男性少20%的收入。

研究估计,丹麦男女收入差距的80%是生育造成的 。

越来越多类似的研究表明,我们通常所说的“男女收入差距”,准确来说应该是“生育收入差距”或“做母亲的代价”。

相比于经历了收入断崖下跌的母亲们,没有孩子的女性和男性的收入相似。有孩子的女性的薪酬在生育期急速下跌,之后缓慢增长;

而没孩子的女性薪酬却持续增长,最终两者间出现一定的收入差距。

这显示出生儿育女对女性薪酬发展的巨大影响。

在中国,女性接受高等教育的比例已经超过男性。

受教育程度和收入是成正比的,所以教育因素对收入的影响是在逐渐消失。

但女性生育的“职业惩罚”,在对女性收入的影响上是顽固的,没有减缓迹象。

职场不存在绝对公平,但一个不可否认的事实是,在中国,男女职场人遭遇的不公平待遇呈现差异化。

对于女性来讲,“应聘过程中被问及婚姻生育状况”现象最为普遍,一半以上的女性都有遇到。而男性,被问到这个问题的机率非常小。

男性在谈到晋升障碍时,多归结于个人能力、上级领导、同级竞争、公司制度等与职场相关度高的原因。

而女性更担忧的是“照顾家庭,职场经历分散”、“处在婚育阶段,被动失去晋升”、“性别歧视”。

这也再度反映了性别、婚育计划等与工作能力无关的要素,却很大程度上影响女性职场价值,成为牵制女性职业发展的“玻璃天花板”。

尽管工作时间与男性和非父母职工相当,职场妈妈群体的平均收入,也会比生育前少挣12.5%。

部分女性群体还存在生育后就业几率变小的问题。尤其是工资水平较高和高级职位的女性,可能会受到更严重的惩罚,因为她们生育后的月收入增长最慢。

其中也有特例,比如我朋友A在喂母乳到孩子一岁之后,重新找了工作,之后几年经历了收入和职位的节节上升。

她说,“我得特别感谢我老公,实际上来讲我们俩有点像打配合的感觉,说在过去这几年里,尤其是说我生了老二之后,其实他帮了我担负了很多。

pic_a0675e3c.png

因为这几年是我爬坡的过程,他自己开公司,他会比较相对来讲有自由度,但是我是没有的,所以他很大程度上能帮我去看孩子。

所以我们俩的配合通常是说他是带孩子的,比方说周日啊出去,都是我老公,我呆在家里,可能我要加班,也会收拾屋子买菜做饭。”

还有一些女性从原生家庭获得了很多育儿和家务支持。她们在育儿方面的投入增加,但工作强度不变或者增多,休闲时间相应减少。

例如,许多女性因为二宝出生后的经济压力,变得更加努力工作来提升职位和收入。

兰卡斯特的副教授胡扬在2016年出版的书中提到,这一代中国年轻女性经历体现了生命历程的结构性不一致(life-coursestructural inconsistency)。

在校期间,她们经历去性别化的教育竞争。她们被要求在应试教育中取得优异成绩,考入名牌大学,找到一份好工作。

pic_2d6cd1c0.png

在这个阶段,家长、老师和社会对她们的主要期待是去性别化的。

然而,在结婚生孩子后,社会期待女性以家庭为中心,期待女性在家庭中付出更多时间和精力,默认家庭内男女角色不同。

这些期待促使职业女性接受了性别化的角色。因此,她们为了家庭调整了自己的职业发展轨迹,经历了性别的“再社会化”,接纳与遵循了传统的社会性别分工。

如何应对这个结构性的问题,是值得整个社会和每个受影响的个体去思考的。

参考文献:

[1] https://focus.kornferry.com/wp-content/uploads/2015/02/KF-Gender_Pay_Gap-%E6%B6%88%E9%99%A4%E6%80%A7%E5%88%AB%E8%96%AA%E5%B7%AE.pdf

[2] Hu, Y. (2016). Chinese-British Intermarriage:Disentangling Gender and Ethnicity, London: Palgrave Macmillan.

来源:https://mp.weixin.qq.com/s/htCOWCywWBXOwGSSIDiTnw

收起阅读 »

科学研究:女性比男性更优秀?

导读:新女性时代要来临了吗?01 未来是女性的尽管目前男性在中高层的管理者当中依然占据着绝大多数,但10年后,伴随着自动化的发展,他们将逐渐失落。为什么?因为,未来发展的方向很明显是“零工经济”:自由职业者和“小微创业者”为了一个项目组成临时的团队,完成眼前的...
继续阅读 »
导读:新女性时代要来临了吗?

pic_48ebd6eb.png

01 未来是女性的
尽管目前男性在中高层的管理者当中依然占据着绝大多数,但10年后,伴随着自动化的发展,他们将逐渐失落。
为什么?
因为,未来发展的方向很明显是“零工经济”:自由职业者和“小微创业者”为了一个项目组成临时的团队,完成眼前的任务;透明度、信任以及有效的调控由技术负责;项目本身则是新的主管。
在这一模式下,经理基本已无存在的必要。
当然,如此一来,更紧要的将是兼具责任心和能力的领导层,这一特质在女性那里要常见得多。
高校里遍布着受到良好教育的年轻女性,在西方大学的许多未来性专业中,女性学生如今已占多数。

02 大脑研究:为什么女性更具领导力?
圣雄甘地一早就断言:

指称女性为弱势性别是一种污名化,是男性对女性的不公正对待。若从血腥暴力的角度去理解,女性自然逊于男性。
然而,若从道德力的角度去理解,女性的优越程度则难以计量。她们有更强烈的直觉、更伟大的牺牲精神,她们更坚忍也更勇敢,不是吗?没有她们,男性什么都不是。如果非暴力是我们实然状态的法则,那么未来将属于女性。

美国精神病科专家、知名医生丹尼尔·亚蒙在其著作《女性脑》中从神经学的角度说明了,就当今世界的需求而言,女性构造具有怎样的优势。
亚蒙列举了女性明显胜任领导者职位的五大长处:
同理心、合作、直觉、自制和责任心。
为什么女性的大脑在这些领域中的表现会比男性的大脑亮眼得多?
pic_0477a9d3.png
原因在于激素储备的性别差异。女性的大脑早在子宫里便沉浸在女性的性激素——雌激素当中,相反男性的大脑是被男性的性激素睾酮所包裹的。
当下的研究结果显示:脑前额叶或前额叶皮质的发育会受到雌激素的强烈刺激。因此女性的这一结构普遍比男性大,比男性成熟得早。
人类大脑这一最新进化出的成分专门司职我们的认知和决策。
这两个素质对企业行为和一般领导力而言至关重要,尤其是在当今这个越发复杂且变化越发迅速的社会中。
右前额叶尤其与前瞻性思考相关,这是一种相当重要的关乎项目调控的预见力。
所有女性通常会比男性更积极地寻求提早完成课业,无论是在中小学、高校,还是在工作场合中。
男性因睾酮之故更具攻击性和性冲动。一般来说,他们的体格比女性强壮,然而就21世纪社会所需的技能而言,显然他们才处于弱势。
男性可以练就更大块的肌肉,可以更威猛地引发对峙和紧张关系。就在时间压力下完成一项任务而言,他们也比女性要强一些,因为他们喜欢由压力产生动力:时间越紧迫,男性机体中多巴胺和降肾上腺素这类神经递质会分泌得越多。
相反,女性在无聊的工作中也基本能保持积极性。如果一项工作可以被提早完成,女性机体中的应激激素水平就会下降。
大脑研究者也曾宣称,女性在棘手的情境下更容易保持冷静的头脑。众所周知的杏仁核在女性大脑里所占区域明显比男性要小。
大脑里这一古老的组织负责直觉性的行为模式,如攻击和盛怒。
在战斗或逃跑模式下,我们的大脑会从前额叶主导切换至杏仁核主导:我们在短时间内将自己变为原始时代的猎人(或是被其追捕的逃亡中的猎物)。
男性的杏仁核不仅比女性的大,它还拥有大量的雄性激素接收装置,一旦男性激素被大量释放,它就会异常活跃。
相对而言,即便为棘手状况所困,女性大脑里的前额叶皮质,即理性之“我”的所在,依然能战胜大脑中主管攻击和恐慌机制的古老区域,保持控制。
女性大脑里的保险装置不易熔断也和前扣带皮层有关。主导抑制冲动作用的边缘系统的这一部分在女性大脑中的分布区域明显大于男性。
研究者认为,这一人体结构上的差异至少部分解释了女性相较男性而言对低风险的偏好。
较小的杏仁核与较大的前扣带皮层这一对组合使女性在较大的压力下也能控制其情感,寻求最优的解决方式。
pic_48ab6bec.png

03 女性VS男性:我们如何使用大脑?
此外,不同的“布局”也很重要。男性的大脑拥有更多的神经元,但是女性的大脑半球之间显示出更强的关联性。
就典型性而言,男性偏爱使用主管理智、逻辑和模式识别的左脑,因此他们更擅长将注意力集中在具体目标上并系统性地着手完成任务。他们可以实现自我激励,但倾向于机械性地、超脱于周边环境来做出反应。
相反,女性更偏爱使用右脑。右脑可以帮助其良好地共情、与他人建立联系、塑造和获得社会关联,以及创造性地寻找到答案。
此外,她们“跨半球”运用大脑的能力也大大优于男性,这是一种同时使用两个大脑半球的能力。
不止于此,就连女性大脑里的岛叶皮质也比男性的大。那是直觉,俗称“第六感”的所在。岛叶皮质主管共情能力、情绪自觉以及语言传递性思维。
因而女性(同其他雌性灵长类动物一样)比男性拥有更强的沟通能力,可以更轻易地辨别面孔和更充分地表达情感。
此外,她们解读他人情感和深意的能力更强,对此男性常常不能在第一时间予以充分关注。
而且,女性通常比男性的记忆力更佳。
她们的海马体,这一主管记忆的结构也比男性的更大、更活跃。也正因为如此,她们的学习能力和长久储存所学知识的能力更强,尤其是她们的听觉皮层(负责将所记和所学转化为语言的生理构造)比男性的大。
简而言之(且以“典型的男性角度”):就21世纪的领导性岗位而言,女性的资质相当卓越。
出于大脑构造的缘故,她们在共情、团队合作和自控方面明显要比男性强。
所有的一切都表明,在量子经济下,她们将占据领导性岗位当中的相当份额。
至少可以确定的是:就我们的经济和社会的中央枢纽而言,我们需要大量拥有女性大脑的人。

作者:安德斯·因赛特 来源:身边的经济学(ID:jjchangshi)  

收起阅读 »

大洗牌!2021年,全国TOP50城市GDP排行榜

导读:中国城市,正在迎来新一轮大洗牌。产业格局、人口形势、国际局势变迁,疫情、洪涝、能耗双控等因素影响……全国城市经济格局发生了巨大变化。谁进谁退?01 50强城市,谁是守门员?这是2021年内地GDP50强城市排行榜:中国城市第一梯队的竞争,已从最初的万亿俱...
继续阅读 »
导读:中国城市,正在迎来新一轮大洗牌。

产业格局、人口形势、国际局势变迁,疫情、洪涝、能耗双控等因素影响……全国城市经济格局发生了巨大变化。
谁进谁退?

01 50强城市,谁是守门员?

这是2021年内地GDP50强城市排行榜:
pic_e67a993c.png
中国城市第一梯队的竞争,已从最初的万亿俱乐部,向2万亿乃至3万亿俱乐部进军。
同时,内地万亿GDP城市已经扩容到24个,南方18个, 北方6个。
TOP50城市的门槛也提高到了5000亿元以上,太原是“守门员”,还有部分省会未能晋级50强之列。
具体来看,京沪联袂突破4万亿,成为我国仅有的2个4万亿大市。
作为地位最为超然的两大一线城市,一个是政治和文化中心,一个是经济和金融中心,未来综合实力仍将遥遥领先。
京沪之后,同为一线城市的广深,都在向3万亿进军。
虽然与京沪之间的差距有所拉大,但广深都只是副省级城市,广州更是受制于三级财政,无论行政级别还是城市能级,抑或政策力度都难与京沪匹敌,更多依靠自力更生,能取得如此成绩殊为不易。
在2万亿俱乐部中,广州、重庆、苏州遥遥领先。2022年,广州、重庆突破3万亿问题不大,而苏州未来5年同样不乏挑战3万亿的可能。
pic_97d74350.png
事实上,在TOP10城市里,成都离2万亿只有一步之遥,而杭州、武汉等地突破2万亿也只是一两年的时间问题。
同时,万亿俱乐部成员,再次迎来扩容,但2021年仅有东莞一城晋级。目前,全国已有24个万亿城市,18个位于南方,6个位于北方。
当然,随着万亿城市大扩容,万亿GDP的含金量也在急速下降,万亿将只是城市竞争的起点,而非终点。

02 大城博弈:谁晋级了?
城市经济,可谓你追我赶,不进则退,没有谁能永葆强势。
过去一年,哪些城市守住了经济优势地位?哪些城市受到了挑战?
其一,济南、合肥、福州、东莞、唐山、沈阳、南昌、太原等省会或经济强市,实现了晋级。
太原以1亿元左右的微弱优势,超过南宁,晋级50强城市,成为TOP50城市的“守门员”。
南宁、贵阳、乌鲁木齐、兰州、呼和浩特等省会城市止步于50强之外。
其二,广州稳超重庆,守住了GDP第四城之位,且拉近了与深圳之间的距离。
2021年,广州GDP为2.82万亿元,重庆为2.78万亿,广州对重庆的领先优势扩大到300亿元以上。
同时,广深之间的差距,从前几年高峰时期的4000亿元以上,缩小到2400亿元左右。
pic_94049bb6.png
正如《全国GDP第四城之争,再无悬念》一文的分析,这背后得益于广州完成了产业转型,借助新一代信息技术、生物医药、新能源等新动能,以及城市更新的助力,经济发展重回高增长轨道。
其三,天津仍然止步于十强之外。
自2020年南京取代天津成为内地GDP第十城之后,天津经济何去何从就成了舆论关注焦点,十强城市只剩下北京一个北方城市之类的说法不绝于耳,这在凯风新书《中国城市大趋势》中有详细论述。
不过,2021年,天津离TOP10仍有一步之遥。2021年,天津GDP1.57万亿,离位居第十名的南京仍有600多亿的差距。
pic_75e94abc.png
继2020年南京首次超越天津,晋级十强城市之后,2021年天津仍未能反超南京,与南京的差距约为600亿左右。

03 谁是最大“黑马”?
2021年,城市经济涌现出一批“黑马”,经济实力实现大幅跃升。
在TOP50城市中,进步最大的城市有4个:陕西榆林、山西太原、山东潍坊、福建福州。
其中,福州借助强省会优势重回省内经济第一大市之位,GDP连超泉州、南通和西安,跻身TOP20城市之列。
而山东潍坊,则反超石家庄、绍兴、盐城等城市,排名大幅提升。
潍坊的晋级,一方面得益于工业增长强劲,工业增速超过12%;另一方面则来自投资的贡献,固定投资增速超过16%,其中基建投资超过33%。
潍坊曾喊出“咬定地区生产总值过万亿、冲刺全国大中城市综合实力前30强、加速迈入国内二线城市行列”的口号。
如果能保持高速增长,这一目标未必没有实现的可能。
相比而言,榆林、太原更为突出。
2021年,榆林、太原GDP名义增速分别高达32.9%、23.3%,城市排名分别跃升11位、6位,两城更是得以晋级50强之列。
pic_b3d2de23.png
榆林和太原大幅晋级,得益于资源价格大涨。
榆林是著名的煤炭大城,而太原所在的山西则是全国产煤第一大省。
过去1年,在全球供应链紧张、通胀高企以及能耗双控影响之下,煤炭产能扩张,而价格大幅上涨,由此带动资源型城市经济总量的罕见大增长。
pic_79a2bed5.png
这其中,榆林最为典型。2021年,榆林GDP达5435.18亿元,不仅超过了邻省省会太原,而且还超过了襄阳、宜昌,直逼长期领跑中西部非省会城市的洛阳。
煤炭价格大涨,对榆林经济的贡献有多大?
数据显示,2021年榆林全市规上工业产值首次突破7000亿元大关,比上年增长55.9%。其中,能源工业产值增长61.4%;化工行业产值增长54.3%。
pic_22fdc795.png
当然,未来能源行业,还会面临美联储加息、全球供应链恢复常态、双碳战略等影响,能否继续保持高速增长,值得关注。

04 强省会的逆袭
2021年,强省会可谓光芒四射。
这一年,不仅成都、武汉等传统强省会实现了再突破,一些向来“弱势”的省会经济实力也迈上新台阶。

前不久,福建福州、江西南昌、广西南宁、贵州贵阳、河北石家庄、山西太原不约而同将“强省会提上日程”,有地方甚至喊出了“省会强则全省强,省会兴则全省兴”的口号。
过去一年,多个省会经济实力提升:
沈阳逼近30强城市,重返东北第一大省会之位 福州跃居20强城市之列, 太原跻身50强城市, 济南、合肥、南昌等省会排名也有明显提升。
这其中,最典型的当属福州,不仅跃居20强城市,还重回福建第一大市之位。
在福建省,长期以来都是“三城鼎立”的格局:福州为省会,泉州为经济强市,厦门为经济特区、计划单列市和副省级城市。
2021年,福州GDP达11324.48亿元,而泉州为11304.17亿元,厦门为7033.9亿元。
这意味着,福州以20亿的领先优势,时隔20多年,重回福建经济第一大市。
pic_25c08bad.png
这背后,不无强省会战略的助力。
去年,福建出台《关于支持福州实施强省会战略的若干意见》,支持福州创建国家中心城市,大力支持福州做大做强,增强省会城市辐射带动力。
pic_1fe279aa.png
这意味着,福州不仅要做强省会,而且还要竞夺国家中心城市。要知道,同属一省的厦门,也曾提出创建国家中心城市的想法。
虽然福州重回福建第一大市之位,但福州与厦门、泉州的良性竞争还会持续。

05 东北,仍然只有一个TOP30城市
东北共有4大中心城市:大连、沈阳、长春、哈尔滨。
这4个城市全部位列副省级城市,占全国副省级城市(共15个)的四分之一强。
据分析,东北4市之所以能全员晋级,是因为在副省级城市设立的1990年代,东北经济还在全国位居前列。
后来副省级城市再未进行任何扩容,这也导致郑州、长沙、合肥等万亿强市仍然只是普通省会。
不过,近年来,东北经济强市被其他省会陆续赶超。加上前几年经济普查,原本已经突破7000亿的多个东北地市,GDP遭遇挤水分,又回到了重新攀上7000亿的历程。
2020年之后,东北各地发展均步入正轨。
pic_a170e9d3.png
从2021年经济数据来看,大连以7825.9亿元位居东北第一,在全国排名第29名。
沈阳GDP达7249亿元,反超昆明、长春,位列31名,重回东北第一大省会之位。
长春GDP为7000亿元左右,位列30-40名之间,哈尔滨GDP为5351亿元,位列50名之内。
未来几年,东北有望诞生第一个万亿城市。

06 谁是下一个万亿城市?
目前,内地共有24个万亿GDP城市,其中南方18个,北方6个。
广东独占4席,与江苏并列第一,广州、深圳、佛山、东莞全部破万亿。
北方的6个万亿城市分别是:北京、天津、青岛、郑州、济南、西安。
pic_d958d245.png
那么,谁是下一个万亿城市?
可以看到,东莞晋级之后,9000亿量级城市存在明显断层,这意味着2022年或许将是没有新晋万亿城市的一年。(参阅《又一个万亿GDP城市诞生》)
目前,8000亿量级共有4个城市:江苏常州、山东烟台、河北唐山、江苏徐州。
这些城市快则未来2年、慢则2025年之前,都有望跻身万亿城市之列。
而在7000亿量级,则有大连、温州、昆明、沈阳、潍坊等众多城市,这些地方未来5年左右也大概率会有万亿城市诞生,而东北地区也有望实现零万亿城市的突破。
当然,随着中国经济总量超过美国3/4,省域经济最高已经攀升到12万亿以上,市域经济最高也超过4万亿,万亿城市的含金量将与以往有着明显不同。
届时,2万亿城市,或将是新的起点。

作者:凯风 来源:国民经略(ID:guominjinglve) 收起阅读 »

什么是元宇宙、新基建、赛博空间?7个最火科技名词解释,都在这里了

导读:人们从学术、科幻、政府、产业等角度对数字未来有一系列设想,在过去、现在与未来,这些设想引导我们去探索与创造。这里做简要梳理供你参考。01 地球村(Global Village)这是媒介学者麦克卢汉提出的理论,在他1964年的著作《理解媒介:论人的延伸》中...
继续阅读 »

导读:人们从学术、科幻、政府、产业等角度对数字未来有一系列设想,在过去、现在与未来,这些设想引导我们去探索与创造。这里做简要梳理供你参考。

pic_f9843244.png

01 地球村(Global Village)

这是媒介学者麦克卢汉提出的理论,在他1964年的著作《理解媒介:论人的延伸》中提出。这个词形象地告诉我们,信息技术的发展缩短了地球上的时空距离,整个地球像一个小小村落。

02 赛博空间(Cyberspace)

它由科幻小说作家威廉·吉布森在1982年的小说《全息玫瑰碎片》中提出,指计算机以及计算机网络里的虚拟现实。它还演化出了“赛博朋克”等概念,对科幻小说与电影的影响巨大。机器与人的混合体“赛博格”(Cyborg)与它有着同样的渊源——控制论(cybernetics)。

两年后,在小说《神经漫游者》中,吉布森让赛博空间更加具象,主人公凯斯让自己的神经系统挂上全球计算机网络,他使用各种匪夷所思的人工智能与软件为自己服务。赛博空间原指与工业化实体空间截然不同的新空间,后来逐渐被等同于网络空间或数字空间。

03 数字化生存(Digital Being) 数字化生活(Digital Living)

它于1996年由尼葛洛庞帝在开启数字化未来的畅销书《数字化生存》中提出,他当时是美国麻省理工学院(MIT)的未来科技研究机构媒体实验室主任。

数字化生存指的是,人们从原子世界的生存演进到比特世界的生存。他展示的众多数字化生活的设想,后来大多变成了现实。在过去30年,互联网产业发展外溢形成数字经济、数字社会,人类的数字化生存与生活逐渐成为现实。

pic_4ac289b4.png

04 信息高速公路(Information Highway) 中国“新基建”(China New Infrastructure)

我们可以看到中美两国的相关政策举措虽时隔近30年,但遥相呼应。1992年,时任参议员、后曾任美国副总统的戈尔倡导建立“国家信息基础设施”,并形象地命名为“信息高速公路”。

2020年,中国的相关政策强调加快5G网络、数据中心等新型基础设施的建设进度。一般认为,新基建包括5G、特高压、城际高速铁路和城际轨道交通、新能源汽车充电桩、大数据中心、人工智能、工业互联网、物联网等领域,其中主要为与数字技术相关的基础设施。

05 互联网公司(Dot.Com & Internet Company) 数字经济(Digital Economy)

互联网公司最初被称为Dot.Com,后来逐渐地形成了包括多个细分领域(如内容、社交、电商)的互联网大产业。自20世纪90年代初互联网商业化以来,互联网产业以自身的方式演化与发展——从PC互联网到移动互联网,从线上到线下。

近年来,互联网的关注重点从应用为主(新闻、社交、电商、游戏、打车等),转向技术主导(大数据、机器学习、芯片设计与制造、虚拟增强现实、区块链等)。现在人们通常认为,互联网公司的典型形态是连接供需双方的互联网平台。

唐·塔普斯科特被认为在1995年出版的《数字经济》一书中首次提出了“数字经济”。后来马化腾、孟昭莉等著的《数字经济》中提到人类社会、网络世界与物理世界的融合,这三者融合形成的正是现在我们所说的数字经济,这一观点的特点是将人类社会中的社交关系纳入了数字经济之中。

pic_22e73816.png

06 全球大脑(Global Brain)

近年来,人工智能在数据、算法、算力的三重刺激下重新爆发。人们看到,互联网在大数据与人工智能的支持下成了人类整体的“全球大脑”。全球大脑不是全新概念,凯文·凯利在《必然》一书中有一种形象的描述,既呼应了前人的观点,又结合了新变化:

真正的人工智能不太可能诞生在独立的超级电脑上,它会出现在网络这个由数十亿电脑芯片组成的超级组织中……任何与这个网络人工智能的接触都是对其智能的分享和贡献。这种人工智能连接了70亿人的大脑、数万兆联网的晶体管、数百艾字节的现实生活数据,以及整个文明的自我修正反馈循环。

07 元宇宙(Metaverse) 第三代互联网(Web 3.0)

元宇宙这个概念由科幻小说家尼尔·斯蒂芬森在其1992年的小说《雪崩》中提出,主人公戴上接入网络的虚拟现实头盔,就可以生活在由电脑与网络构成的虚拟空间。这本书对虚拟现实和游戏的发展影响巨大。最终在21世纪第三个10年,在技术与产业成熟之后,元宇宙成为数字化未来设想的代名词。

我们将元宇宙视为实体世界与数字世界融合的新世界,称之为第三代互联网(Web 3.0)(相关阅读:为什么Web 3.0就是元宇宙?),并将它细分为立体互联网与价值互联网。

作者:方军 来源:大数据DT(ID:hzdashuju)  

收起阅读 »

浙江出招:大学生如果创业失败,贷款10万以下的由政府代偿

2月17日上午,国家发展改革委举行新闻发布会,介绍支持浙江省高质量发展建设共同富裕示范区推进情况。浙江省人力资源和社会保障厅副厅长陈中在答记者问中介绍,为鼓励大学生创业, 浙江大学生如果创业失败,贷款10万以下的可由政府代偿。陈中表示,高校毕业生是宝...
继续阅读 »

2月17日上午,国家发展改革委举行新闻发布会,介绍支持浙江省高质量发展建设共同富裕示范区推进情况。

浙江省人力资源和社会保障厅副厅长陈中在答记者问中介绍,为鼓励大学生创业, 浙江大学生如果创业失败,贷款10万以下的可由政府代偿。

陈中表示,高校毕业生是宝贵的人才资源,浙江始终坚持把高校毕业生就业工作当作人才工作来抓,从来没有把他们当作包袱、压力、负担,而是把他们作为优质的资源来配置、引进、使用和储备。今年,全国高校毕业生超过1000万,对浙江来说是一个很好的机遇,我们要抓住这个机遇,大力引进高校毕业生。

浙江的高校毕业生就业政策比较丰富。除了杭州市区,全面放开专科以上学历毕业生的落户限制,杭州的落户条件为本科以上学历。高校毕业生到浙江工作,可以享受 2万到40万不等的生活补贴或购房租房补贴。大学生想创业,可贷款10万到50万, 如果创业失败,贷款10万以下的由政府代偿,贷款10万以上的部分,由政府代偿80%。大学生从事家政、养老和现代农业创业,政府给予10万元的创业补贴,大学生到这些领域工作,政府给予 每人每年1万的就业补贴,连续补贴3年。大学生到浙江实习的,各地提供生活补贴。对家庭困难的毕业生,发放每人3000元的求职创业补贴。我们欢迎全国的高校毕业生到浙江来就业创业。

浙江是用工大省,省外务工人员在浙江有2300万,他们为浙江经济社会发展作出了重要贡献。在浙江,省外务工人员与本地户籍的劳动者享受同等的就业创业服务和政策。另外,浙江还开发不讲技能、不讲学历、不讲年龄的爱心岗位,专门安置脱贫人口,保证他们的月薪4500元以上,去年全省有脱贫人口225万。

浙江的平台经济比较发达,各种新就业形态快速发展,浙江非常关注新就业形态劳动者的劳动保障问题。去年,浙江专门出台了维护新就业形态劳动者劳动保障权益的实施办法,主要是放开了灵活就业人员在就业地参加企业职工基本养老保险、基本医疗保险的户籍限制,支持新就业形态劳动者单险种参加工伤保险;我们还要求平台企业发挥数据技术优势,合理管控在线工作时间,对连续工作超过4小时的要安排工间休息。

来源:国家发展和改革委员会官方网站、浙江新闻客户端

收起阅读 »

近六成员工强烈支持,携程将推出“3+2 ”工作模式,一周三天到岗两天在家办公

对于通勤时间动辄一两个小时的上班族来说,在家办公可谓是最理想的选择。情人节当天,携程宣布将推出“3+2”混合办公制度,并在全公司实行。从3月1日起,每周三、周五,员工可以根据自己的实际需求选择在家办公,当然这个“家”也可以是咖啡馆和酒店等任意地点。 携程方面透...
继续阅读 »

对于通勤时间动辄一两个小时的上班族来说,在家办公可谓是最理想的选择。情人节当天,携程宣布将推出“3+2”混合办公制度,并在全公司实行。从3月1日起,每周三、周五,员工可以根据自己的实际需求选择在家办公,当然这个“家”也可以是咖啡馆和酒店等任意地点。 携程方面透露,这次全面推广“3+2”工作制,将覆盖该集团全部员工(约3万人),不分男女、不分值岗、不做薪资调整。
pic_0e7c2131.png 新制度经过多次试验

此次携程推出的新制度并不是盲目跟风,而是经过了对公司员工的多次实验。早在2010年,携程就组织客服人员进行了为期9个月的“在家办公”试验。结果显示,员工的业绩由随机实验中的13%进一步上升为22%,员工的离职率下降了50%。 后来在2020年疫情爆发期间,携程近70%的客服员工依据经验迅速实施“在家办公”方案,部分部门在家办公人数近85%。结果显示,在不受办公室干扰和通勤时间等因素的影响下,在家办公的人完成的工作更多。 该试验的发起人——携程集团联合创始人兼董事局主席梁建章表示,设立新工作制度的目的,既是疫情防控所需,更为缓解员工照顾家庭、带孩子的压力,最好能促进社会生育率的提升。恢复正常上班后,梁建章再次倡导职能更广泛的混合办公试验。 在携程2021年8月到2022年1月的混合办公试验中,有超过1600人参加。结果显示,员工参与意愿上升至近六成,不支持的人数下降至了0.1%。

pic_985fcd1a.png

图源:携程 在支持混合办公的原因中,投票最高的3个理由分别是:减少了通勤时间;工作和生活更平衡;更幸福、更富创造力。在不影响绩效的情况下,离职率下降约1/3。

pic_38ef91ac.png

图源:携程 但同时也有部分员工和主管表达了对这个模式的担忧,其中,“担心影响同事之间的交流”占近50%,主管也有“担心难以管理”等问题。对此,携程人力资源部相关负责人介绍,接下来公司将重点探索的是对员工赋能,提升远程办公效率和对主管赋能,助力主管加强管理能力。

pic_dd8a9fa3.png

图源:携程 携程集团董事局主席梁建章说:“混合办公正在变成一种全球性的趋势,期待更多企业效仿和推广。”他还表示,混合办公制度的推广是企业、员工和社会的多赢。
pic_f4964732.png 多家互联网大厂实行混合办公制度

随着互联网技术的发展,远程办公已逐渐成熟并可被实现,受新冠疫情的影响,全球更是掀起了远程办公的潮流。据报道,已有多家美国互联网公司实行混合办公制度。 微软和推特早在2020年就推出允许员工长期在家办公的新政策。扎克伯格在去年宣布,在所有全职员工可以远程完成自己的工作的前提下,允许他们长期在家办公。Google去年也实行了混合型工作周,大多数员工只需在办公室办公三天,其余两天可以在他们最适合的地方办公;另外还有“自由工作周”,即员工每年“可以在办公室以外的地方工作4周 (经经理批准)”。苹果和亚马逊也实施了类似的混合办公政策。 再看国内,去年钉钉总裁叶军在全员群发出倡议,从11月开始,钉钉员工可在家使用钉钉远程办公一天。12月,阿里巴巴在内网公布了多项针对员工关怀的“暖心计划”,其中一项就是将试行灵活办公制度,鼓励有条件的团队试行每周不超过一天的灵活办公,可自由选择办公地点。 此前公众号的一篇文章曾提到,远程办公或常态化。CSDN的AI小组的成员分布在北京,长沙,深圳和天津,每个人都可以说是在远程办公,他们每天早上9:30有一个半小时的晨会,交流项目进度,然后其余的时间就各自按照各自的优先级自行工作。他们通过企业微信,Git来交流。他们的“技能树森林” 项目,还有不少外部团队通过开源的方式贡献。
pic_95df5f84.png 网友:既高兴又担忧

消息一出,不少网友表示,“真令人羡慕,谁不愿意在家办公呢?”,“如何让老板不经意间看到这条微博。”同时,也有网友提出了自己的担忧,“在家办公基本等于24小时在线办公”,“这只会让工作和生活界限模糊。” 网友@奶盖呆桃:什么?!这么好,我也想在家办公,这样压力会小很多的 网友@一枚儿童:减少不必要通勤时间,挺好 网友@大尾巴婷:建议大力推广 网友@茶茶喝香草养乐多: 这个模式还可以,不过要先试试看,如果合适就推行就行啦 网友@Chaoybb_虞:居家办公24小时待机,而且自制力差的真的不建议。像我就是总玩,然后熬夜通宵,身体熬完了 网友@我寄愁心与明月··:我不想,家里办公,效率太低,不仅仅要工作,还有一堆琐事 携程“3+2”混合办公模式算是企业灵活办公模式的首次尝试,具体结果如何还要看后续的实施情况。你对携程的新制度有什么看法呢?如果你所在的公司可以申请混合办公,你会支持吗?欢迎在评论区留下你的看法~ 

参考链接:

整理 | 于轩
出品 | 程序人生(ID:coder _life)

收起阅读 »

图解 ArrayDeque 比 LinkedList 快

在之前的两篇文章中主要分析了 Java 栈的缺点 ,为什么不推荐使用 Java 栈 ,以及 为什么不推荐直接使用 ArrayDeque 代替 Java Stack 。更多内容点击下方链接前去查看。 算法动画图解 | 被 "废弃" 的 Java 栈,为什么还在...
继续阅读 »

在之前的两篇文章中主要分析了 Java 栈的缺点为什么不推荐使用 Java 栈 ,以及 为什么不推荐直接使用 ArrayDeque 代替 Java Stack 。更多内容点击下方链接前去查看。



接口 Deque 的子类 ArrayDeque ,作为栈使用时比 Stack 快,因为原来的 Java 的 Stack 继承自 Vector,而 Vector 在每个方法中都加了锁,而 Deque 的子类 ArrayDeque 并没有锁的开销。


接口 Deque 还有另外一个子类 LinkedListLinkedList 基于双向链表实现的双端队列,ArrayDeque 作为队列使用时可能比 LinkedList 快。


而这篇文章主要来分析,为什么 ArrayDequeLinkedList 快。在开始分析之前,我们需要简单的了解一下它们的数据结构的特点。


接口 Deque


接口 Deque 继承自 Queue 即队列, 在 Java 中队列有两种形式,单向队列( AbstractQueue ) 和 双端队列( Deque ),单向队列效果如下所示,只能从一端进入,另外一端出去。



而今天主要介绍双端队列( Deque ), Deque 是双端队列的线性数据结构, 可以在两端进行插入和删除操作,效果如下所示。



双端队列( Deque )的子类分别是 ArrayDequeLinkedListArrayDeque 基于数组实现的双端队列,而 LinkedList 基于双向链表实现的双端队列,它们的继承关系如下图所示。



接口 DequeQueue 提供了两套 API ,存在两种形式,分别为抛出异常,和不抛出异常,返回一个特殊值 null 或者布尔值 ( true | false )。



























操作类型抛出异常返回特殊值
插入addXXX(e)offerXXX(e)
移除removeXXX()pollXXX()
查找element()peekXXX()

ArrayDeque


ArrayDeque 是基于(循环)数组的方式实现双端队列,数组初始化容量为 16(JDK 8),结构图如下所示。



ArrayDeque 具有以下特点:



  • 因为双端队列只能在头部和尾部插入或者删除元素,所以时间复杂度为 O(1),但是在扩容的时候需要批量移动元素,其时间复杂度为 O(n)

  • 扩容的时候,将数组长度扩容为原来的 2 倍,即 n << 1

  • 数组采用连续的内存地址空间,所以查询的时候,时间复杂度为 O(1)

  • 它是非线程安全的集合


LinkedList


LinkedList 基于双向链表实现的双端队列,它的结构图如下所示。



LinkedList 具有以下特点:



  • LinkedList 是基于双向链表的结构来存储元素,所以长度没有限制,因此不存在扩容机制

  • 由于链表的内存地址是非连续的,所以只能从头部或者尾部查找元素,查询的时间复杂为 O(n),但是 JDK 对 LinkedList 做了查找优化,当我们查找某个元素时,若 index < (size / 2),则从 head 往后查找,否则从 tail 开始往前查找 , 但是我们在计算时间复杂度的时候,常数项可以省略,故时间复杂度 O(n)


Node<E> node(int index) {
// size >> 1 等价于 size / 2
if (index < (size >> 1)) {
// form head to tail
} else {
// form tail to head
}
}


  • 链表通过指针去访问各个元素,所以插入、删除元素只需要更改指针指向即可,因此插入、删除的时间复杂度 O(1)

  • 它是非线程安全的集合


最后汇总一下 ArrayDequeLinkedList 的特点如下所示:































集合类型数据结构初始化及扩容插入/删除时间复杂度查询时间复杂度是否是线程安全
ArrqyDeque循环数组初始化:16
扩容:2 倍
0(n)0(1)
LinkedList双向链表0(1)0(n)

为什么 ArrayDeque 比 LinkedList 快


了解完数据结构特点之后,接下来我们从两个方面分析为什么 ArrayDeque 作为队列使用时可能比 LinkedList 快。




  • 从速度的角度:ArrayDeque 基于数组实现双端队列,而 LinkedList 基于双向链表实现双端队列,数组采用连续的内存地址空间,通过下标索引访问,链表是非连续的内存地址空间,通过指针访问,所以在寻址方面数组的效率高于链表。




  • 从内存的角度:虽然 LinkedList 没有扩容的问题,但是插入元素的时候,需要创建一个 Node 对象, 换句话说每次都要执行 new 操作,当执行 new 操作的时候,其过程是非常慢的,会经历两个过程:类加载过程 、对象创建过程。




    • 类加载过程



      • 会先判断这个类是否已经初始化,如果没有初始化,会执行类的加载过程

      • 类的加载过程:加载、验证、准备、解析、初始化等等阶段,之后会执行 <clinit>() 方法,初始化静态变量,执行静态代码块等等




    • 对象创建过程



      • 如果类已经初始化了,直接执行对象的创建过程

      • 对象的创建过程:在堆内存中开辟一块空间,给开辟空间分配一个地址,之后执行初始化,会执行 <init>() 方法,初始化普通变量,调用普通代码块






接下来我们通过 算法动画图解 | 被 "废弃" 的 Java 栈,为什么还在用 文章中 LeetCode 算法题:有效的括号,来验证它们的执行速度,以及在内存方面的开销,代码如下所示:


class Solution {
public boolean isValid(String s) {

// LinkedList VS ArrayDeque

// Deque<Character> stack = new LinkedList<Character>();
Deque<Character> stack = new ArrayDeque<Character>();

// 开始遍历字符串
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
// 遇到左括号,则将其对应的右括号压入栈中
if (c == '(') {
stack.push(')');
} else if (c == '[') {
stack.push(']');
} else if (c == '{') {
stack.push('}');
} else {
// 遇到右括号,判断当前元素是否和栈顶元素相等,不相等提前返回,结束循环
if (stack.isEmpty() || stack.poll() != c) {
return false;
}
}
}
// 通过判断栈是否为空,来检查是否是有效的括号
return stack.isEmpty();
}
}

正如你所看到的,核心算法都是一样的,通过接口 Deque 来访问,只是初始化接口 Deque 代码不一样。


// 通过 LinkedList 初始化     
Deque<Character> stack = new LinkedList<Character>();

// 通过 ArrayDeque 初始化
Deque<Character> stack = new ArrayDeque<Character>();


结果如上所示,无论是在执行速度、还是在内存开销上 ArrayDeque 的性能都比 LinkedList 要好。





如果有帮助 点个赞 就是对我最大的鼓励


代码不止,文章不停


欢迎关注公众号:ByteCode,持续分享最新的技术







最后推荐长期更新和维护的项目:




  • 个人博客,将所有文章进行分类,欢迎前去查看 hi-dhl.com




  • KtKit 小巧而实用,用 Kotlin 语言编写的工具库,欢迎前去查看 KtKit




  • 计划建立一个最全、最新的 AndroidX Jetpack 相关组件的实战项目 以及 相关组件原理分析文章,正在逐渐增加 Jetpack 新成员,仓库持续更新,欢迎前去查看 AndroidX-Jetpack-Practice




  • LeetCode / 剑指 offer / 国内外大厂面试题 / 多线程 题解,语言 Java 和 kotlin,包含多种解法、解题思路、时间复杂度、空间复杂度分析






近期必读热门文章



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

Kotlin常用的by lazy你真的了解吗

前言 在使用Kotlin语言进行开发时,我相信很多开发者都信手拈来地使用by或者by lazy来简化你的属性初始化,但是by lazy涉及的知识点真的了解吗 假如让你实现这个功能,你会如何设计。 正文 话不多说,我们从简单的属性委托by来说起。 委托属性 什...
继续阅读 »

前言


在使用Kotlin语言进行开发时,我相信很多开发者都信手拈来地使用by或者by lazy来简化你的属性初始化,但是by lazy涉及的知识点真的了解吗 假如让你实现这个功能,你会如何设计。


正文


话不多说,我们从简单的属性委托by来说起。


委托属性


什么是委托属性呢,比较官方的说法就是假如你想实现一个比较复杂的属性,它们处理起来比把值保存在支持字段中更复杂,但是却不想在每个访问器都重复这样的逻辑,于是把获取这个属性实例的工作交给了一个辅助对象,这个辅助对象就是委托。


比如可以把这个属性的值保存在数据库中,一个Map中等,而不是直接调用其访问器。


看完这个委托属性的定义,假如你不熟悉Kotlin也可以理解,就是我这个类的实例由另一个辅助类对象来提供,但是这时你可能会疑惑,上面定义中说的支持字段和访问器是什么呢,这里顺便给不熟悉Kotlin的同学普及一波。


Java的属性

当你定义一个Java类时,在定义字段时并不是所有字段都是属性,比如下面代码:


//Java类
public class Phone {

//3个字段
private String name;
private int price;
private int color;

//name字段访问器
private String getName() {
return name;
}

private void setName(String name){
this.name = name;
}

//price字段访问器
private int getPrice() {
return price;
}

private void setPrice(int price){
this.price = price;
}
}

上面我在Phone类中定义了3个字段,但是只有name和price是Phone的属性,因为这2个字段有对应的get和set,也只有符合有getter和setter的字段才叫做属性。


这也能看出Java类的属性值是保存在字段中的,当然你也可以定义setXX函数和getXX函数,既然XX属性没有地方保存,XX也是类的属性。


Kotlin的属性

而对于Kotlin的类来说,属性定义就非常简单了,比如下面类:


class People(){
val name: String? = null
var age: Int? = null
}

在Kotlin的类中只要使用val/var定义的字段,它就是类的属性,然后会自带getter和setter方法(val属性相当于Java的final变量,是没有set方法的),比如下面:


val people = People()
//调用name属性的getter方法
people.name
//调用age属性的setter方法
people.age = 12

这时就有了疑问,为什么上面代码定义name时我在后面给他赋值了即null值,和Java一样不赋值可以吗 还有个疑问就是在Java中是把属性的值保存在字段中,那Kotlin呢,比如name这个属性的值就保存给它自己吗


带着问题,我们继续分析。


Kotlin属性访问器

前面我们可知Java中的属性是保存在字段中,或者不要字段,其实Kotlin也可以,这个就是给属性定义自定义setter方法和getter方法,如下代码:


class People(){
val name: String? = null
var age: Int = 0
//定义了isAbove18这个属性
var isAbove18: Boolean = false
get() = age > 18
}

比如这里自定义了get访问器,当再访问这个属性时,便会调用其get方法,然后进行返回值。


Kotlin属性支持字段field

这时一想那Kotlin的属性值保存在哪里呢,Kotlin会使用一个field的支持字段来保存属性。如下代码:


class People{
val name: String? = null
var age: Int = 0
//返回field的值
get() = field
//设置field的值
set(value){
Log.i("People", "旧值是$field 新值是$value ")
field = value
}

var isAbove18: Boolean = false
get() = age > 18
}

可以发现每个属性都会有个支持字段field来保存属性的值。


好了,为了介绍为什么Kotlin要有委托属性这个机制,假如我在一个类中,需要定义一个属性,这时获取属性的值如果使用get方法来获取,会在多个类都要写一遍,十分不符合代码设计,所以委托属性至关重要。


委托属性的实现


在前面说委托属性的概念时就说了,这个属性的值需要由一个新类来代理处理,这就是委托属性,那我们也可以大概猜出委托属性的底层逻辑,大致如下面代码:


class People{
val name: String? = null
var age: Int = 0
val isAbove18: Boolean = false
//email属性进行委托,把它委托给ProduceEmail类
var email: String by ProduceEmail()
}

假如People的email属性需要委托,上面代码编译器会编译成如下:


class People{
val name: String? = null
var age: Int = 0
val isAbove18: Boolean = false
//委托类的实例
private val productEmail = ProduceEmail()
//委托属性
var email: String
//访问器从委托类实例获取值
get() = productEmail.getValue()
//设置值把值设置进委托类实例
set(value) = productEmail.setValue(value)
}

当然上面代码是编译不过的,只是说一下委托的实现大致原理。那假如想使ProduceEmail类真的具有这个功能,需要如何实现呢。


by约定

其实我们经常使用 by 关键字它是一种约定,是对啥的约定呢 是对委托类的方法的约定,关于啥是约定,一句话说明白就是简化函数调用,具体可以查看我之前的文章:


# Kotlin invoke约定,让Kotlin代码更简洁


那这里的by约定简化了啥函数调用呢 其实也就是属性的get方法和set方法,当然委托类需要定义相应的函数,也就是下面这2个函数:


//by约定能正常使用的方法
class ProduceEmail(){

private val emails = arrayListOf("111@qq.com")

//对应于被委托属性的get函数
operator fun getValue(people: People, property: KProperty<*>): String {
Log.i("zyh", "getValue: 操作的属性名是 ${property.name}")
return emails.last()
}

//对于被委托属性的get函数
operator fun setValue(people: People, property: KProperty<*>, s: String) {
emails.add(s)
}

}

定义完上面委托类,便可以进行委托属性了:


class People{
val name: String? = null
var age: Int = 0
val isAbove18: Boolean = false
//委托属性
var email: String by ProduceEmail()
}

然后看一下调用地方:


val people = People()
Log.i("zyh", "onCreate: ${people.email}")
people.email = "222@qq.com"
Log.i("zyh", "onCreate: ${people.email}")

打印如下:


image.png


会发现每次调用email属性的访问器方法时,都会调用委托类的方法。


关于委托类中的方法,当你使用by关键字时,IDE会自动提醒,提醒如下:


image.png


比如getValue方法中的参数,第一个就是接收者了,你这个要委托的属性是哪个类的,第二个就是属性了,关于KProperty不熟悉的同学可以查看文章:


# Kotlin反射全解析3 -- 大展身手的KProperty


它就代表这属性,可以调用其中的一些方法来获取属性的信息。


而且方法必须使用operator关键字修饰,这是重载操作符的必须步骤,想使用约定,就必须这样干。


by lazy的实现


由前面明白了by的原理,我们接着来看一下我们经常使用的by lazy是个啥,直接看代码:


//这里使用by lazy惰性初始化一个实例
val instance: DataStoreManager by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
DataStoreManager(store) }

比如上面代码,使用惰性初始化初始了一个实例,我们来看一下这个by的实现:


//by代码
@kotlin.internal.InlineOnly
public inline operator fun <T> Lazy<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value

哦,会发现它是Lazy类的一个扩展函数,按照前面我们对by的理解,它就是把被委托的属性的get函数和getValue进行配对,所以可以想象在Lazy< T >类中,这个value便是返回的值,我们来看一下:


//惰性初始化类
public interface Lazy<out T> {

//懒加载的值,一旦被赋值,将不会被改变
public val value: T

//表示是否已经初始化
public fun isInitialized(): Boolean
}

到这里我们注意一下 by lazy的lazy,这个就是一个高阶函数,来创建Lazy实例的,lazy源码:


//lazy源码
public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
when (mode) {
LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
}

这里会发现第一个参数便是线程同步的模式,第二个参数是初始化器,我们就直接看一下最常见的SYNCHRONIZED的模式代码:


//线程安全模式下的单例
private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
private var initializer: (() -> T)? = initializer
//用来保存值,当已经被初始化时则不是默认值
@Volatile private var _value: Any? = UNINITIALIZED_VALUE
//锁
private val lock = lock ?: this

override val value: T
//见分析1
get() {
//第一次判空,当实例存在则直接返回
val _v1 = _value
if (_v1 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST")
return _v1 as T
}
//使用锁进行同步
return synchronized(lock) {
//第二次判空
val _v2 = _value
if (_v2 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST") (_v2 as T)
} else {
//真正初始化
val typedValue = initializer!!()
_value = typedValue
initializer = null
typedValue
}
}
}

//是否已经完成
override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE

override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."

private fun writeReplace(): Any = InitializedLazyImpl(value)
}

分析1:这个单例实现是不是有点眼熟,没错它就是双重校验锁实现的单例,假如你对双重校验锁的实现单例方式还不是很明白可以查看文章:


# Java双重校验锁单例原理 赶快看进来


这里实现懒加载单例的模式就是双重校验锁,2次判空以及volatile关键字都是有作用的,这里不再赘述。


总结


先搞明白by的原理,再理解by lazy就非常好理解了,虽然这些关键字我们经常使用,不过看一下其源码实现还是很舒爽的,尤其是Kotlin的高阶函数的一些SDK写法还是很值的学习。


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

Flutter通用页面Loading组件

前沿 页面通用Loading组件是一个App必不可少的基础功能,之前只开发过Android原生的页面Loading,这次就按原生的逻辑再开发一个Flutter的Widget,对其进行封装复用 我们先看下效果: 原理 状态 一个通用的页面加载Loading组件...
继续阅读 »

前沿


页面通用Loading组件是一个App必不可少的基础功能,之前只开发过Android原生的页面Loading,这次就按原生的逻辑再开发一个Flutter的Widget,对其进行封装复用


我们先看下效果:
bloggif_60b603bed8875.gif


原理


状态


一个通用的页面加载Loading组件应该具备以下几种状态:


IDLE 初始化

Idle状态,此时的组件还只是初始化


LOADING 加载中

Loading状态,一般在网络请求或者耗时加载数据时调用,通用显示的是一个progress或者自定义的帧动画


LOADING_SUCCESS

LoadingSuccess加载成功,一般在网络请求成功后调用,并将需要展示的页面展示出来


LOADING_SUCCESS_BUT_EMPTY

页面加载成功但是没有数据,这种情况一般是发起列表数据请求但是没有数据,通常我们会展示一个空数据的页面来提醒用户


NETWORK_BLOCKED

网络错误,一般是由于网络异常、网络请求连接超时导致。此时我们需要展示一个网络错误的页面,并且带有重试按钮,让用户重新发起请求


ERROR

通常是接口错误,这种情况下我们会根据接口返回的错误码或者错误文本提示用户,并且也有重试按钮


/// 状态枚举
enum LoadingStatus {
idle, // 初始化
loading, // 加载中
loading_suc, // 加载成功
loading_suc_but_empty, // 加载成功但是数据为空
network_blocked, // 网络加载错误
error, // 加载错误
}

点击事件回调


当网络异常或者接口报错时,会显示错误页面,并且提供重试按钮,让用户点击重新请求。基于这个需求,我们还需要提供点击重试后的事件回调让业务可以处理重新请求。


 /// 定义点击事件
typedef OnTapCallback = Function(LoadingView widget);

提示文案


提供提示文案的自定义,方便业务根据自己的需求展示特定的提示文案


代码实现


根据上面的原理来实现对应的代码



  1. 构造方法


 /// 构造方法
LoadingView({
Key key,
@required this.child, // 需要加载的Widget
@required this.todoAfterError, // 错误点击重试
@required this.todoAfterNetworkBlocked, // 网络错误点击重试
this.networkBlockedDesc = "网络连接超时,请检查你的网络环境",
this.errorDesc = "加载失败",
this.loadingStatus = LoadingStatus.idle,
}) : super(key: key);


  1. 根据不同的Loading状态展示对应的Widget



  • 其中idle、success状态直接展示需要加载的Widget(这里也可以使用渐变动画进行切换过度)


 ///根据不同状态展示不同Widget
Widget _buildBody() {
switch (widget.loadingStatus) {
case LoadingStatus.idle:
return widget.child;
case LoadingStatus.loading:
return _buildLoadingView();
case LoadingStatus.loading_suc:
return widget.child;
case LoadingStatus.loading_suc_but_empty:
return _buildLoadingSucButEmptyView();
case LoadingStatus.error:
return _buildErrorView();
case LoadingStatus.network_blocked:
return _buildNetworkBlockedView();
}
return widget.child;
}


  1. buildLoadingView,这里简单用了系统的CircularProgressIndicator,也可以自己显示帧动画


  /// 加载中 View
Widget _buildLoadingView() {
return Container(
width: double.maxFinite,
height: double.maxFinite,
child: Center(
child: SizedBox(
height: 22.w,
width: 22.w,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(AppColors.primaryBgBlue),
),
),
),
);
}


  1. 其他提示页面,这里做了一个统一的封装



/// 编译通用页面
Container _buildGeneralTapView({
String url = "images/icon_network_blocked.png",
String desc,
@required Function onTap,
}) {
return Container(
color: AppColors.primaryBgWhite,
width: double.maxFinite,
height: double.maxFinite,
child: Center(
child: SizedBox(
height: 250.h,
child: Column(
children: [
Image.asset(url,
width: 140.w, height: 99.h),
SizedBox(
height: 40.h,
),
Text(
desc,
style: AppText.gray50Text12,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
SizedBox(
height: 30.h,
),
if (onTap != null)
BorderRedBtnWidget(
content: "重新加载",
onClick: onTap,
padding: 40.w,
),
],
),
),
),
);
}

/// 加载成功但数据为空 View
Widget _buildLoadingSucButEmptyView() {
return _buildGeneralTapView(
url: "images/icon_empty.png",
desc: "暂无数据",
onTap: null,
);
}

/// 网络加载错误页面
Widget _buildNetworkBlockedView() {
return _buildGeneralTapView(
url: "images/icon_network_blocked.png",
desc: widget.networkBlockedDesc,
onTap: () {
widget.todoAfterNetworkBlocked(widget);
});
}

/// 加载错误页面
Widget _buildErrorView() {
return _buildGeneralTapView(
url: "images/icon_error.png",
desc: widget.errorDesc,
onTap: () {
widget.todoAfterError(widget);
});
}

使用


  Widget _buildBody() {
var loadingView = LoadingView(
loadingStatus: LoadingStatus.loading,
child: _buildContent(),
todoAfterNetworkBlocked: (LoadingView widget) {
// 网络错误,点击重试
widget.updateStatus(LoadingStatus.loading);
Future.delayed(Duration(milliseconds: 1000), () {
widget.updateStatus(LoadingStatus.error);
});
},
todoAfterError: (LoadingView widget) {
// 接口错误,点击重试
widget.updateStatus(LoadingStatus.loading);
Future.delayed(Duration(milliseconds: 1000), () {
// widget.updateStatus(LoadingStatus.loading_suc);
widget.updateStatus(LoadingStatus.loading_suc_but_empty);
});
},
);
Future.delayed(Duration(milliseconds: 1000), (){
loadingView.updateStatus(LoadingStatus.network_blocked);
});
return loadingView;
}

总结


至此已经完成了对整个Loading组件的封装,代码已上传Github


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

Flutter 外接纹理

背景 在Flutter开发中需要用到视频播放的功能,Flutter对视频播放的支持并不是很友好所以Google提供了TextureLayer让Flutter端能够使用原生端的渲染,这样我们原生端很多优秀的视频播放组件能够在Flutter程序上使用了 Textu...
继续阅读 »

背景


在Flutter开发中需要用到视频播放的功能,Flutter对视频播放的支持并不是很友好所以Google提供了TextureLayer让Flutter端能够使用原生端的渲染,这样我们原生端很多优秀的视频播放组件能够在Flutter程序上使用了


Texture的创建


Texture是Platform端创建的,创建是会生成一个textureId,textureId可以映射获取到Texture



  • Androaid端FlutterRenderer创建SurfaceTexture


  @Override
public SurfaceTextureEntry createSurfaceTexture() {
  Log.v(TAG, "Creating a SurfaceTexture.");
  //创建SurfaceTexture
  final SurfaceTexture surfaceTexture = new SurfaceTexture(0);
  surfaceTexture.detachFromGLContext();
  final SurfaceTextureRegistryEntry entry =
      new SurfaceTextureRegistryEntry(nextTextureId.getAndIncrement(), surfaceTexture);
  Log.v(TAG, "New SurfaceTexture ID: " + entry.id());
  // 映射 textureId/entry.id() 和 SurfaceTexture的关系
  registerTexture(entry.id(), entry.textureWrapper());
  return entry;
}


  • Flutter Engine中 platform_view_android_jni_impl.cc


static void RegisterTexture(JNIEnv* env,
                          jobject jcaller,
                          jlong shell_holder,
                          jlong texture_id,
                          jobject surface_texture) {
ANDROID_SHELL_HOLDER->GetPlatformView()->RegisterExternalTexture(
    static_cast<int64_t>(texture_id),                       //
    fml::jni::JavaObjectWeakGlobalRef(env, surface_texture) //
);
}


  • android_shell_holder.cc


fml::WeakPtr<PlatformViewAndroid> AndroidShellHolder::GetPlatformView() {
FML_DCHECK(platform_view_);
return platform_view_;
}


  • platform_view_android.cc


void PlatformViewAndroid::RegisterExternalTexture(
  int64_t texture_id,
  const fml::jni::JavaObjectWeakGlobalRef& surface_texture) {
  //AndroidExternalTextureGL 即 Texture
RegisterTexture(std::make_shared<AndroidExternalTextureGL>(
    texture_id, surface_texture, std::move(jni_facade_)));
}


  • platform_view.cc


void PlatformView::RegisterTexture(std::shared_ptr<flutter::Texture> texture) {
delegate_.OnPlatformViewRegisterTexture(std::move(texture));
}


  • shell.cc


// |PlatformView::Delegate|
void Shell::OnPlatformViewRegisterTexture(
  std::shared_ptr<flutter::Texture> texture) {
FML_DCHECK(is_setup_);
FML_DCHECK(task_runners_.GetPlatformTaskRunner()->RunsTasksOnCurrentThread());

task_runners_.GetRasterTaskRunner()->PostTask(
    [rasterizer = rasterizer_->GetWeakPtr(), texture] {
      if (rasterizer) {
        if (auto* registry = rasterizer->GetTextureRegistry()) {
          registry->RegisterTexture(texture);
        }
      }
    });
}


  • texture.cc


void TextureRegistry::RegisterTexture(std::shared_ptr<Texture> texture) {
if (!texture) {
  return;
}
mapping_[texture->Id()] = texture;
}

通过上述流程Flutter Engine层最终会把SurfaceTexture存到mapping_中


Texture的获取


Texture的获取是在Flutter端,通过textureId获取到mapping_中保存的Texture并且创建出一个TextureLayer映射到Flutter framework层



  • Flutter端TextureLayer


  @override
void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
  final Rect shiftedRect = layerOffset == Offset.zero ? rect : rect.shift(layerOffset);
  builder.addTexture(
    textureId,
    offset: shiftedRect.topLeft,
    width: shiftedRect.width,
    height: shiftedRect.height,
    freeze: freeze,
    filterQuality: filterQuality,
  );
}


  • SceneBuilder


  void _addTexture(double dx, double dy, double width, double height, int textureId, bool freeze,
    int filterQuality) native 'SceneBuilder_addTexture'; // 调用Engine中的方法


  • scene_builder.cc


void SceneBuilder::addTexture(double dx,
                            double dy,
                            double width,
                            double height,
                            int64_t textureId,
                            bool freeze,
                            int filterQualityIndex) {
auto sampling = ImageFilter::SamplingFromIndex(filterQualityIndex);
auto layer = std::make_unique<flutter::TextureLayer>(
    SkPoint::Make(dx, dy), SkSize::Make(width, height), textureId, freeze,
    sampling);
AddLayer(std::move(layer));
}


  • texture_layer.cc


//GPU线程绘制时会调用该方法
void TextureLayer::Paint(PaintContext& context) const {
TRACE_EVENT0("flutter", "TextureLayer::Paint");
FML_DCHECK(needs_painting(context));

//获取texture_registry中注册好的texture
std::shared_ptr<Texture> texture =
    context.texture_registry.GetTexture(texture_id_);
if (!texture) {
  TRACE_EVENT_INSTANT0("flutter", "null texture");
  return;
}
texture->Paint(*context.leaf_nodes_canvas, paint_bounds(), freeze_,
                context.gr_context, sampling_);
}

image-20220126174016419


Texture的使用


Texture是封装的TextureLayer,通过上述流程分析后再来使用TextureLayer就比较简单了,可以通过MethodChannel的方式让Platform端创建一个Texture,最终返回一个textureId到Flutter端,Flutter端通过textureId的映射获取到Flutter Engine层创建好的Texture并包装成一个TextureLayer返回到Flutter framework层。



  1. 创建MethodChannel

  2. 创建SurfaceTexture

  3. 获取textureId创建Texture


以下是以获取摄像头预览为例:



  • Android 端示例代码


import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.SurfaceTexture;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CaptureRequest;
import android.os.Handler;
import android.util.Log;
import android.view.Surface;


import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;

import java.util.Arrays;

import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.view.TextureRegistry;

public class MainActivity extends FlutterActivity {
  public static final String TAG = "MainActivity";
  MethodChannel channel;
  private Handler backgroundHandler;

  @Override
  public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
      super.configureFlutterEngine(flutterEngine);
      backgroundHandler = new Handler();
      channel = new MethodChannel(flutterEngine.getDartExecutor()
              .getBinaryMessenger(), "flutter/texture/channel");

      channel.setMethodCallHandler((call, result) -> {
          switch (call.method) {
              case "createTexture":
                  createTexture(flutterEngine,result);
                  break;
          }
      });
  }

  private void createTexture(FlutterEngine flutterEngine,MethodChannel.Result result) {
      CameraManager cameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
      if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
          result.success(-1);
          ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.CAMERA},1);
          return;
      }
      TextureRegistry.SurfaceTextureEntry entry =
              flutterEngine.getRenderer().createSurfaceTexture();
      SurfaceTexture surfaceTexture = entry.surfaceTexture();
      Surface surface = new Surface(surfaceTexture);
      try {
          cameraManager.openCamera(
                  "0",
                  new CameraDevice.StateCallback() {
                      @Override
                      public void onOpened(@NonNull CameraDevice device) {
                          result.success(entry.id());
                          CaptureRequest.Builder previewRequestBuilder = null;
                          try {
                              previewRequestBuilder = device.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
                              previewRequestBuilder.addTarget(surface);
                              startPreview(result,device,surface,previewRequestBuilder,backgroundHandler);
                          } catch (CameraAccessException e) {
                              e.printStackTrace();
                          }

                      }

                      @Override
                      public void onDisconnected(@NonNull CameraDevice camera) {
                           
                      }

                      @Override
                      public void onError(@NonNull CameraDevice cameraDevice, int errorCode) {
                          Log.i(TAG, "open | onError");
                          result.success(-1);
                      }
                  },
                  backgroundHandler);
      } catch (CameraAccessException e) {
          e.printStackTrace();
      }


  }

  private void startPreview(MethodChannel.Result result,CameraDevice device, Surface surface, CaptureRequest.Builder previewRequestBuilder, Handler backgroundHandler) {
      try {
          device.createCaptureSession(Arrays.asList(surface), new CameraCaptureSession.StateCallback() {
              @Override
              public void onConfigured(@NonNull CameraCaptureSession session) {
                  Log.i(TAG, "startPreview");
                  try {
                      session.setRepeatingRequest(previewRequestBuilder.build(),null,backgroundHandler );
                  } catch (CameraAccessException e) {
                      e.printStackTrace();
                  }
              }

              @Override
              public void onConfigureFailed(@NonNull CameraCaptureSession session) {
                  Log.i(TAG, "startPreview Failed");
              }
          },backgroundHandler);
      } catch (CameraAccessException e) {
          e.printStackTrace();
      }
  }
}


  • Flutter端示例代码


import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

MethodChannel methodChannel = MethodChannel('flutter/texture/channel');

void main() {
runApp(MyApp());
}

class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
  return MaterialApp(
    title: 'Flutter Demo',
    theme: ThemeData(
      primarySwatch: Colors.blue,
    ),
    home: MyHomePage(title: 'Flutter Demo Home Page'),
  );
}
}

class MyHomePage extends StatefulWidget {
MyHomePage({Key? key, required this.title}) : super(key: key);
 
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
int textureId = -1;

Future<void> _createTexture() async {
  print('textureId = $textureId');
  if (textureId < 0) {
    methodChannel.invokeMethod('createTexture').then((value) {
      textureId = value;
      setState(() {
        print('textureId ==== $textureId');
      });
    });
  }

}

@override
Widget build(BuildContext context) {

  return Scaffold(
    appBar: AppBar(
      title: Text(widget.title),
    ),
    body: Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          if (textureId > -1)
            Container(
              width: 300,
              height: 400,
              child: Texture(
                textureId: textureId,
              ),
            )
        ],
      ),
    ),
    floatingActionButton: FloatingActionButton(
      onPressed: _createTexture,
      tooltip: 'createTexture',
      child: Icon(Icons.add),
    ),
  );
}
}

上述代码在Android并未创建View,而是创建了SurfaceTexture与camera绑定后通过Texture的形式在Flutter端显示


Texture和PlatformView的区别


PlatformView是Flutter中嵌套Platform中的View,如:TextView。它们的区别在Texture是渲染层的东西,而PlatformView本质是一个View它拥有View所有的属性。


总结


本文通过源码分析以及简单的摄像头预览示例讲解了Flutter外接纹理的原理和使用方式,希望能够帮助部分刚接触Flutter开发的同学加深对Flutter外接纹理的认识。


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

【MPFlutter浅尝】使用flutter写一个微信小程序

前言:12份左右flutter发布2.8.0 flutter对于桌面端和web端的支持越来越完善 想着这玩意能不能写微信小程序呢 一搜还真有 由一兜糖团队开发的MPFlutter项目开源了 本着尝鲜的心态看了下已上线的一兜糖小程序 哎呀妈呀 感觉还不错的样子 ...
继续阅读 »

前言:12份左右flutter发布2.8.0 flutter对于桌面端和web端的支持越来越完善 想着这玩意能不能写微信小程序呢 一搜还真有 由一兜糖团队开发的MPFlutter项目开源了 本着尝鲜的心态看了下已上线的一兜糖小程序 哎呀妈呀 感觉还不错的样子 话不多嗦 vscode启动


MPFlutter项目地址


MPFlutter Gayhub地址


所以 我应该写个啥玩意项目好呢

开始我想的是仿站 或者把公司项目直接用MPFlutter写一遍小程序 这样我接口什么的都有现成的
但是我是一个Apex游戏玩家 我要干票大的 我看刑


第一步借鉴ui
打开WeGame 打开小黑盒
简单查看后先借鉴下小黑盒 ps:不得不说小黑盒数据非常全 基本所有Steam游戏都囊括了
开始设计ui
简单的画两笔 差不多就这样


7f6f64b563b2a3eaf18908190bf22a8.jpg


第二步 收集数据
apex英雄这个游戏是一个大逃杀类型游戏 数十个英雄 二三十把枪械 数以千记的皮肤 边框 动作 数据太多自己搜集太麻烦这部分 但是小黑盒的数据非常全 嘿嘿嘿
简简单单写个后台填上数据


第三步 开始绘制前端
因为我flutter写的还行但是我从未写过微信小程序。但是好在MPFLutter的文档写的很详细。属于我奶奶来了都能把环境搭好的那种


mpflutter 应该是类似mpvue的类型。是一个跨平台 Flutter 开发框架,可用于各种小程序、H5、原生应用开发。开发者可以通过 Dart 语言开发 App,一套代码同时在多个平台运行 微信 京东 钉钉等等


Apex项目启动

环境搭建


windows配置环境


macOS中配置环境


这里部分和flutter配置环境是一样的
。但是我们需要


克隆模板工程

git clone https://github.com/mpflutter/mpflutter_template.git apex_wechat


安装依赖

./mpflutter packages get



注意,这里用的是 ./mpflutter 而不是 flutter



这里模板工程克隆下来之后按F5就可以成功运行 你可以看到演示的demo 。这里继续看文档 要把模板工程变成这里的项目。我们需要


初始化应用信息

dart scripts/help.dart


这将出现以下信息,help.dart 是 MPFlutter 的帮助中心,可帮助你完成应用的初始化和构建工作。


image.png


我们选择初始化 MPFlutter 模板工程,并根据提示输入工程名称、输出目标。




  • 是否移除模板工程自带的 Git 源? (y/N)



    • 对于新克隆的模板工程,选 y 即可,移除自带的 Git 源,后续可以添加自己的 Git 源。




  • 请输入工程名称,合法字符为全小写英文和下划线:



    • 输入一个合法的工程名称,如 awesome_project,这将会同步修改 pubspec.yaml 中的 name 值。




  • 该工程需要输出到 Web 吗?(如果选择否,将删除 Web 目录。) (y/N)



    • 如果你不需要输出到 Web (HTML5) 可以选择否,一般情况下,我们会选 y 保留该目标。




  • 该工程需要输出到微信小程序吗?(如果选择否,将删除 weapp 目录。) (y/N)



    • 如果你不需要输出到微信小程序可以选择否。




......


在命令行执行 ./mpflutter packages get


这里你的项目就成功跑起来了


然后就开始我们的apex启动


目前MPFlutter支持的第三库有GetX Provider 富文本 Bloc等等 我这里简单使用了Provider 和GetX
用法和写FLutter项目没有差别 正常引入后./mpflutter packages get就行


提一嘴就是Flutter很多组件都是经过MPFlutter二次封装过的 material组件包含的是不可用的 MPFlutter提供了大部分替换类的组件,但是还是有一小部分需要自己实现。就意味这部分ui需要自己写 pub.dev上的第三方ui组件差不多也是不能用的 算了 写就写吧 又不是不能看 丑就丑点吧


花了大概两个星期总算是把前后数据都通了完成了百科 商店 头条页面 上线的话目前还没有想法 过完年不懒了再说 现在我只想打Apex 效果图如下


image.png


image.png


image.png


具体代码地址 小孩子不懂事写着玩的 轻点喷


看到这里可能有小伙伴要问了 我应该如何调用原生小程序的APi 登录授权 查看大图等等。不用担心 MPFlutter早已经把这部分的问题解决了 提供了所有的原生小程序的APi调用 具体说明


总的来说经过使用MPFlutter写了一个微信小程序感觉就是有点回归写html的时候的样子 自己想要的ui效果得自己写 但是像一些基础组件还是可以用的 包括我在使用过程中遇到各种问题也一直在请教MPFLutter作者 @PonyCui。大佬也一直很有耐心的解答我的各种奇葩问题和解决我的奇葩需求。
再次感谢 @PonyCui的开源 希望flutter社区越来越好 一统我的技术栈


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

办一届奥运会,到底赚不赚钱?

2月4日,北京冬奥会盛大开幕。尽管有疫情影响,91个国家和地区的代表团健儿仍然将给全世界人民带来一场场精彩的冰雪赛事。不过,也是因为疫情,这次冬奥会将原计划通过公开销售门票的方式调整为定向组织观众现场观赛。▲2022年北京冬奥会会徽 ©Getty Images...
继续阅读 »

pic_f6b9f0fc.png

2月4日,北京冬奥会盛大开幕。

尽管有疫情影响,91个国家和地区的代表团健儿仍然将给全世界人民带来一场场精彩的冰雪赛事。

不过,也是因为疫情,这次冬奥会将原计划通过公开销售门票的方式调整为定向组织观众现场观赛。

pic_f1c3b575.png

▲2022年北京冬奥会会徽 ©Getty Images

客观上说,这必然会减少这次冬奥会的门票收入。

举办奥运会的价值谁都知道,对于任何一个国家和城市来说,奥运会都有无法用金钱衡量的重要意义。

可大家同样还是会好奇,举办奥运会到底赚不赚钱呢?

事实上,自从1896年第一届雅典奥运会开幕,直到1984年的洛杉矶奥运会前,办奥运会的国家并不赚钱,也压根没想到要赚钱。

01 早期的奥运会纯属“赔本赚吆喝”

说起来,这与奥运会创始人顾拜旦所提出的奥运会三大原则“非职业化、非政治化、非商业化”有关。

pic_54b28a3b.png

这三条原则也是奥运精神的重要体现,并得到全世界的公认。

既然办奥运会是全世界人民体育和文化事业的盛事,谈钱那就俗了。

一个国家承办奥运会,不仅能在举办期间得到全世界的关注,而且还是综合国力的体现,即便不赚钱,也同样让很多国家为申办奥运而挤破头。

钱不是万能的,可没钱是万万不能的。

不管怎么说,如此关系重大的体育赛事,总不可能让参赛国自行解决费用吧?况且不是说承办国都是实力雄厚,财大气粗么?

因此,虽说国际奥委会并没有明确说明奥运会期间的选手食宿、交通等费用到底该由谁承担,可按照约定俗成的规矩,无论是夏季奥运会还是冬季奥运会,参赛运动员的所有费用基本都由承办国承担。

pic_42793fbf.png

▲国际奥委会(IOC)展示的“奥运会支出账目”

奥运会举办期间,承办国大多数的收入几乎都是微不足道的门票收入,这点钱和兴建场馆,还要对参赛国选手包吃包住等费用来说,几乎可以忽略不计。

虽然举办奥运不是个赚钱的买卖,可毕竟是挺有面子的国际重大赛事,更是申办国经济实力的重要体现。经济不发达的国家就是想申请也没戏。

早期奥运会承办国为解决资金,基本就三个套路:政府拨款、发行彩票以及民间捐款。

可因为后两项资金来源实在不稳定,加上募集的钱也杯水车薪,最后几乎都是承办国政府买了单。

虽然承办国也有电视转播权收入,可因那时全世界的电视普及率实在太低,这点钱也只是杯水车薪。

说到底,对早期奥运申办国来说,办奥运就是赔本赚吆喝。

可问题是,很多举办奥运会的国家是“申奥一时爽,赛后火葬场”。

由于举办奥运纯属只有投入没有盈利的事情,使得很多奥运举办国在奥运会后背了一屁股债。

比如1972年的德国慕尼黑奥运会耗费10亿美元,最后亏损6亿美元,中间还闹出11名以色列运动员被恐怖分子杀害的严重恐怖事件。

而1976年的加拿大的蒙特利尔更惨,总投资20亿美元,结果亏了10亿美元。不仅导致蒙特利尔市政府几近破产,还让当地300万市民背上了20多年的债务,还由此诞生了“蒙特利尔陷阱”这样的专有名词。

pic_9fb12d68.png

随后的1980年莫斯科奥运会,主办国投资了90亿美元,收入基本为零,亏大发了。

这说的还是夏季奥运会,其实办一次冬奥会也不容易。

同样是1980年,这年的冬奥会是在纽约普莱西湖镇举办。

其中最为人熟知的故事是东道主美国冰球队面对强大的苏联冰球队,以4:3反败为胜,打破了苏联自1964年来垄断冰球冠军的局面。

只是,本次冬奥会最终使得普莱西德湖小镇欠下800多万美元债务,让仅有3000人的小镇实在不堪重负。幸好所在的纽约州最终伸出援手,代其支付了全部债务。

不过,1980年的纽约普莱西湖冬奥会最值得一提的是,距首次参加第10届奥运会48年后,中国奥运代表团首次出现在奥运会的赛场上。

pic_d44486ef.png

▲1980年纽约普莱西湖冬奥会是新中国成立后首次参加奥运会

只是因为中国队的成绩不是很理想,宣传较少,使得很多人都误以为1984年的洛杉矶夏季奥运会才是中国奥运代表团的“首秀”。

正是因为无论夏季奥运会还是冬季奥运会都不赚钱,这之后很多国家都在申办奥运变得畏首畏尾,根本不想接手这个“烫手山芋”。

1984年的美国洛杉矶申奥成功后,美国国内压根就不感冒。

洛杉矶所在的加州政府直接立法规定,禁止洛杉矶奥运会动用任何财政收入举办赛事。

民众也自发上街游行,表示“休想动用我们一分钱的税金来办奥运”。

至于发行彩票?想都别想,在加州这属于违法行为。

当时的美国总统里根态度更狠:要钱没有,得洛杉矶市政府自己想办法。

要钱没钱,民众还十分反感举办奥运会,这让洛杉矶奥运会筹备组急得团团转。

02 奥运会变成了赚钱的买卖

官方没辙,那就只好从民间想办法。

被逼无奈的洛杉矶奥运会筹备组经人推荐,从民间找来一位商人负责筹款事宜。

这个人就是彼得·尤伯罗斯,当时是一家大型旅游公司的老板。

pic_6f22f85e.png

▲将洛杉矶奥运会“点石成金”的商人彼得·尤伯罗斯

洛杉矶奥运资金筹委会刚成立时,就尤伯罗斯一个人。亏得他率先捐出了100美元,才算打破筹委会账目的“零记录”。

尤伯罗斯给出的对策就是“赞助”。

本来赞助这回事对奥运会并不是新鲜事,各届都有赞助商。哪怕亏到家的莫斯科奥运会还拉到200多家赞助商。

可赞助商多是多,真正赞助的总额并不多。

由于之前奥运组委会不懂管理,加上企业杀价,每次拿到的赞助款只有区区数百万美元。

尤伯罗斯上任后,当即改变了奥运会的赞助策略。

他规定奥运会每个项目只能有唯一赞助商,而且全部项目只允许30家赞助企业。

不仅如此,尤伯罗斯还规定,想要赞助,出资必须在400万美元起,低于这个数字免谈。

总结下来,尤伯罗斯其实就是利用奥运会的重要性玩起了“稀缺”牌,搞了个饥饿营销。

为了让更多商家心甘情愿入局,他与洛杉矶奥运会筹备委员会共同提供了一份阵容强大的参赛国名单,其中就包括派出300多人的中国奥运代表团。

也是在这届奥运会上,射击选手许海峰彻底打破了中国奥运金牌史上的“零的纪录”。

pic_8f62fc6c.png

当时国际奥委会成员有159个,最后宣布参加洛杉矶奥运会的有140个国家和地区,远远超过了以往任何一届的规模。

如此盛大的场面,不能不让商家们心动,他们为了广告位展开激烈竞争,这让洛杉矶奥运会喜滋滋地赚了个盆满钵满。

比如当时的可口可乐为了击败百事可乐,砸了1260万美元,几乎是当时可口可乐全年广告投入的十分之一;日本富士为了抢占美国市场,挑战行业巨头柯达,拿出了700万美元。

而这些广告商全部都成了日后奥运会最重要的赞助商,为自己赚足了眼球。

尤伯罗斯开创的赞助模式也成为奥运会后来最主要的盈利手段,衍生出包括top计划、组委会合作伙伴、赞助商、供应商、特许经营企业等盈利模式。

尤伯罗斯的“吸金”计划到这里就结束了么?并没有。

尤伯罗斯还遇到了好时机,当时电视已在全世界得到普及。他顺势又搞出个电视转播权竞标,想投标得先交75万美元的保证金。

最后,美国五大电视广播巨头中的美国广播公司(ABC)以2.25亿美元拿下本次奥运会的转播权,也再次为奥运会开辟了一项新的盈利项目。

pic_d48887f5.png

▲历届奥运会的电视传播权收入

由于洛杉矶奥运会越来越多的参赛国加入,很多人都希望现场第一时间能见证世界纪录的诞生,比赛门票开始金贵起来。

洛杉矶奥运会随即宣布,本次门票的价格会根据赛事不同,将为50-200美元不等,任何人凭票入场,美国总统也不例外。

随后,热门赛事的门票果真卖到这样的价格,热门赛事还被“黄牛”炒到数百美元一张。

pic_b29f6930.png

▲1984年的洛杉矶奥运会打破了办奥运只会赔钱的历史

不用说,这背后还是尤伯罗斯的操作。

洛杉矶奥运会在各项赚钱的项目上一顿操作猛如虎,不仅结束了办奥运会赔钱的历史,还赚了2.5亿美元。

1984年,美国《时代》周刊将尤伯罗斯评选为“1984年最杰出人物”,而他正式开创了奥运动会的商业化运营模式,使各国再次开始争相申办奥运会。

03 真成了赚钱的买卖

因为有了洛杉矶奥运会这样现成的案例,后来几届奥运会几乎都是“照葫芦画瓢”,均取得了不错的经济效益,

这其中不仅有不断提升的广告赞助费用,连电视传播费用也在逐年增长。

根据国际奥委会公布数据,从1993-2012年,奥运电视转播授权费已从12.5亿涨到了38.50亿美元。

根据后来各国公布的数据,1988年的汉城奥运会总投资40亿美元,通过企业赞助,电视转播授权等方式,盈利4.97亿美元,也是首次政府举办奥运会盈利。

1992年,巴塞罗那奥运会总投资达到了96亿美元,虽然直接赛事盈利只有4000万美元,可奥运会后给西班牙和巴塞罗那带来了260亿美元的经济效益。

pic_8c5d6733.png

▲今天风光秀美、基础建设完善的巴塞罗那市要拜当年举办的奥运会所赐

申办奥运之前,巴塞罗那的城市十分破旧,基础设施也很不完善。

正是借助奥运会的契机,巴塞罗那市将90%的支出用在了基建、电信以及环境综合整治上。

奥运会最终让巴塞罗那焕然一新:其中道路设施增加了15%;污水处理系统增加了17%;绿化带和海滨旅游区增加了78%;人工湖和喷泉增加了268%。

旧貌换新颜的巴塞罗那也成为举世闻名的国际旅游城市,旅游收入自此成为该市最主要的财政收入来源。

同样,1996年亚特兰大奥运会算下来也只盈利1000万美元,却创造了50亿美元的经济效益;2012年伦敦奥运会总投资在100亿英镑(约合135亿美元),但是受到奥运经济的带动,整体收益达到210亿英镑(约合284亿美元)。

所谓“奥运经济”是指承办国在奥运筹备以及举办期间一切与之相关的经济活动,也是承办国最重要的经济来源。

换句话说,奥运会的收入已不是单纯从赛事结束就测算,更是从更为长远的承办国整体经济发展衡量。

拿1964年日本东京奥运会来说,由于当时办奥运还是“赔钱”项目,奥运结束后的账目自然是亏的。可靠着奥运带来的旅游和外资项目收入,日本当年GDP的增幅达到9%。

pic_9289de77.png

说来说去,大家肯定最关心2008年北京奥运会到底是赚是赔。

2009年6月19日,国家审计署公布了北京奥运会财务收支跟踪审计结果显示“北京奥组委收入将达到205亿元,较预算增加8亿元;支出将达到193.43亿元,较预算略有增加;收支结余将超过10亿元”。

不过,这只是经济学上的直接成本和收入。

而间接成本根据当时有关媒体报道,为筹备北京奥运会,中国差不多总投入近3000亿元,其中2800亿大部分用在了城市基建上。

pic_6cd57c45.png

根据北京申奥报告财政预算和北京“十五”计划的数据显示,北京奥运筹备期间,用于城市基础设施建设1800亿元:900亿元用于修建地铁、轻轨、高速公路、机场等;450亿元用于环境治理;300亿元用于信息化建设;其余150亿元将用于水电气热等生活设施的建设和改造。

这些钱也不是单单为了举办奥运会而花。

实际按照有关部门规划,无论是否举办奥运,这笔经费都最终要花在改善相关民生的项目上。

与此同时,北京奥运还加速了京津冀主要城市间的交通建设,形成了30分钟经济圈。

其中,我国首条时速超过350公里/小时的高速铁路——京津城际铁路就是在这期间建成的。

pic_7c3ffcac.png

▲北京奥运带动了京津冀主要城市进入1小时交通圈

从客观角度说,举办奥运会对承办国的经济确实会起到积极的推进作用。但是各国经济发展的趋势主要还是取决于经济的基本面,奥运会对经济体量较大的国家影响并不会十分明显。

中国自从北京奥运会之后经济增速一直保持在10%左右,当然,这是中国多年经济发展的成果,并不只是奥运会单一的促进作用。

04 举办奥运会的意义,绝非只是为了赚钱

一届奥运会不仅考验一个国家的综合管理和运营能力,难以预料的政治和疫情因素也会让承办国欲哭无泪。

比如2004年的雅典奥运会,原本预算是50亿美元。因对间接成本估计不足,加上当时恐怖事件频发,雅典方面不得不在安保等环节上大幅增加投入,使得实际支出达到100多亿美元。

而雅典赛事收入仅为20亿美元,加之许多比赛场馆赛后也没能充分利用,最终导致巨额亏损,还拖累了希腊经济。

2016年的巴西里约热内卢奥运会,不仅场馆、安全以及住宿问题屡被参赛选手诟病,甚至由于巴西政局动荡,包括巴西总统在内的多国政要都缺席了开幕式。

也是因为国内政治更迭,没人花心思在赛后的经济提振上,导致这次奥运会血亏了几十亿美元,被巴西人称为“巴西的灾难”。

pic_411656ae.png

▲2016年的巴西里约热内卢奥运会因为政局动荡以及国内诸多问题,亏损严重

而去年的东京奥运会,东京奥组委申奥期间靠着各种赞助还赚了2亿多美元,甚至为了节省预算,连奥运村的桌椅床铺都换为了纸质材料。

精打细算的日本东京奥组委本想在奥运会的时候再大赚一笔,谁想到新冠疫情袭来,奥运会不得不延期一年举办,疫情的反复使日本东京奥运会最终亏损达300亿美元。

转过头来看,此次北京冬奥会虽然因疫情影响无法正常售卖门票,少了部分收入,可实际的赞助和电视转播权等收入并没减少。

而且,我们也必须要明白,举办奥运会最终目的并非是为了赚钱,而是为了弘扬奥运精神,以“更快、更高、更强——更团结”的宗旨来传承人类文明的历史和文化。

pic_5728e1cd.png

▲奥运会的举办并不是为了赚钱,而是为了体现“更快、更高、更强——更团结”的奥林匹克格言

面对突如其来的新冠疫情,我们或许更加体会到人类的命运始终是一体的,全世界的人们面对共同的灾难,更需要表现出前所未有的团结力量和坚定信心。

事实上,顾拜旦创立奥运会的目的之一,就是希望通过体育运动增强民众的体质,与各种疾病作抗争。

也正如顾拜旦所说:对人生而言,重要的绝不是凯旋,而是战斗。

收起阅读 »

加班最狠的城市,北京只能排第三

当内卷的现象一直在持续,朝九晚五的行业越来越少,没有年轻人对“加班”是陌生的。虽然大家都在网上抵制996,但现实工作只能默默接受加班。除了白天效率低导致的加班,很大一部分原因是领导临时布置任务、白天频繁开会,或者领导没走不敢走、临时开会等等。即使这些客观情况很...
继续阅读 »

pic_7dcf8412.png

pic_f0d3544e.png

pic_33960cbc.png

pic_4997eb8e.png

pic_ccc8b0db.png

pic_43aa870e.png

pic_45072756.png

pic_1156bbff.png

pic_cbe17c29.png

pic_b162e3bd.png

pic_d7a63a01.png

pic_6aa08df7.png

pic_eb75cf78.png

pic_2711981c.png

pic_4633dcb1.png

pic_ea1dc4da.png

当内卷的现象一直在持续,朝九晚五的行业越来越少,没有年轻人对“加班”是陌生的。

虽然大家都在网上抵制996,但现实工作只能默默接受加班。除了白天效率低导致的加班,很大一部分原因是领导临时布置任务、白天频繁开会,或者领导没走不敢走、临时开会等等。

即使这些客观情况很难改变,我们也并不鼓励年轻人频繁跳槽(你会发现大部分公司都一样),但并非我们对加班这件事就应该“无动于衷”。

除了更高效、合理地安排每周、每天的工作,你还应该提高自己的“职场议价力”。当你在职场有了更强的实力和更出色的作品,你的底气也就会越足。对于一些莫须有的加班,你也会有更足的拒绝的勇气。

就像马东在“下班要不要回工作消息”这一期《奇葩说》的总结,“我们的人生最重要的,是照顾着苟且,别忘了诗和远方。我们最悲哀的,是既不容于眼前的苟且,而又忘了诗和远方。”

作者:梅雪 永旺 十一
来源:后浪研究所(ID:youth36kr)

收起阅读 »

系统监测员工聊天记录惹争议 ,业内:系统早就有了,成本最低1万/年

一款号称能监控员工跳槽倾向的“员工行为感知”系统引起关注。2月11日,网友@深圳张逸轩在社交平台上称,有监控系统能提前获知员工跳槽念头,并附上系统监控页面图。图源:微博截图根据其发布的截图显示,该系统能检测到员工访问求职网站次数、聊天关键词量、搜索关键词量以及...
继续阅读 »

一款号称能监控员工跳槽倾向的“员工行为感知”系统引起关注。

2月11日,网友@深圳张逸轩在社交平台上称,有监控系统能提前获知员工跳槽念头,并附上系统监控页面图。


图源:微博截图

根据其发布的截图显示,该系统能检测到员工访问求职网站次数、聊天关键词量、搜索关键词量以及简历投递次数等内容。

除此之外,员工通过公司内网的聊天记录、上网时长、访问应用的特征等上网行为都会被这套系统监控,并通过预定义的规则判定员工的工作状态。

“员工行为感知”系统曝光后,不少网友质疑“公司侵犯员工个人隐私”。

上海普世万联律师事务所高级合伙人陶书澄对时代周报记者表示,部分企业使用先进的、技术化的管理程序及方式,实行正当的管理行为,无可厚非。但随着科技的发展、信息技术运用的延伸,很容易出现涉及侵犯劳动者隐私的情形。

01 连聊天记录都被监控

2月12日,某家上市公司员工对时代周报记者表示,该公司使用了“员工行为感知”系统。“我是从公司公告中知道这事儿的,平时我们在公司,手机、电脑在连着WiFi的情况下,聊天都不敢讨论不利于公司的内容,担心被系统监控到”。

当天下午,时代周报记者以客户身份咨询了深圳一家主营上网行为管理的公司,其相关产品涉及“行为感知”系统。

据该公司销售经理介绍,“员工行为感知”系统并不算很稀有的产品,市面上已经出来7-8年了,“像一些互联网大厂都有自己独立的一套系统,一般运用于公司内网”。

“网站、贴吧、QQ聊天记录等,员工在电脑上面的全部上网行为都可以提取出关键词,做一个分析报表”,该经理向时代周报记者发来的《上网行为管理产品产品介绍书》显示,上网行为管理涉及网页、文件、邮件和聊天记录。

其中,网页记录管理是指全面记录内网用户向公网BBS、论坛、博客、空间等发表的帖子内容及附件;聊天记录管理则支持对即时通讯协议如QQ、飞信、旺旺等进行阻止登录,并对文字聊天、语音聊天及文件传输进行过滤与聊天内容记录。

此外,时代周报记者发现,该公司“员工行为感知”系统除了提供离职倾向分析服务外,还有专门的员工工作效率分析。

主要内容包括:针对员工在工作时段的浏览内容、访问应用的特征进行时长统计,根据预定义的规则判定员工的工作状态;针对工作效率低下的员工,可详细查看具体上网的网站和应用,以及时长,判定消极怠工有据可查。

02 成本价低至1万元/年

上述销售经理称,监控系统安装起来不难,只需要在主机房的路由器旁外接一个硬件,就可以管控到全公司的电脑,“一般情况下,我们会建议公司告知员工,否则员工难以察觉”。

那么,企业采购这样一套监控系统,需要花费多少?

该销售经理表示,其公司是直销的厂商,如果购买企业人数在50以下,基本按照成本价1万块/年来卖,之后每年付1千多块的维护费用。“主要是按企业人头区间计费,如果通过中间商销售,费用肯定比我们高。”

据媒体报道,有相关研发方称,“员工行为感知”系统均为按年付费,费用与网络带宽、终端数量有关。如果是两套系统配合使用,那这一费用会上升到十几万元,在配备更好的网络宽带、更多的终端数量时,费用甚至能达到几十万元。

据红星新闻报道,本次网传“行为感知系统”页面截图与A股上市公司深信服科技股份有限公司(以下简称“深信服”)2017年开发的一款软件“深信服行为感知系统BA”产品介绍页面一致。

时代周报记者查阅专利检索及分析系统发现,2018年3月,深信服公开申请一项名为“一种离职倾向分析方法、装置、设备及存储介质”的专利,于当年8月公开。这份专利的摘要显示:该方法包括获取员工终端中的上网行为数据;判断所述上网行为数据中是否存在与离职倾向相关的行为数据等。

03 律师:极易侵犯劳动者隐私

对于收集员工上网行为数据,企业是否涉及相关法律风险?前述销售经理表示,上网行为审计,是国家相关文件支持的,是对上网行为的预警,“并不会有相关法律风险”。

北京瀛和律师事务所律师谭威告诉时代周报记者,收集员工上网行为数据是否侵犯隐私,“不能一概而论,要具体情况具体分析”。

“如果公司恶意抓取员工信息,不仅监控员工工作时间的网络使用情况,还可能会对员工在非劳动时间内和工作场所外的互联网使用情况进行监视,那么公司就涉嫌侵犯员工隐私权了。”谭威称。

对于本次引发争议的事件,陶书澄认为,企业使用“行为感知、研判、识别”技术及管理方式,涉及的监管、分析范围不仅仅针对劳动者基于劳动关系提供的劳动行为本身,还涵盖了劳动者的聊天记录、浏览记录、邮件投递记录甚至邮件附件等。

“聊天记录、邮件附件等内容,不排除包含劳动者工作事项、工作时间之外的个人信息及记录,因此针对这些内容的监管及分析,极易侵犯劳动者的隐私及其他合法权益。”陶书澄提醒。

陶书澄认为,企业使用“员工行为感知”系统,应该内部事先告知、征得劳动者同意,并且只进行与提升工作效率相关的监管,对涉及劳动者个人隐私的信息、记录不做监管。

此外,对于获取的信息进行制度化、合法化的保密等措施,对于企业在技术运用、提升效率与保护劳动者隐私的平衡及风险规避,具有重要的运用。

作者:郭梓昊 石恩泽
来源:https://mp.weixin.qq.com/s/LFrhQZONSTKOXuTMLOxM1g

收起阅读 »

卧槽!用代码实现冰墩墩,太浪漫了吧

声明:本文涉及奥运元素3D模型仅用于个人学习、研究和欣赏,请勿二次修改、非法传播、转载、出版、商用、及进行其他获利行为。背景迎冬奥,一起向未来!2022冬奥会马上就要开始了,本文使用 Three.js + React 技术栈,实现冬日和奥运...
继续阅读 »

声明:本文涉及奥运元素3D模型仅用于个人学习、研究和欣赏,请勿二次修改、非法传播、转载、出版、商用、及进行其他获利行为。

背景

迎冬奥,一起向未来!2022冬奥会马上就要开始了,本文使用 Three.js + React 技术栈,实现冬日和奥运元素,制作了一个充满趣味和纪念意义的冬奥主题 3D 页面。本文涉及到的知识点主要包括:TorusGeometry 圆环面、MeshLambertMaterial 非光泽表面材质、MeshDepthMaterial 深度网格材质、custromMaterial 自定义材质、Points 粒子、PointsMaterial 点材质等。

效果

实现效果如以下 👇 动图所示,页面主要由 2022 冬奥会吉祥物 冰墩墩 、奥运五环、舞动的旗帜 🚩、树木 🌲 以及下雪效果 ❄️ 等组成。按住鼠标左键移动可以改为相机位置,获得不同视图。

pic_b005c37f.png

👀 在线预览: https://dragonir.github.io/3d… (部署在 GitHub,加载速度可能会有点慢 😓

实现

引入资源

首先引入开发页面所需要的库和外部资源,OrbitControls 用于镜头轨道控制、TWEEN 用于补间动画实现、GLTFLoader 用于加载 glb 或 gltf 格式的 3D 模型、以及一些其他模型、贴图等资源。

import React from 'react';
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { TWEEN } from "three/examples/jsm/libs/tween.module.min.js";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import bingdundunModel from './models/bingdundun.glb';
// ...

页面DOM结构

页面 DOM 结构非常简单,只有渲染 3D 元素的 #container 容器和显示加载进度的 .olympic_loading元素。


{this.state.loadingProcess === 100 ? '' : (

{this.state.loadingProcess} %


)}


场景初始化

初始化渲染容器、场景、相机。关于这部分内容的详细知识点,可以查阅我往期的文章,本文中不再赘述。

container = document.getElementById('container');
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
container.appendChild(renderer.domElement);
scene = new THREE.Scene();
scene.background = new THREE.TextureLoader().load(skyTexture);
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 30, 100);
camera.lookAt(new THREE.Vector3(0, 0, 0));

添加光源

本示例中主要添加了两种光源:DirectionalLight 用于产生阴影,调节页面亮度、AmbientLight 用于渲染环境氛围。

// 直射光
const light = new THREE.DirectionalLight(0xffffff, 1);
light.intensity = 1;
light.position.set(16, 16, 8);
light.castShadow = true;
light.shadow.mapSize.width = 512 * 12;
light.shadow.mapSize.height = 512 * 12;
light.shadow.camera.top = 40;
light.shadow.camera.bottom = -40;
light.shadow.camera.left = -40;
light.shadow.camera.right = 40;
scene.add(light);
// 环境光
const ambientLight = new THREE.AmbientLight(0xcfffff);
ambientLight.intensity = 1;
scene.add(ambientLight);

加载进度管理

使用 THREE.LoadingManager 管理页面模型加载进度,在它的回调函数中执行一些与加载进度相关的方法。本例中的页面加载进度就是在 onProgress 中完成的,当页面加载进度为 100% 时,执行 TWEEN 镜头补间动画。

const manager = new THREE.LoadingManager();
manager.onStart = (url, loaded, total) => {};
manager.onLoad = () => { console.log('Loading complete!')};
manager.onProgress = (url, loaded, total) => {
if (Math.floor(loaded / total * 100) === 100) {
this.setState({ loadingProcess: Math.floor(loaded / total * 100) });
// 镜头补间动画
Animations.animateCamera(camera, controls, { x: 0, y: -1, z: 20 }, { x: 0, y: 0, z: 0 }, 3600, () => {});
} else {
this.setState({ loadingProcess: Math.floor(loaded / total * 100) });
}
};

创建地面

本示例中凹凸起伏的地面是使用 Blender 构建模型,然后导出 glb 格式加载创建的。当然也可以只使用 Three.js 自带平面网格加凹凸贴图也可以实现类似的效果。使用 Blender 自建模型的优点在于可以自由可视化地调整地面的起伏效果。

var loader = new THREE.GLTFLoader(manager);
loader.load(landModel, function (mesh) {
mesh.scene.traverse(function (child) {
if (child.isMesh) {
child.material.metalness = .1;
child.material.roughness = .8;
// 地面
if (child.name === 'Mesh_2') {
child.material.metalness = .5;
child.receiveShadow = true;
}
});
mesh.scene.rotation.y = Math.PI / 4;
mesh.scene.position.set(15, -20, 0);
mesh.scene.scale.set(.9, .9, .9);
land = mesh.scene;
scene.add(land);
});

pic_9d8f0ff5.png

创建冬奥吉祥物冰墩墩

现在添加可爱的冬奥会吉祥物熊猫冰墩墩 🐼,冰墩墩同样是使用 glb 格式模型加载的。它的原始模型来源于这里,从这个网站免费现在模型后,原模型是使用 3D max 建的我发现并不能直接用在网页中,需要在 Blender 中转换模型格式,还需要调整调整模型的贴图法线,才能还原渲染图效果。

原模型:

pic_23ebc67c.png

冰墩墩贴图:

pic_bef8c61c.png

转换成Blender支持的模型,并在Blender中调整模型贴图法线、并添加贴图:

pic_193e130a.png

导出glb格式:

pic_e3005891.png

📖 在 Blender 中给模型添加贴图教程传送门: 在Blender中怎么给模型贴图

仔细观察冰墩墩 🐼可以发现,它的外面有一层透明塑料或玻璃质感外壳,这个效果可以通过修改模型的透明度、金属度、粗糙度等材质参数实现,最后就可以渲染出如 👆 banner图 所示的那种效果,具体如以下代码所示。

loader.load(bingdundunModel, mesh => {
mesh.scene.traverse(child => {
if (child.isMesh) {
// 内部
if (child.name === 'oldtiger001') {
child.material.metalness = .5
child.material.roughness = .8
}
// 半透明外壳
if (child.name === 'oldtiger002') {
child.material.transparent = true;
child.material.opacity = .5
child.material.metalness = .2
child.material.roughness = 0
child.material.refractionRatio = 1
child.castShadow = true;
}
}
});
mesh.scene.rotation.y = Math.PI / 24;
mesh.scene.position.set(-8, -12, 0);
mesh.scene.scale.set(24, 24, 24);
scene.add(mesh.scene);
});

创建奥运五环

奥运五环由基础几何模型圆环面 TorusGeometry 来实现,创建五个圆环面,并调整它们的材质颜色和位置来构成蓝黑红黄绿顺序的五环结构。五环材质使用的是 MeshLambertMaterial

const fiveCycles = [
{ key: 'cycle_0', color: 0x0885c2, position: { x: -250, y: 0, z: 0 }},
{ key: 'cycle_1', color: 0x000000, position: { x: -10, y: 0, z: 5 }},
{ key: 'cycle_2', color: 0xed334e, position: { x: 230, y: 0, z: 0 }},
{ key: 'cycle_3', color: 0xfbb132, position: { x: -125, y: -100, z: -5 }},
{ key: 'cycle_4', color: 0x1c8b3c, position: { x: 115, y: -100, z: 10 }}
];
fiveCycles.map(item => {
let cycleMesh = new THREE.Mesh(new THREE.TorusGeometry(100, 10, 10, 50), new THREE.MeshLambertMaterial({
color: new THREE.Color(item.color),
side: THREE.DoubleSide
}));
cycleMesh.castShadow = true;
cycleMesh.position.set(item.position.x, item.position.y, item.position.z);
meshes.push(cycleMesh);
fiveCyclesGroup.add(cycleMesh);
});
fiveCyclesGroup.scale.set(.036, .036, .036);
fiveCyclesGroup.position.set(0, 10, -8);
scene.add(fiveCyclesGroup);

💡 TorusGeometry 圆环面

TorusGeometry 一个用于生成圆环几何体的类。

构造函数:

TorusGeometry(radius: Float, tube: Float, radialSegments: Integer, tubularSegments: Integer, arc: Float)
  • radius:圆环的半径,从圆环的中心到管道(横截面)的中心。默认值是 1
  • tube:管道的半径,默认值为 0.4
  • radialSegments:圆环的分段数,默认值为 8
  • tubularSegments:管道的分段数,默认值为 6
  • arc:圆环的圆心角(单位是弧度),默认值为 Math.PI * 2

💡 MeshLambertMaterial 非光泽表面材质

一种非光泽表面的材质,没有镜面高光。该材质使用基于非物理的 Lambertian 模型来计算反射率。这可以很好地模拟一些表面(例如未经处理的木材或石材),但不能模拟具有镜面高光的光泽表面(例如涂漆木材)。

构造函数:

MeshLambertMaterial(parameters : Object)
  • parameters:(可选)用于定义材质外观的对象,具有一个或多个属性。材质的任何属性都可以从此处传入。

创建旗帜

旗面模型是从sketchfab下载的,还需要一个旗杆,可以在 Blender中添加了一个柱状立方体,并调整好合适的长宽高和旗面结合起来。

pic_9352c4e1.png

旗面贴图:

pic_6e3c199f.png

旗面添加了动画,需要在代码中执行动画帧播放。

loader.load(flagModel, mesh => {
mesh.scene.traverse(child => {
if (child.isMesh) {
child.castShadow = true;
// 旗帜
if (child.name === 'mesh_0001') {
child.material.metalness = .1;
child.material.roughness = .1;
child.material.map = new THREE.TextureLoader().load(flagTexture);
}
// 旗杆
if (child.name === '柱体') {
child.material.metalness = .6;
child.material.roughness = 0;
child.material.refractionRatio = 1;
child.material.color = new THREE.Color(0xeeeeee);
}
}
});
mesh.scene.rotation.y = Math.PI / 24;
mesh.scene.position.set(2, -7, -1);
mesh.scene.scale.set(4, 4, 4);
// 动画
let meshAnimation = mesh.animations[0];
mixer = new THREE.AnimationMixer(mesh.scene);
let animationClip = meshAnimation;
let clipAction = mixer.clipAction(animationClip).play();
animationClip = clipAction.getClip();
scene.add(mesh.scene);
});

创建树木

为了充实画面,营造冬日氛围,于是就添加了几棵松树 🌲 作为装饰。添加松树的时候用到一个技巧非常重要:我们知道因为树的模型非常复杂,有非常多的面数,面数太多会降低页面性能,造成卡顿。本文中使用两个如下图 👇 所示的两个交叉的面来作为树的基座,这样的话树只有两个面数,使用这个技巧可以和大程度上优化页面性能,而且树 🌲 的样子看起来也是有 3D 感的。

pic_5929966b.png

材质贴图:

pic_f6f036b7.png

为了使树只在贴图透明部分透明、其他地方不透明,并且可以产生树状阴影而不是长方体阴影,需要给树模型添加如下 MeshPhysicalMaterialMeshDepthMaterial 两种材质,两种材质使用同样的纹理贴图,其中 MeshDepthMaterial 添加到模型的 custromMaterial 属性上。

let treeMaterial = new THREE.MeshPhysicalMaterial({
map: new THREE.TextureLoader().load(treeTexture),
transparent: true,
side: THREE.DoubleSide,
metalness: .2,
roughness: .8,
depthTest: true,
depthWrite: false,
skinning: false,
fog: false,
reflectivity: 0.1,
refractionRatio: 0,
});
let treeCustomDepthMaterial = new THREE.MeshDepthMaterial({
depthPacking: THREE.RGBADepthPacking,
map: new THREE.TextureLoader().load(treeTexture),
alphaTest: 0.5
});
loader.load(treeModel, mesh => {
mesh.scene.traverse(child =>{
if (child.isMesh) {
child.material = treeMaterial;
child.custromMaterial = treeCustomDepthMaterial;
}
});
mesh.scene.position.set(14, -9, 0);
mesh.scene.scale.set(16, 16, 16);
scene.add(mesh.scene);
// 克隆另两棵树
let tree2 = mesh.scene.clone();
tree2.position.set(10, -8, -15);
tree2.scale.set(18, 18, 18);
scene.add(tree2)
// ...
});

实现效果也可以从 👆 上面 Banner 图中可以看到,为了画面更好看,我取消了树的阴影显示。

📌 在 3D 功能开发中,一些不重要的装饰模型都可以采取这种策略来优化。

💡 MeshDepthMaterial 深度网格材质

一种按深度绘制几何体的材质。深度基于相机远近平面,白色最近,黑色最远。

构造函数:

MeshDepthMaterial(parameters: Object)
  • parameters:(可选)用于定义材质外观的对象,具有一个或多个属性。材质的任何属性都可以从此处传入。

特殊属性:

  • .depthPacking[Constant]depth packing 的编码。默认为 BasicDepthPacking
  • .displacementMap[Texture]:位移贴图会影响网格顶点的位置,与仅影响材质的光照和阴影的其他贴图不同,移位的顶点可以投射阴影,阻挡其他对象,以及充当真实的几何体。
  • .displacementScale[Float]:位移贴图对网格的影响程度(黑色是无位移,白色是最大位移)。如果没有设置位移贴图,则不会应用此值。默认值为 1
  • .displacementBias[Float]:位移贴图在网格顶点上的偏移量。如果没有设置位移贴图,则不会应用此值。默认值为 0

💡 custromMaterial 自定义材质

给网格添加 custromMaterial 自定义材质属性,可以实现透明外围 png 图片贴图的内容区域阴影。

创建雪花

创建雪花 ❄️,就要用到粒子知识。THREE.Points 是用来创建点的类,也用来批量管理粒子。本例中创建了 1500 个雪花粒子,并为它们设置了限定三维空间的随机坐标及横向和竖向的随机移动速度。

// 雪花贴图
let texture = new THREE.TextureLoader().load(snowTexture);
let geometry = new THREE.Geometry();
let range = 100;
let pointsMaterial = new THREE.PointsMaterial({
size: 1,
transparent: true,
opacity: 0.8,
map: texture,
// 背景融合
blending: THREE.AdditiveBlending,
// 景深衰弱
sizeAttenuation: true,
depthTest: false
});
for (let i = 0; i < 1500; i++) {
let vertice = new THREE.Vector3(Math.random() * range - range / 2, Math.random() * range * 1.5, Math.random() * range - range / 2);
// 纵向移速
vertice.velocityY = 0.1 + Math.random() / 3;
// 横向移速
vertice.velocityX = (Math.random() - 0.5) / 3;
// 加入到几何
geometry.vertices.push(vertice);
}
geometry.center();
points = new THREE.Points(geometry, pointsMaterial);
points.position.y = -30;
scene.add(points);

💡 Points 粒子

Three.js 中,雨 🌧️、雪 ❄️、云 ☁️、星辰  等生活中常见的粒子都可以使用 Points 来模拟实现。

构造函数:

new THREE.Points(geometry, material);
  • 构造函数可以接受两个参数,一个几何体和一个材质,几何体参数用来制定粒子的位置坐标,材质参数用来格式化粒子;
  • 可以基于简单几何体对象如 BoxGeometrySphereGeometry等作为粒子系统的参数;
  • 一般来讲,需要自己指定顶点来确定粒子的位置。

💡 PointsMaterial 点材质

通过 THREE.PointsMaterial 可以设置粒子的属性参数,是 Points 使用的默认材质。

构造函数:

PointsMaterial(parameters : Object)
  • parameters:(可选)用于定义材质外观的对象,具有一个或多个属性。材质的任何属性都可以从此处传入。

💡 材质属性 .blending

材质的.blending 属性主要控制纹理融合的叠加方式,.blending 属性的值包括:

  • THREE.NormalBlending:默认值
  • THREE.AdditiveBlending:加法融合模式
  • THREE.SubtractiveBlending:减法融合模式
  • THREE.MultiplyBlending:乘法融合模式
  • THREE.CustomBlending:自定义融合模式,与 .blendSrc.blendDst 或 .blendEquation 属性组合使用

💡 材质属性 .sizeAttenuation

粒子的大小是否会被相机深度衰减,默认为 true(仅限透视相机)。

💡 Three.js 向量

几维向量就有几个分量,二维向量 Vector2 有 x 和 y 两个分量,三维向量 Vector3 有xyz 三个分量,四维向量 Vector4 有 xyzw 四个分量。

相关API:

  • Vector2:二维向量
  • Vector3:三维向量
  • Vector4:四维向量

镜头控制、缩放适配、动画

controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 0, 0);
controls.enableDamping = true;
// 禁用平移
controls.enablePan = false;
// 禁用缩放
controls.enableZoom = false;
// 垂直旋转角度限制
controls.minPolarAngle = 1.4;
controls.maxPolarAngle = 1.8;
// 水平旋转角度限制
controls.minAzimuthAngle = -.6;
controls.maxAzimuthAngle = .6;
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}, false);
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
controls && controls.update();
// 旗帜动画更新
mixer && mixer.update(new THREE.Clock().getDelta());
// 镜头动画
TWEEN && TWEEN.update();
// 五环自转
fiveCyclesGroup && (fiveCyclesGroup.rotation.y += .01);
// 顶点变动之后需要更新,否则无法实现雨滴特效
points.geometry.verticesNeedUpdate = true;
// 雪花动画更新
let vertices = points.geometry.vertices;
vertices.forEach(function (v) {
v.y = v.y - (v.velocityY);
v.x = v.x - (v.velocityX);
if (v.y <= 0) v.y = 60;
if (v.x <= -20 || v.x >= 20) v.velocityX = v.velocityX * -1;
});
}

🔗 完整代码: https://github.com/dragonir/3…

总结

💡 本文中主要包含的新知识点包括:

  • TorusGeometry 圆环面
  • MeshLambertMaterial 非光泽表面材质
  • MeshDepthMaterial 深度网格材质
  • custromMaterial 自定义材质
  • Points 粒子
  • PointsMaterial 点材质
  • 材质属性 .blending.sizeAttenuation
  • Three.js 向量

进一步优化的空间:

  • 添加更多的交互功能、界面样式进一步优化;
  • 吉祥物冰墩墩添加骨骼动画,并可以通过鼠标和键盘控制其移动和交互。

作者:dragonir

来源:https://segmentfault.com/a/1190000041363089

收起阅读 »

北京冬奥从开幕式就黑科技曝了!日本网友:人类半年里科技进步巨大

这一次的北京冬奥会,从开幕式就直接火了!不仅在国内各大平台迅速登上热榜,国外也被开幕式的诸多惊艳瞬间刷屏,连奥林匹克官方都开始求带货:而除了大型带货现场之外,最让国内外网友感到震撼的,就是其中展现出来的十足的科技感。最开始破冰而出的五环一出,其逼真的碎冰效果就...
继续阅读 »

这一次的北京冬奥会,从开幕式就直接火了!

不仅在国内各大平台迅速登上热榜,国外也被开幕式的诸多惊艳瞬间刷屏,连奥林匹克官方都开始求带货:


而除了大型带货现场之外,最让国内外网友感到震撼的,就是其中展现出来的十足的科技感。

最开始破冰而出的五环一出,其逼真的碎冰效果就已经让不少人惊呼:不会真是现场雕刻的吧?


8K屏倾斜而下的瀑布流水,让人仿佛能直观地感受那种黄河之水天上来的震撼:


△图源CCTV16直播

还有跟随演员的脚步变幻的“流沙”,也是由AI动作捕捉实时实现的灯光特效:


△图源CCTV5 直播

面对此情此景,有日本网友甚至拉出了自家半年前的夏季奥运会,语气复杂地表示:

仅仅半年时间,人类科技的发展竟如此惊人。


接下来,就让我们来依次看看开幕式里那些令人惊艳的黑科技。

冰雪五环:重3吨LED异形屏

首先是开场那晶莹透亮的冰雪五环,可以说是兼具科技感与美感:


△图源中国青年报

有网友这样称赞道:

希望是破土而出,未来是破冰而生。


奥运五环经过24道激光“雕刻”后,从冰立方中破冰而出并随着音乐冉冉升起,随后就在高空伫立整整了70-80分钟。

这究竟是怎么做到的?

事实上,整个冰雪五环就是个巨大的LED异形屏,长达20米,重3吨。

据《盛会》纪录片报道,当时面临三个问题。

一个是透亮。即便是在大风、大雨等极端天气下也能保证看得到。最终经过不断地选择和调试,确定屏幕选择2毫米PC板,透光率达到70%。

还有就是供电方式和体重。按照原计划,五环升起需要用两根挨着地的拖线来牵引。

这样可以确保即便是在大风情况下,不至于晃悠太厉害,而且人力也可以介入来保障。

但在张艺谋导演团队看来,这样用两根细线来拉,会显得不够“美”。

于是他们就改变计划——上下都不拉线,由威亚吊起。


△图源冬奥会纪录片《盛会》

这就要求团队改变供电方式以及减轻体重。

供电方式上,他们从外部供电改为原本用来应急的电池。经过航天技术团队反复测试,并完成耐低温的性能测试,确定了航天强化电源。

而在体重控制上,工作人员曾描述,已经精确抠到一个小螺丝钉算重量。

最后总体重量从3.5吨控制到了3吨。

而将其包裹的冰立方,也是整场发布会中少有的几个大装置之一,通体被LED屏包围。

雪花火炬:嵌有55万颗LED灯珠

随后就是代表团入场,每个国家和地区入场时都有一个雪花引导牌。


这个引导牌可不简单。除了形状上像中国结,还在材质上下了不少功夫。

据团队介绍,一开始他们考虑的是3D打印,但是透光率不行、做不到晶莹剔透,看起来也笨重。

最终采用雪花和字母部分用LED灯珠代替,其他部分细钢丝支撑,利用光的折射来隐藏钢丝。


△图源冬奥会纪录片《盛会》

另外,为了防止过程中出现停电导致字母缺失、雪花不亮,团队采用五路供电的方式,即便3、4根电线断了,也能正常发光。

随着中国代表队入场之后,引导牌随着志愿者的动作汇聚在一起。此时,巨大的虚拟雪花开始形成。

大雪花形状的主火炬台就此诞生。

据京东方官微介绍,这个主火炬台直径有14.89米,由96块小雪花形状和6块橄榄枝形状的LED双面屏组成。

上面嵌着超55万颗LED灯珠,每个LED灯珠都由驱动芯片的单一信道独立控制。

而在点火仪式上,巨型雪花屏由中心向四周辐射开来。


这当中保证屏快速变化、视频画面完美协同的技术,同样也是京东方同/异步兼容信发系统。

异步集控能在极短时间内将大规模视频内容快速下发,同步集控确保102块双面屏幕实现毫秒级响应。

此外,“主路+环路”备份的高冗余控制系统确保了火炬台播控系统的超高可靠性。

与此同时,还采用了LoRa(远距离无线电)低延迟控制系统搭配同步播放时间校正技术,进一步确保视频画面完美协同。

最强AI辅助:4个摄像头实现全场动捕

除了这些黑科技之外,冬奥会还完成了几个首次。

其中一个首次,就是世界上首次对超过600人集体实时AI动作捕捉。

在《雪花》的节目中,上百位小朋友挥舞着和平鸽灯,在场地中四处游荡合唱~

他们脚下的每一步,都化作一朵朵雪花。


△图源央视直播

整个过程包括数据采集、传输以及实时渲染,总共时间不超过0.1秒,达到实时互动的效果。

而且据技术团队介绍,仅需4部摄像机就能覆盖全场。

核心主要解决两个方面的问题。

一个是识别出舞台上的演员并定位他们所在的位置;另一个则是保证实时互动。

前者是由英特尔提供的3DAT三维运动员追踪技术,由摄像机信息采集、数据分析、艺术效果渲染三大子系统构成,其核心算法是实时人体检测和位置追踪技术。后者由北京电影学院提供实时渲染支持。

实际上在这个节目之前,四组滑冰选手划出奥运口号那一刻,就已经使用了该技术。

8K地面屏:42000多块LED模块

要实现整场节目的效果完美呈现,还少不了一个大功臣。

那就是8K地面屏。

据介绍,这是目前世界上最大的一块LED地屏,面积达到了10552平方米。

光LED模块就用了42000多块。背后的技术团队京东方采用了多个8K+级分辨率的画面融合技术,以及光学校正算法。

其中,光学校正算法可对每个显示画面进行像素点级的光学校正。

最终呈现出100000:1超高对比度,3840Hz超高刷新率,以及29900x15096超高分辨率的超高清绚丽画面。

△图源CCTV16直播

但除了要实现8K超高清视频播放视觉体验外,还要兼具舞台的耐磨、结构承重、防水防寒以及电气安全等特性。

而且更具挑战的是,传统的地屏也没有经历过冬季室外环境的考验。

负责地屏建设的航天科技集团,为了解决这一问题,给传统的LED屏幕穿上了一层特殊的“防护服”。

最终,每一平方米的承重量都超过500公斤以上。

“我负责精彩,航天负责成功”

这是开幕式的总设计张艺谋挂在嘴巴的一句话。

没错,开幕式上的这些震撼画面的背后,离不开航天科技集团的支持。

这一集团共负责了地面舞台、威亚、火炬、冰立方、指挥监控、通信系统、地面显示系统、冰瀑、指挥中心、央视视频播放及VR、视频渲染机房等11个大项目。

比如开场“雕刻”出五环的那块冰立方,就是基于航天一院15所的多级折叠结构,最终在1毫米的误差之下成功升起的。

航天一院12所则负责整个开幕式的通信支持,开发出了有线内通系统、无线通信系统和无线FM广播系统。

除了开幕式上肉眼可见的“科技感”,冬奥会背后也可谓是卧虎藏龙。

北京冬奥会赛事时长约1000小时,转播内容总生产量将达6000小时。

所有的内容都将通过阿里云以4K的超高清格式向全球转播,部分重要赛事还将以8K格式转播,这将使今年的北京冬奥会成为全球首个全程4K直播的奥林匹克竞赛。


还有来自百度智能云团队的AI手语主播,在经历了朱广权的魔鬼面试后,现已正式上岗,将在各类冰雪赛事中,为2780万听障人士提供24小时不间断的手语服务。


场地里随处可见的智能向导机器人,不仅能指路,还会在被外国选手打招呼时,无情提醒“带好你的口罩”:


……

再回想起总导演张艺谋在赛前剧透的那句:

这场比赛很有科技含量,但是我们不炫技。

可以说是非常的凡尔赛了。

参考链接:

[1]https://search.bilibili.com/all?keyword=%E7%9B%9B%E4%BC%9A&from_source=webtop_search&spm_id_from=333.1007
[2]https://mp.weixin.qq.com/s/ikiOjXueuIQyEJsmZftRrg
[3]https://mp.weixin.qq.com/s/DFsmy3k1bP3xfA1oHQPp8Q

来源丨量子位(ID:QbitAI)
杨净 博雯 发自 凹非寺
https://mp.weixin.qq.com/s/g6jIJjNrPTeYAjhlaAD9WA

收起阅读 »

一些著名的软件都用什么语言编写?

1、操作系统Microsoft Windows :汇编 -> C -> C++备注:曾经在智能手机的操作系统(Windows Mobile)考虑掺点C#写的程序,比如软键盘,结果因为写出来的程序太慢,实在无法和别的模块合并,最终又回到C++重写。相...
继续阅读 »

1、操作系统

Microsoft Windows :汇编 -> C -> C++


备注:曾经在智能手机的操作系统(Windows Mobile)考虑掺点C#写的程序,比如软键盘,结果因为写出来的程序太慢,实在无法和别的模块合并,最终又回到C++重写。

相信很多朋友都知道Windows Vista,这个系统开发早期比尔盖茨想全部用C#写,但最终因为执行慢而放弃,结果之前无数软件工程师日夜劳作成果一夜之间被宣告作废。

Linux :C


Apple MacOS : 主要为C,部分为C++。

备注:之前用的语言比较杂,最早是汇编和Pascal。


Sun Solaris : C

HP-UX : C

Symbian OS : 汇编,主要为C++(诺基亚手机)

Google Android :2008 年推出:C语言(有传言说是用Java开发的操作系统,但最近刚推出原生的C语言SDK)

RIM BlackBerry OS 4.x :黑莓 C++

2、图形界面层

Microsoft Windows UI :C++

Apple MacOS UI (Aqua) : C++

Gnome (Linux图形界面之一,大脚): C和C++, 但主要是C

KDE (Linux图形界面): C++

3、桌面搜索工具

Google Desktop Search : C++


Microsoft Windows Desktop Search : C++

Beagle (Linux/Windows/UNIX 下): C# (基于开源的 .net : Mono)

4、办公软件

Microsoft Office :在 汇编 -> C -> 稳定在C++


Sun Open Office : 部分JAVA(对外接口),主要为C++ (开源,可下载其源代码)

Corel Office/WordPerfect Office : 1996年尝试过Java,次年被抛弃,重新回到C/C++

Adobe Systems Acrobat Reader/Distiller : C++

5、关系型数据库

Oracle : 汇编、C、C++、Java。主要为C++


MySQL : C++


IBM DB2 :汇编、C、C++,但主要为C


Microsoft SQL Server : 汇编 -> C->C++

IBM Informix : 汇编、C、C++,但主要为C

SAP DB/MaxDB : C++

6、Web Browsers/浏览器

Microsoft Internet Explorer : C++


Mozilla Firefox : C++


Netscape Navigator :The code of Netscape browser was written in C, and Netscape engineers, all bought to Java (see M. Cusumano book and article) redeveloped the browser using Java. It was too slow and abandoned. Mozilla, the next version, was later developed using C++.

Safari : (2003年1月发布)C++

Google Chrome : (2008的发布)C++


Sun HotJava : Java (死于1999年)

Opera : C++ (手机上占用率比较大)

Opera Mini : Opera Mini (2007) has a very funny architecture, and is indeed using both C++ and Java. The browser is split in two parts, an ultra thin (less than 100Kb) “viewer” client part and a server side responsible of rendering. The first uses Java and receives the page under the OBML format, the latter reuses classical Opera (C++) rendering engine plus Opera’s Small Screen Rendering, on the server. This allows Opera to penetrate various J2ME-enabled portable devices, such as phones, while preserving excellent response time. This comes obviously with a few sacrifices, for instance on JavaScript execution.

Mosaic : 鼻祖(已死) C 语言

7、邮件客户端

Microsoft Outlook : C++


IBM Lotus Notes : Java


Foxmail : Delphi


8、软件开发集成环境/IDE

Microsoft Visual Studio :C++


Eclipse : Java (其图形界面SWT基于C/C++)


Code::Blocks :C++


易语言:C++


火山中文:C++

火山移动:C++

9、虚拟机

Microsoft .Net CLR (.NET的虚拟机): C++


Java Virtual Machine (JVM) : Java 虚拟机:C++


10、ERP软件 (企业应用)

SAP mySAP ERP : C,后主要为“ABAP/4”语言

Oracle Peoplesoft : C++ -> Java


Oracle E-Business Suite : Java

11、商业智能(Business Intelligence )

Business Objects : C++

12、图形处理

Adobe Photoshop : C++


The GIMP : C

13、搜索引擎

Google : 汇编 与 C++,但主要为C++

14、著名网站

eBay : 2002年为C++,后主要迁至Java

facebook : C++ 和 PHP

This line is only about facebook, not its plugins. Plugins can be developed in many different technologies, thanks to facebook’s ORB/application server, Thrift. Thrift contains a compiler coded in C++. facebook people write about Thrift: “The multi-language code generation is well suited for search because it allows for application development in an efficient server side language (C++) and allows the Facebook PHP-based web application to make calls to the search service using Thrift PHP libraries.” Aside the use of C++, facebook has adopted a LAMP architecture.


阿里巴巴和淘宝:php->C++/Java(主要用)


15、游戏

汇编、C、C++

星际争霸、魔兽争霸、CS、帝国时代、跑跑卡丁车、传奇、魔兽世界… 数不胜数了,自己数吧


都是用C开发的,C语言靠近系统地称,执行速度最快。比如你的两个朋友与你分别玩用VB、Java、与C编写的“跑跑卡丁车”,你玩C编写的游戏已经跑玩结束了,发现你的两个朋友还没开始跑呢,那是相当的卡啊。

16、编译器

Microsoft Visual C++ 编译器: C++

Microsoft Visual Basic 解释、编译器:C++

Microsoft Visual C# :编译器: C++

gcc (GNU C compiler) : C

javac (Sun Java compiler) : Java

Perl : C++

PHP : C

17、3D引擎

Microsoft DirectX : C++


OpenGL : C


OGRE 3D : C++


18、Web Servers (网页服务)

Apache : C和C++,但主要为C


Microsoft IIS : C++

Tomcat : Java


Jboss : Java


19、邮件服务

Microsoft Exchange Server : C->C++

Postfix : C

hMailServer : C++

Apache James : Java

20、CD/DVD刻录

Nero Burning ROM : C++


K3B : C++

21、媒体播放器

Nullsoft Winamp : C++


Microsoft Windows Media Player : C++


22、Peer to Peer (P2P软件)

eMule : C++

μtorrent : C++

Azureus : Java (图形界面使用基于C/C++的SWT,类Eclipse)

23、全球定位系统(GPS)

TomTom : C++


Hertz NeverLost : C++

Garmin : C++

Motorola VIAMOTO : 2007年6月,停止服务,Java

24、3D引擎

Microsoft DirectX : C++(相信玩游戏的同学都知道这个,现在最高版本是DX11)

OpenGL : C

OGRE 3D : C++

25、服务器软件

Apache:C

Nginx:C


IIS:C

26、其它

OpenStack:Python


作者:土豆居士
来源:一口Linux

收起阅读 »

压缩11000条 key 减少 7.2M,飞书如何实现 i18n 前端体积优化

背景在推进国际化的进程中,涌现出很多方案可以帮大家实现国际化文案定义以及使用。在飞书前端架构中,国际化文案已经做到了按需引入及按需加载,只不过随着业务的发展,国际化文案数量逐渐增多。再来看代码中的文案部分,key 长度越来越长,这部分都属于无用代码,如果能够缩...
继续阅读 »

背景

在推进国际化的进程中,涌现出很多方案可以帮大家实现国际化文案定义以及使用。在飞书前端架构中,国际化文案已经做到了按需引入及按需加载,只不过随着业务的发展,国际化文案数量逐渐增多。再来看代码中的文案部分,key 长度越来越长,这部分都属于无用代码,如果能够缩短,可以节省部分代码体积,加快 js 在浏览器中运行的速度

如何做?

通过压缩 i18nkey 的方式,将 i18n 的 key 从字母压缩为短字符串。目前业界中为了提升 webpack 打包速度,发展出很多利用多进程进行 js 编译的方案。飞书前端为了提高 webpack 编译速度,大量使用了 thread-loader 进行并发编译,i18n 扫描则采用了 babel 插件进行扫描和统计,那如何在 babel 扫描的过程中将扫描结果收集起来,如何将运行时的 key 更换为更短的 key,并且能够按照文件归类,实现按需加载呢?

思路

  1. 在 webpack 编译之前,先拿到当前业务下载的文案列表,将列表中所有的 key 进行编码,编码后的长度应该越短越好;

  2. 在 babel loader 扫描的过程中,将用到的文案上报,并将引入文案时使用的 key,替换为短编码;

  3. 在扫描完成后,生成文案的部分,使用编码后的短字符串,作为文案的 key,打包进文案文件中。

具体代码

编码方式

将下载的所有 i18n 的 key 进行一次编码映射,通过 key 在数组中的 index,做一个 26 进制转换,再把转换后的字符串中的数字填充为剩余的未用到的字母,保证 key 中无数字,可获得一个不超过 5 位的短 key。

  const NUMBER_MAP = {
  0: 'q',
  1: 'r',
  2: 's',
  3: 't',
  4: 'u',
  5: 'v',
  6: 'w',
  7: 'x',
  8: 'y',
  9: 'z',
};
const i18nKeys = Object.keys(resources['zh-CN']).reduce((all: object, key: string, index: number) => {
  // 将i18n的key重新编码,编码成26进制,然后用字母替换掉所有数字。
  // 因为变量名称不能用数字开头,所以需要替换掉所有数字
  all[key] = index.toString(26).replace(/\d/g, (s) => NUMBER_MAP[s]);
  return all;
}, {});

最初的设想中如果有从某个 enum 中引入 key 的行为,可以将 enum 的成员名字一起缩短,所以采用了替换所有数字的方式,保证短 key 不会以数字开头,后来在开发过程中发现没有这种用法,但是编码方式还是保留下来了。

扫描方式

借助 babel plugin 强大的 ast api,可以轻松完成 i18n key 的扫描和替换。

export default function babelI18nPlugin(options, args: {i18nKeys: {[key: string]: string}}) {
const i18nKeys = args.i18nKeys;

return {
  visitor: {
    StringLiteral: (tree, module) => {
      const { node, parentPath: {
        node: parent, scope, type
      } } = tree;
      const { filename } = module;
      if (!shouldAnalyse(filename)) {
        return;
      }
      const stringValue = node.value;
      if (stringValue && i18nKeys.hasOwnProperty(stringValue)) {
        if (
          /**
            * 飞书前端中使用了 __Text 和 _t 的全局方法来获得对应的文案内容,所以在这里限定了只有在全局方法
            * __Text 和 _t 中传递的第一个参数为字符串时,才将字符串修改为短key
            */
          type === 'CallExpression' &&
          ['__t', '__Text', '__T'].includes(parent.callee.name) &&
          !scope.hasBinding(parent.callee.name)
        ) {
          node.value = i18nKeys[stringValue];
          /**
            * 通过在source中写入一个特殊注释的方式将key标记在代码中,
            * 交给下一步的webpack来收集
            */
          tree.addComment('leading', `${COMMENT_PREFIX} ${i18nKeys[stringValue]}`);
        } else {
          /**
            * 当匹配到的字符串并不是通过 _t 和 __Text 使用的场景,依然上报长key,保证代码稳定性
            */
          tree.addComment('leading', `${COMMENT_PREFIX} ${stringValue}`);
        }
      }
    },
    MemberExpression: (tree, { filename }) => {
      if (!shouldAnalyse(filename)) {
        return;
      }
      const { node } = tree;
      const memberName = node.property.name;
      if (memberName && i18nKeys.hasOwnProperty(memberName)) {
        tree.addComment('leading', `${COMMENT_PREFIX} ${memberName}`);
      }
    },
  }
};
}

如果扫描到了 i18n 相关的字符串字段,将在原地添加一个注释,用来标记当前模块使用到的 key,这种方式可以让扫描结果落在代码中,使得扫描的操作可以被cache-loader缓存,进一步提升构建速度。

收集过程

通过 babel-loader 的模块都会被标记上使用到的 i18n 的 key 和替换后的短 key,在 webpack 的 parse 阶段只需要遍历文件的所有注释即可拿到模块内用到的所有 i18n 的 key。

export default class ChunkI18nPlugin implements Plugin {
static fileCache = new Map<string, Set<string>>();

constructor(private i18nConfig: I18nBundleConfig) {
}

public apply(compiler: Compiler) {
  compiler.hooks.compilation.tap('ChunkI18nPlugin', (compilation, { normalModuleFactory }) => {

    const handler = (parser) => {
      // 在 parser 中 hook program 钩子
      parser.hooks.program.tap('ChunkI18nPlugin', (ast, comments) => {
        const file = parser.state.module.resource;

        if (!ChunkI18nPlugin.fileCache.has(file)) {
          ChunkI18nPlugin.fileCache.set(file, new Set<string>());
        }
        const keySet = ChunkI18nPlugin.fileCache.get(file);

        // 拿到module的所有注释,扫描其中包含的i18n信息,缓存到一个map中
        comments.forEach(({ value }: {value: string}) => {
          const matcher = value.match(/\s*@i18n\s*(?<keys>.*)/);
          if (matcher?.groups?.keys) {
            const keys = matcher.groups?.keys?.split(' ');
            (keys || []).forEach(keySet.add.bind(keySet));
          }
        });
      });
    };

    // 监听 normalModuleFactory 的 parser 的 hooks
    normalModuleFactory.hooks.parser
      .for('javascript/auto')
      .tap('DefinePlugin', handler);
    normalModuleFactory.hooks.parser
      .for('javascript/dynamic')
      .tap('DefinePlugin', handler);
    normalModuleFactory.hooks.parser
      .for('javascript/esm')
      .tap('DefinePlugin', handler);
  });
}

...

}

有什么不足?

按照模块收集到的 key 是基于源文件扫描到的所有的 key。实际上我们可能存在一些较大的工具方法模块,或者组件模块,并不会用到全部的代码(部分代码会被 treeshaking 机制删掉),后续优化方向可以探索如何只扫描用到的代码中的 key,进一步压缩打包后的总体积。

最终收益

在一段时间的灰度测试后,最终方案上线运行,飞书前端大约 11000 条 key 的情况下,所有单页前端代码体积总计下降 7.2MB。

作者:字节跳动技术团队
来源:https://mp.weixin.qq.com/s/Qt6BL5pa7OJIBLH7Sl_WCA

收起阅读 »

竟还有如此沙雕的代码注释!我笑喷了

然后,也要发出直击灵魂的质问:你是尊贵的付费大会员吗?其实,不止这些,代码注释还有很多种玩法。毕竟,最会玩的还是你们程序员。01杀了个产品经理祭天大概全天下的程序员,都悄悄在代码里藏进了自己对产品举起的那把大刀,而且一不留神,刀尖就露出来了......傻逼的是...
继续阅读 »

某站后端代码被“开源”,同时刷遍全网的,还有代码里的那些神注释。

我们这才知道,原来程序员个个都是段子手;这么多年来,我们也走过了他们的无数套路......

首先,产品经理,是永远永远吐槽不完的!网友的评论也非常扎心,说看这些代码就像在阅读程序员的日记,每一页都写满了对产品经理的恨。

eb60368d165e7c0ad07c142d5e68f118.png

然后,也要发出直击灵魂的质问:你是尊贵的付费大会员吗?

cdda766efc8f6b721df2dbadbb035334.png

这不禁让人想起之前某音乐app的穷逼Vip,果然,穷逼在哪里都是会被标记的。

3eaa5896f4823e431905129d7b368dfe.png

其实,不止这些,代码注释还有很多种玩法。毕竟,最会玩的还是你们程序员。

01杀了个产品经理祭天

大概全天下的程序员,都悄悄在代码里藏进了自己对产品举起的那把大刀,而且一不留神,刀尖就露出来了......

傻逼的是产品,不是我

7675eaee2e0529535ff9f7007a161736.png

到底要什么,我也很无奈啊.jpg

07df3abc6889ed3540a89edc9cf7dc15.png

▲昕霖是产品经理,李超是设计师

锅是产品的,不是我的

e71291474ca6a626ef68ce939853c473.png

02 诉求都在注释里了

出来工作,不就是为了赚钱吗?一不小心,真实的想法就在注释里流露。

不得不说,该站的程序员,真的很会搞事情。

9b8a6b9abc7dfd8fbbc77b2556b6fe34.png

“钱多活少办公室大,最好还能经常去国外旅游并能报销。”学生时代的Sergey Brin也把这个朴素的愿景写在了简历代码的注释里。原来,每个人的职业追求,都差不多。虽然后来的他成了Google联合创始人。

abad5e10652846e5099028c456b5b410.png

03 一不小心,就把实话说出来了

领导和老板们总有那么些不可言说的小心思,不过,程序员们你们也太耿直了吧?!

662ae0923616c10560ee29b602ed9a9c.png

老板的心机,都被你们暴露了。

/** 老板说多线程先不开,等客户提需求优化 */

当然,同事也并没有多么靠谱。

4c7d6a1eee7dd3c4a4695241520eeeb4.png

04 隔空喊话,“友好”切磋

在代码注释里,程序员们还能隔着时空通过comment喊话,进行友好地交流与切磋。

比如下面这两行注释,就是跨越两年的一段喊话。

9a1b69955f2245fe8c405a4f26c38537.png

不过,一不小心,画风可能就变了,忍不住就Diss了一下。

12142544328cdc03b955e2c55a9d0346.png

//somedev1 - 6/7/02 添加对登录屏幕的暂时追踪功能

// somedev2 - 5/22/07 暂时个屁

05 猿们,要学好英语啊

还有些注释里,包含着前辈程序员的语重心长:要学好英语啊!否则是会闹笑话的。

蓝翔毕业不要紧,重要的是缩写不能乱写:

9fa8df2dfac89ede6c574f9514204c3b.png

否则后果很严重:

# 不要再用 anal 做变量名了
# 你们想用 anal 这个缩写来表示analyze(分析),可是 anal 这个单词的意思是“肛门”
# 我特么在哪都能看到 anal 这个词!
# 请不要再这么做了!
# 你们要用就用analyze,或者xbvvzr,要不然用什么其他的都可以。就是别写成 anal_insert 或者 anal_check了
# insert是插入的意思,check是检查的意思,自行脑补吧
06 喜提彩蛋,招聘了解一下

如果你有一双善于发现的眼睛,也许你就能看到,那些藏在Console里的招聘广告。也许,你从此就走上了升职加薪的人生巅峰呢~

081570388341646338fc8ea7519e4327.png 01fb9cf60a28f7b159be14df1bb187e5.png

07离职员工的温馨提醒

不过,跳槽需谨慎。史上最良心注释,碰到这样的坑,就赶紧撤吧。

c86fc336a05324220db2ec2bc113485e.png

08 我的代码就像一首诗

最后,写代码就像写一首诗,就像唱一首歌。

9053ba8140ccecff0637aaebd6a0f6ce.png

这样写注释,代码无bug!

不过,话说回来,你们写代码,居然都写注释?



来源:顶级程序员

收起阅读 »

代码对比工具,我就用这6个

WinMerge会将两个文件内容做对比,并在相异之处以高亮度的方式显示,让使用者可以很快的查知;可以直接让左方的文件内容直接覆盖至右方,或者反过来也可以覆盖。 支持常见的版本控制工具,包括 CVS、subversion、git、mercurial 等,你可以通...
继续阅读 »

WinMerge

pic_2c55c38b.png

WinMerge是一款运行于Windows系统下的文件比较和合并工具,使用它可以非常方便地比较多个文档内容,适合程序员或者经常需要撰写文稿的朋友使用。

WinMerge会将两个文件内容做对比,并在相异之处以高亮度的方式显示,让使用者可以很快的查知;可以直接让左方的文件内容直接覆盖至右方,或者反过来也可以覆盖。

Diffuse

pic_73d4bebc.png

Diffuse在命令行中的速度是相当快的,支持像 C++、Python、Java、XML 等语言的语法高亮显示。可视化比较,非常直观,支持两相比较和三相比较。这就是说,使用 Diffuse 你可以同时比较两个或三个文本文件。

支持常见的版本控制工具,包括 CVS、subversion、git、mercurial 等,你可以通过 Diffuse 直接从版本控制系统获取源代码,以便对其进行比较和合并。

Beyond Compare

pic_45e693a5.png

Beyond Compare可以很方便地对比出两份源代码文件之间的不同之处,相差的每一个字节用颜色加以表示,查看方便,支持多种规则对比。

Beyond Compare选择最好的方法来突出不同之处,文本文件可以用语法高亮和设置比较规则的方法进行查看和编辑,适用于用于文档、源代码和HTML。

Altova DiffDog

pic_4afb57c3.png

pic_b3fd72aa.png

是一款用于文件、目录、数据库模式与表格对比与合并的使用工具。

这个强大易用的对比/合并工具可以让你通过其直观的可视化界面快速比较和合并文本或源代码文件,同步目录以及比较数据库模式与表格。DiffDog还提供了先进XML的差分和编辑功能。

AptDiff

pic_5bfb9a16.png

AptDiff是一个文件比较工具,可以对文本和二进制文件进行比较和合并,适用于软件开发、网络设计和其它的专业领域。

它使用方便,支持键盘快捷键,可以同步进行横向和纵向卷动,支持Unicode格式和大于4GB的文件,可以生成HTML格式的比较报告。

Code Compare

pic_1ff7b983.png

Code Compare是一款用于程序代码文件的比较工具,目前Code Compare支持的对比语言有:C#、C++、CSS、HTML、Java、JavaScrip等代码语言。

Code Compare的运行环境为Visual Studio,而Visual Studio可以方便所有的程序开发设计。

作者:小白学视觉
来源:https://mp.weixin.qq.com/s/I5__jnuDAIJlWbCHJsPDRQ

收起阅读 »

Java之父独家专访:我可太想简化一下 Java了

IEEE Spectrum 2021 年度编程语言排行榜新鲜出炉,不出意料,Java 仍稳居前三。自 1995 年诞生以来,Java 始终是互联网行业炙手可热的编程语言。近年来,新的编程语言层出不穷,Java 如何做到 26 年来盛行不衰?面对技术新趋势,Ja...
继续阅读 »

IEEE Spectrum 2021 年度编程语言排行榜新鲜出炉,不出意料,Java 仍稳居前三。自 1995 年诞生以来,Java 始终是互联网行业炙手可热的编程语言。近年来,新的编程语言层出不穷,Java 如何做到 26 年来盛行不衰?面对技术新趋势,Java 语言将如何发展?在亚马逊云科技 re:Invent 十周年之际,InfoQ 有幸对 Java 父 James Gosling 博士进行了一次独家专访。James Gosling 于 2017 年作为“杰出工程师”加入亚马逊云科技,负责为产品规划和产品发布之类的工作提供咨询支持,并开发了不少原型设计方案。在本次采访中,James Gosling 谈到了 Java 的诞生与发展、他对众多编程语言的看法、编程语言的未来发展趋势以及云计算带来的改变等问题。


Java 的诞生与发展

InfoQ:Java 语言是如何诞生的?是什么激发您创建一门全新的语言?

James Gosling:Java 的诞生其实源于物联网的兴起。当时,我在 Sun 公司工作,同事们都觉得嵌入式设备很有发展前景,而且随着设备数量的激增,整个世界正逐渐向智能化的方向发展。我们投入大量时间与不同行业的从业者进行交流,也拜访了众多东南亚、欧洲的从业者,结合交流心得和行业面临的问题,决定构建一套设计原型。正是在这套原型的构建过程中,我们深刻地意识到当时主流的语言 C++ 存在问题。

最初,我们只打算对 C++ 做出一点小调整,但随着工作的推进、一切很快“失控”了。我们构建出不少非常有趣的设备原型,也从中得到了重要启示。因此,我们及时对方向进行调整,希望设计出某种适用于主流业务和企业计算的解决方案,这正是一切故事的开端。

InfoQ:Java 作为一门盛行不衰的语言,直到现在依旧稳居编程语言的前列,其生命力何在?

James Gosling:Java 得以拥有顽强的生命力背后有诸多原因。

首先,采用 Java 能够非常便捷地进行多线程编程,能大大提升开发者的工作效率。

其次,Java 提供多种内置安全功能,能够帮助开发者及时发现错误、更加易于调试,此外,各种审查机制能够帮助开发者有效识别问题。

第三,热修复补丁功能也非常重要,亚马逊开发者开发出的热补丁修复程序,能够在无须停机的前提下修复正在运行的程序,这是 Java 中非常独特的功能。

第四,Java 拥有很好的内存管理机制,自动垃圾收集大大降低了内存泄露或者双重使用问题的几率。总之,Java 的设计特性确实提升了应用程序的健壮性,特别是极为强大的现代垃圾收集器方案。如果大家用过最新的长期支持版本 JDK17,应该对其出色的垃圾收集器印象深刻。新版本提供多种强大的垃圾收集器,适配多种不同负载使用。另外,现代垃圾收集器停顿时间很短、运行时的资源消耗也非常低。如今,很多用户会使用体量极为庞大的数据结构,而只要内存能容得下这种 TB 级别的数据,Java 就能以极快的速度完成庞大数据结构的构建。

InfoQ:Java 的版本一直以来更新得比较快,几个月前发布了最新的 Java17 版本,但 Java8 仍然是开发人员使用的主要版本,新版本并未“得宠”,您认为主要的原因是什么?

James Gosling:对继续坚守 Java8 的朋友,我想说“是时候作出改变了”。新系统全方位性更强、速度更快、错误也更少、扩展效率更高。无论从哪个角度看,大家都有理由接纳 JDK17。确实,大家在从 JDK8 升级到 JDK9 时会遇到一个小问题,这也是 Java 发展史中几乎唯一一次真正重大的版本更替。大多数情况下,Java 新旧版本更替都非常简单。只需要直接安装新版本,一切就能照常运作。长久以来,稳定、非破坏性的升级一直是 Java 的招牌特性之一,我们也不希望破坏这种良好的印象。

InfoQ:回顾当初,你觉得 Java 设计最成功的点是什么?相对不太满意的地方是什么?

James Gosling:这其实是一种博弈。真正重要的是 Java 能不能以更便利的方式完成任务。我们没办法设想,如果放弃某些问题域,Java 会不会变得更好?或者说,如果我现在重做 Java,在取舍上会有不同吗?区别肯定会有,但我估计我的取舍可能跟大多数人都不一样,毕竟我的编程风格也跟多数人不一样。不过总的来讲,Java 确实还有改进空间。

InfoQ:有没有考虑简化一下 Java?

James Gosling:我可太想简化一下 Java 了。毕竟简化的意义就是放下包袱、轻装上阵。所以 JavaScript 刚出现时,宣传的就是精简版 Java。但后来人们觉得 JavaScript 速度太慢了。在 JavaScript 的早期版本中,大家只是用来执行外部验证之类的简单事务,所以速度还不太重要。但在人们打算用 JavaScript 开发高性能应用时,得出的解决方案就成了 TypeScript。其实我一直觉得 TypeScript 的定位有点搞笑——JavaScript 就是去掉了 Type 的 Java,而 TypeScript 在 JavaScript 的基础上又把 type 加了回来。Type 系统有很多优势,特别是能让系统运行得更快,但也确实拉高了软件开发者的学习门槛。但如果你想成为一名专业的软件开发者,那最好能克服对于学习的恐惧心理。

Java 之父的编程语言之见

InfoQ:一款优秀的现代化编程语言应该是怎样的?当下最欣赏哪一种编程语言的设计理念?

James Gosling:我个人还是会用最简单的评判标准即这种语言能不能改善开发者的日常工作和生活。我尝试过很多语言,哪种更好主要取决于我想干什么。如果我正要编写低级设备驱动程序,那我可能倾向于选择 Rust。但如果需要编写的是用来为自动驾驶汽车建立复杂数据结构的大型导航系统,那我几乎肯定会选择 Java。

InfoQ:数据科学近两年非常热门,众所周知,R 语言和 Python 是数据科学领域最受欢迎的两门编程语言,那么,这两门语言的发展前景怎么样?因具体的应用领域产生专用的编程语言,会是接下来编程语言领域的趋势之一吗?

James Gosling:我是领域特定语言的铁粉,也深切认同这些语言在特定领域中的出色表现。大多数领域特定语言的问题是,它们只能在与世隔绝的某一领域中发挥作用,而无法跨越多个领域。这时候大家更愿意选择 Java 这类语言,它虽然没有针对任何特定领域作出优化,但却能在跨领域时表现良好。所以,如果大家要做的是任何形式的跨领域编程,肯定希望单一语言就能满足所有需求。有时候,大家也会尝试其他一些手段,希望在两种不同的领域特定语言之间架起一道桥梁,但一旦涉及两种以上的语言,我们的头脑通常就很难兼顾了。

InfoQ:Rust 一直致力于解决高并发和高安全性系统问题,这也确实符合当下绝大部分应用场景的需求,对于 Rust 语言的现在和未来您怎么看?

James Gosling:在我看来,Rust 太过关注安全了,这让它出了名的难学。Rust 解决问题的过程就像是证明定理,一步也不能出错。如果我们只需要编写一小段代码,用于某种固定不变的设备,那 Rust 的效果非常好。但如果大家需要构建一套具有高复杂度动态数据结构的大规模系统,那么 Rust 的使用难度就太高了。

编程语言的学习和发展

InfoQ:编程语言倾向于往更加低门槛的方向发展,开发者也更愿意选择学习门槛低的开发语言,一旦一门语言的学习成本过高,开发者可能就不愿意去选择了。对于这样的现象,您怎么看?

James Gosling:要具体问题具体分析。我到底需要 Rust 中的哪些功能特性?我又需要 Java 中的哪些功能特性?很多人更喜欢 Python,因为它的学习门槛真的很低。但跑跑基准测试,我们就会发现跟 Rust 和 Java 相比,Python 的性能实在太差了。如果关注性能,那 Rust 或 Java 才是正确答案。另外,如果你需要的是只有 Rust 能够提供的那种致密、安全、严谨的特性,代码的编写体量不大,而且一旦出问题会造成严重后果,那 Rust 就是比较合适的选择。只能说某些场景下某些语言更合适。Java 就属于比较折衷的语言,虽然不像 Python 那么好学,但也肯定不算难学。

InfoQ:当前,软件项目越来越倾向采用多语言开发,对程序员的要求也越来越高。一名开发人员,应该至少掌握几种语言?最应该熟悉和理解哪些编程语言?

James Gosling:我刚刚入行时,市面上已经有很多语言了。我学了不少语言,大概有几十种吧。但很多语言的诞生本身就很荒谬、很没必要。很多语言就是同一种语言的不同方言,因为它们只是在用不同的方式实现基本相同的语言定义。最让我振奋的是我生活在一个能够致力于解决问题的世界当中。Java 最大的吸引力也正在于此,它能帮助我们解决几乎任何问题。具有普适性的语言地位高些、只适用于特定场景的语言则地位低些,对吧?所以到底该学什么语言,取决于你想解决什么问题、完成哪些任务。明确想要解决什么样的问题才是关键。

InfoQ:2021 年,技术圈最热门的概念非元宇宙莫属,您认为随着元宇宙时代的到来,新的应用场景是否会对编程语言有新的需求?可否谈谈您对未来编程语言的理解?

James Gosling:其实人们从很早开始就在构建这类虚拟世界系统了,所以我觉得元宇宙概念对编程不会有什么影响。唯一的区别是未来我们可以漫步在这些 3D 环境当中,类似于大型多人游戏那种形式。其实《我的世界》就是用户构建型元宇宙的雏形嘛,所以这里并没有什么真正新鲜的东西,仍然是游戏粉加上社交互动机制的组合。我还想强调一点,虚拟现实其实没什么意思。我更重视与真实人类的面对面互动,真的很难想象自己有一天会跟独角兽之类的虚拟形象聊天。

写在最后:云计算带来的改变

InfoQ:您最初是从什么时候或者什么具体事件开始感受到云计算时代的到来的?

James Gosling:云计算概念的出现要远早出云计算的真正实现。因为人们一直把计算机摆在大机房里,再通过网络连接来访问,这其实就是传统的 IT 服务器机房,但这类方案维护成本高、建造成本高、扩展成本也高,而且对于人员技能等等都有着很高的要求。如果非要说,我觉得多租户云的出现正是云计算迎来飞跃的关键,这时候所有的人力与资本支出都由云服务商负责处理,企业客户再也不用为此烦心了。他们可以单纯关注自己的业务重心,告别那些没完没了又没有任何差异性可言的繁重工作。

InfoQ:云计算如今已经形成巨大的行业和生态,背后的根本驱动力是什么?

James Gosling:云计算的驱动力实际上与客户当前任务的实际规模有很大关系。过去几年以来,数字化转型已经全面掀起浪潮,而这波转型浪潮也凸显出新的事实,即我们还有更多的探索空间和机遇,例如,现在人们才刚刚开始探索真正的机器学习能做些什么,能够以越来越有趣且多样的方法处理大规模数据,开展数据分析,获取洞见并据此做出决策,而这一切既是客户需求,也为我们指明了接下来的前进方向。亚马逊云科技做为云科技领导者,引领着云科技的发展,改变着 IT 世界,切实解决了企业客户的诸多痛点。

作者:张雅文
来源:https://mp.weixin.qq.com/s/B4_YaVrnltm54aV4cW1XpA

收起阅读 »

cURL开源作者怒怼“白嫖”企业:我不删库跑路,但答疑得付钱!

cURL 作者 Daniel Stenberg 在 1 月 21 日收到了一家美国《财富》500 强企业发来的电子邮件,要求 Stenberg 回答关于 cURL 是否受到 Log4Shell 漏洞影响以及如何处理等问题。随后,他将邮件内容截图发到了推特上,并...
继续阅读 »

cURL 作者 Daniel Stenberg 在 1 月 21 日收到了一家美国《财富》500 强企业发来的电子邮件,要求 Stenberg 回答关于 cURL 是否受到 Log4Shell 漏洞影响以及如何处理等问题。随后,他将邮件内容截图发到了推特上,并写道:

如果你是一家价值数十亿美元的公司还关注 Log4j,怎么不直接给那些你从未支付过任何费用的 OSS 作者发邮件,要求他们 24 小时内免费回复你这么多问题?

这件事迅速引发了网友们的关注。

把开源当成供应商

根据公开的邮件内容,这家《财富》500 强企业(暂且称为“NNNN”)将 Daniel 团队当成了产品供应商,并要求其 24 小时内免费提供关于 Log4j 漏洞的解决方案。下面是 NNNN 要求 Stenberg 回答的问题:

  1. 如果您在应用程序中使用了 Java 日志库,那么正在运行的是哪些 Log4j 版本?
  2. 贵公司是否发生过任何已确认的安全事件?
  3. 如果是,哪些应用程序、产品、服务和相关版本会受到影响?
  4. 哪些 NNNN 产品和服务受到影响?
  5. NNNN 的非公开或个人信息是否会受到影响?
  6. 如果是,请立即向 NNNN 提供详细信息。
  7. 什么时候完成修复?列出每个步骤,包括每个步骤的完成日期。
  8. NNNN 需要采取什么行动来完成此修复?

pic_16829db5.png

cURL(client URL 请求库的简称)是一个命令行接口,用于检索可通过计算机网络访问资源的内容。资源由 URL 指定,并且必须是软件支持的类型。cURL 程序实现了用户界面,基于用 C 语言开发的 libcurl 软件库。

Apache Log4j 日志库被 Java/J2EE 应用开发项目和基于 Java/J2EE 的现成软件解决方案的供应商大量使用。去年 12 月 9 日,Log4j 中被发现了一个漏洞,攻击者通过该漏洞能够进行远程代码执行,具体包括通过受影响的设备或应用程序访问整个网络、运行任何代码、访问受影响设备或应用程序上的所有数据、删除或加密文件等。可以说,cURL 开源代码与 Log4j 漏洞事件毫不相干。

虽然 Stenberg 从未参与过任何 Log4j 的开发工作,也没有任何使用了 Log4j 代码的版权产品,但 Stenberg 还是回复道,“你不是我们的客户,我们也不是你的客户。”并略带调侃地表示,只要双方签了商业合同就很乐意回答所有的问题。

“发邮件”只是例行公事?

“这封电子邮件显示出来的无知和无能程度令人难以置信。”Stenberg 在博文里写道,“很奇怪他们现在才发送关于 Log4j 的查询邮件,这似乎有点晚了。”

“这很可能只是一封发送给数十或数百个其他软件供应商 / 开发人员的模板电子邮件。如果确实来自像我过去工作过的那些大型企业,他们很可能会要求各种 IT 支持和开发团队编制一份企业使用的所有软件 / 工具的列表以及每个软件 / 工具的电子邮件地址。所以,很大可能只是有人按照项目计划中的要求向供应商发送电子邮件以延缓问题,并勾选他们的方框,说明已联系该供应商 / 开发人员。”有网友猜测道。

网友“Jack0r”介绍,其所在公司规定要有一个记载依赖项的 Excel 列表,列表里大多数是开源软件,还有一些封闭源代码和付费产品。开发人员要为每个依赖项设置一个联系人,因此与某软件相关的电子邮件可能会被放入列表中。但这个列表通常非常过时,也没有人专门更新。

“我曾经被要求填写一份 3 页关于 Oracle 数据库的详细资料表,但我们从未使用过 Oracle。有的软件运行在 Postgres 上,有的运行在 MySQL 上,有的运行在 NoSQL 上,但他们说,‘MySQL 是从 Oracle 来的,不是吗?’”网友现身说法。

而当出现严重安全漏洞时,负责 Excel 工作表的人员(非开发人员,也不知道这些依赖项如何使用,甚至不知道它们是什么)必须联系每个依赖项的所有者并向他们提出相同的问题。他们这样做不是为了做有用的事情,只是为了告诉他们的客户“我们正在竭尽全力修复这个漏洞”。大多数情况下,这些甚至要被写进合同中。

Reddit 上也有网友表示,Stenberg 收到的邮件来自对计算机或开源一无所知的律师助理。他只是有一长串的名字要联系,这样就可以为公司建立防御,防止因黑客攻击而被起诉。他甚至不在乎公司是否被黑,也不在乎会不会被起诉,他只关心自己的工作,那就是做好准备,以防万一。

因此,有人庆幸道,这就是为什么开源许可证非常重要的原因。开源许可证保护了作者的权益,同时确保了治理到位是企业的责任。

“只盖房子而不关心地基”

“我认为,这可能是开源金字塔的一个很好例证,上层用户根本不考虑底层设施的维护。只盖房子而不关心地基。”Stenberg 写道。

pic_cd5ffc21.png

开源金字塔的最底部是基础组件、操作系统和库,上面所有的东西都是在此基础上建立的。

越往上走,产品更多是面向终端用户,企业能赚更多的钱,同时产品迭代更快、语言要求层次更高,开放源码的份额也不断减少。在最上面,很多东西已经不是开源的了。反之,越往下走,产品使用寿命更长,语言要求不好,但 bug 的影响更大,修复需要的时间更长,因此维护比重构更重要。在最底部,几乎所有的东西都是开源的,每个组件都被无数的用户所依赖。

只要有可能在不为“公共基础设施”付出很多就能赚到很多钱,那么企业就没有什么动力去投资或支付某些东西的维护费用。但足够好的软件组件也会偶尔出现 bug,但只要这些漏洞没有真正威胁到赚钱的人,这种情况就不会改变。

Stenberg 认为,为依赖项的维护付费有助于降低未来在周末早上过早发出警报的风险。底层组件的开发者们的工作就是要让依赖其组件功能的用户相信,如果他们购买支持,就能更加放心,避免任何隐藏的陷阱。

根据 Linux 基金会和学术研究人员对 FOSS(免费和开源软件)贡献者进行的调查,开发者们花在安全问题上时间低于 3%,同时受访者并不希望增加花在安全上的时间。“安全事业是一项令人沮丧的苦差事”“安全是令人难以忍受的无聊程序障碍”。有足够的资金让工程师将时间花在代码维护上,或许可以降低严重故障的发生率。

与此同时,底层开发者与上层使用者之间的矛盾日益加深。1 月 11 日, Apache PLC4X 的创建者 Christofer Dutz 在 GitHub 发文称,由于得不到任何形式的回报,他将停止对 PLC4X 的企业用户提供免费的社区支持。若后续仍无企业愿意站出来资助项目,他将停止对 PLC4X 的维护和任何形式的支持。

有的组件可能被成千上万家公司用于一项很小而重要的任务,有的是与 Apache PLC4x 一样,可能只有一个少数组织形成的自然市场。但目前没有具体办法来衡量使用组件给企业带来的收益,更没有一个通用方案可以用来收集和分配企业对开源项目的捐款。

开源可持续性问题的解决已经迫在眉睫。

作者:褚杏娟
来源:https://mp.weixin.qq.com/s/G_47x6D8-KXozSy8XWGXbg

收起阅读 »

这才是Yaml的语法精髓, 不要再只有字符串了

文章目录什么是YAML基本语法数据类型标量对象数组文本块显示指定类型引用单文件多配置什么是YAMLYAML是"YAML Ain’t a Markup Language"(YAML不是一种标记语言)的递归缩写。YAML的意思其实是:“Yet Another Ma...
继续阅读 »

文章目录

  • 什么是YAML

  • 基本语法

  • 数据类型

    • 标量

    • 对象

    • 数组

  • 文本块

  • 显示指定类型

  • 引用

  • 单文件多配置

什么是YAML

YAML是"YAML Ain’t a Markup Language"(YAML不是一种标记语言)的递归缩写。YAML的意思其实是:“Yet Another Markup Language”(仍是一种标记语言)。主要强度这种语音是以数据为中心,而不是以标记语音为重心,例如像xml语言就会使用大量的标记。

YAML是一个可读性高,易于理解,用来表达数据序列化的格式。它的语法和其他高级语言类似,并且可以简单表达清单(数组)、散列表,标量等数据形态。它使用空白符号缩进和大量依赖外观的特色,特别适合用来表达或编辑数据结构、各种配置文件等。

YAML的配置文件后缀为 .yml,例如Springboot项目中使用到的配置文件 application.yml

基本语法

  • YAML使用可打印的Unicode字符,可使用UTF-8或UTF-16。

  • 数据结构采用键值对的形式,即 键名称: 值,注意冒号后面要有空格。

  • 每个清单(数组)成员以单行表示,并用短杠+空白(- )起始。或使用方括号([]),并用逗号+空白(, )分开成员。

  • 每个散列表的成员用冒号+空白(: )分开键值和内容。或使用大括号({ }),并用逗号+空白(, )分开。

  • 字符串值一般不使用引号,必要时可使用,使用双引号表示字符串时,会转义字符串中的特殊字符(例如\n)。使用单引号时不会转义字符串中的特殊字符。

  • 大小写敏感

  • 使用缩进表示层级关系,缩进不允许使用tab,只允许空格,因为有可能在不同系统下tab长度不一样

  • 缩进的空格数可以任意,只要相同层级的元素左对齐即可

  • 在单一文件中,可用连续三个连字号(—)区分多个文件。还有选择性的连续三个点号(…)用来表示文件结尾。

  • '#'表示注释,可以出现在一行中的任何位置,单行注释

  • 在使用逗号及冒号时,后面都必须接一个空白字符,所以可以在字符串或数值中自由加入分隔符号(例如:5,280或http://www.wikipedia.org)而不需要使用引号。

数据类型

  • 纯量(scalars):单个的、不可再分的值

  • 对象:键值对的集合,又称为映射(mapping)/ 哈希(hashes) / 字典(dictionary)

  • 数组:一组按次序排列的值,又称为序列(sequence) / 列表(list)

标量

标量是最基础的数据类型,不可再分的值,他们一般用于表示单个的变量,有以下七种:

  1. 字符串

  2. 布尔值

  3. 整数

  4. 浮点数

  5. Null

  6. 时间

  7. 日期

# 字符串
string.value: Hello!我是陈皮!
# 布尔值,true或false
boolean.value: true
boolean.value1: false
# 整数
int.value: 10
int.value1: 0b1010_0111_0100_1010_1110 # 二进制
# 浮点数
float.value: 3.14159
float.value1: 314159e-5 # 科学计数法
# Null,~代表null
null.value: ~
# 时间,时间使用ISO 8601格式,时间和日期之间使用T连接,最后使用+代表时区
datetime.value: !!timestamp 2021-04-13T10:31:00+08:00
# 日期,日期必须使用ISO 8601格式,即yyyy-MM-dd
date.value: !!timestamp 2021-04-13

这样,我们就可以在程序中引入了,如下:

@RestController
@RequestMapping("demo")
public class PropConfig {
   
   @Value("${string.value}")
   private String stringValue;

   @Value("${boolean.value}")
   private boolean booleanValue;

   @Value("${boolean.value1}")
   private boolean booleanValue1;

   @Value("${int.value}")
   private int intValue;

   @Value("${int.value1}")
   private int intValue1;

   @Value("${float.value}")
   private float floatValue;

   @Value("${float.value1}")
   private float floatValue1;

   @Value("${null.value}")
   private String nullValue;

   @Value("${datetime.value}")
   private Date datetimeValue;

   @Value("${date.value}")
   private Date datevalue;
}

对象

我们知道单个变量可以用键值对,使用冒号结构表示 key: value,注意冒号后面要加一个空格。可以使用缩进层级的键值对表示一个对象,如下所示:

person:
 name: 陈皮
 age: 18
 man: true

然后在程序对这几个属性进行赋值到Person对象中,注意Person类要加get/set方法,不然属性会无法正确取到配置文件的值。使用@ConfigurationProperties注入对象,@value不能很好的解析复杂对象。

package com.nobody;

import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

/**
* @Description
* @Author Mr.nobody
* @Date 2021/4/13
* @Version 1.0.0
*/
@Configuration
@ConfigurationProperties(prefix = "my.person")
@Getter
@Setter
public class Person {
   private String name;
   private int age;
   private boolean man;
}

当然也可以使用 key:{key1: value1, key2: value2, ...}的形式,如下:

person: {name: 陈皮, age: 18, man: true}

数组

可以用短横杆加空格 -开头的行组成数组的每一个元素,如下的address字段:

person:
 name: 陈皮
 age: 18
 man: true
 address:
   - 深圳
   - 北京
   - 广州

也可以使用中括号进行行内显示形式,如下:

person:
 name: 陈皮
 age: 18
 man: true
 address: [深圳, 北京, 广州]

在代码中引入方式如下:

package com.nobody;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import java.util.List;

/**
* @Description
* @Author Mr.nobody
* @Date 2021/4/13
* @Version 1.0.0
*/
@Configuration
@ConfigurationProperties(prefix = "person")
@Getter
@Setter
@ToString
public class Person {
 
   
   
   private String name;
   private int age;
   private boolean man;
   private List<String> address;
}

如果数组字段的成员也是一个数组,可以使用嵌套的形式,如下:

person:
name: 陈皮
age: 18
man: true
address: [深圳, 北京, 广州]
twoArr:
-
- 2
- 3
- 1
-
- 10
- 12
- 30
package com.nobody;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import java.util.List;

@Configuration
@ConfigurationProperties(prefix = "person")
@Getter
@Setter
@ToString
public class Person {



private String name;
private int age;
private boolean man;
private List<String> address;
private List<List<Integer>> twoArr;
}

如果数组成员是一个对象,则用如下两种形式形式:

childs:
-
name: 小红
age: 10
-
name: 小王
age: 15
childs: [{name: 小红, age: 10}, {name: 小王, age: 15}]

文本块

如果你想引入多行的文本块,可以使用|符号,注意在冒号:|符号之间要有空格。

person:
name: |
Hello Java!!
I am fine!
Thanks! GoodBye!

它和加双引号的效果一样,双引号能转义特殊字符:

person:
name: "Hello Java!!\nI am fine!\nThanks! GoodBye!"

显示指定类型

有时我们需要显示指定某些值的类型,可以使用 !(感叹号)显式指定类型。!单叹号通常是自定义类型,!!双叹号是内置类型,例如:

# 指定为字符串
string.value: !!str HelloWorld!
# !!timestamp指定为日期时间类型
datetime.value: !!timestamp 2021-04-13T02:31:00+08:00

内置的类型如下:

  • !!int:整数类型

  • !!float:浮点类型

  • !!bool:布尔类型

  • !!str:字符串类型

  • !!binary:二进制类型

  • !!timestamp:日期时间类型

  • !!null:空值

  • !!set:集合类型

  • !!omap,!!pairs:键值列表或对象列表

  • !!seq:序列

  • !!map:散列表类型

引用

引用会用到 &锚点符合和 *星号符号,&用来建立锚点,<< 表示合并到当前数据,* 用来引用锚点。

xiaohong: &xiaohong
name: 小红
age: 20

dept:
id: D15D8E4F6D68A4E88E
<<: *xiaohong

上面最终相当于如下:

xiaohong:
name: 小红
age: 20

dept:
id: D15D8E4F6D68A4E88E
name: 小红
age: 20

还有一种文件内引用,引用已经定义好的变量,如下:

base.host: https://chenpi.com
add.person.url: ${base.host}/person/add

单文件多配置

可以在同一个文件中,实现多文档分区,即多配置。在一个yml文件中,通过 — 分隔多个不同配置,根据spring.profiles.active 的值来决定启用哪个配置

#公共配置
spring:
profiles:
active: pro # 指定使用哪个文档块
---
#开发环境配置
spring:
profiles: dev # profiles属性代表配置的名称

server:
port: 8080
---
#生产环境配置
spring:
profiles: pro

server:
port: 8081

作者:陈皮的JavaLib
来源:https://blog.csdn.net/chenlixiao007/article/details/115654824

收起阅读 »

程序员连相7天亲:规划有多重要!

人家程序员相7个亲都有个规划,身为凡事皆是项目,凡事皆有规划的项目经理,你呢?有相亲对象吗? 相亲必备SOP流程,直接拿走不谢第一步: 明确问题第二步:了解现状在一年之内将T对我好感由1星提升为8星 第四步: 把握真因找对象也一样。下面这个四句口诀,请牢记:...
继续阅读 »

近日,北京一程序员小伙提前放假,避开了连上7天班,不料被家人安排了7场相亲,每天一场。他结合相亲对象性格、爱好等认真制作了规划,并准备了不同年货礼物,感叹好累…


人家程序员相7个亲都有个规划,身为凡事皆是项目,凡事皆有规划的项目经理,你呢?有相亲对象吗? 相亲必备SOP流程,直接拿走不谢

第一步: 明确问题

找到自己的爱人项目在立项之前,最重要的是,先明确做这个项目是为了要解决的问题是什么。要不然这个项目本身就是不成立的。

现在你要解决的问题就是:找对象。而立之年,恋爱结婚是你当前的一大任务!(注:那些本身就不想结婚的人不在我们讨论范围之内。)

第二步:了解现状

好感度等级

我们在做计划之前必须进行一些分析和定义,这样我们才能有针对性的制定计划首先我们对“潜在对象对你的好感度”做等级划分如下:
第三步:设定目标

在一年之内将T对我好感由1星提升为8星

第四步: 把握真因

了解自己和对方真实需求

项目经理在跟甲方对接需求的时候,一定要先了解对方的真实原因。

找对象也一样。下面这个四句口诀,请牢记:

了解自己的优势,扬长避短;

了解目标的界定,聚焦战略;

了解对方的全况,知己知彼;

了解对方的需求,对症下药;

第五步: 制定对策

了解一切的可能,制造机缘

首先,确定T的日常作息。这个可以直接问本人,如果认识T的朋友同学会更好。比如,有的姑娘每周五下班会去超市采购,你就可以也在同样的时间点去偶遇。

其次,了解对方的兴趣爱好。比如T喜欢打篮球,跟他朋友打听一下T经常去什么地方打篮球;如果T喜欢看书,那就去看看T喜欢什么类型的书,增加以后聊天的话题。

总之,就是寻找突破口,制造更多在一起的时间增进彼此了解。

举个例子:

第六步:实施对策

用逻辑树来进行归纳整理以便更好的实施


:以上只是作为举例,具体实践请根据你们交往发展情况而定。

第七步: 评价结果和过程

关注实施方案,随时修正

在采取行动的过程中,要保持警惕,用思维导图分析不良效果的原因,时刻注意对方的变化,采取相应对策化解,修正自己的行为。

使用思维导图进行原因分析:

针对问题列出措施改进方法:

第八步: 评估方法并标准化

了解力量的消长,成功或退出

理性的三个关键词:

  • 敏感:上帝存在于细节之中,如果是你要怎么把握对方的话。

  • 关爱:爱胜在付出,每个人都在为爱播种,是否结果看你照顾多少。

  • 尊严:人最后的防线就是自尊,不要为了恋爱却失去了尊严。

作者:圈圈
来源:https://mp.weixin.qq.com/s/GNPVc5qgOpMgnAeeQlAwrw

收起阅读 »

研究生写脚本抢HPV九价疫苗:被采取强制措施,后果严重

近日,江西省南昌市公安局网安部门报道了一起涉嫌破坏计算机信息系统罪的案件。嫌疑人刘某被采取刑事强制措施,案件还在进一步办理中。1、贴心男友为爱写代码适用于16-26岁女性的HPV九价疫苗让许多年轻女性十分焦虑,经常出现“一苗难求”的现象,这不仅催生了各式各样的...
继续阅读 »
近日,江西省南昌市公安局网安部门报道了一起涉嫌破坏计算机信息系统罪的案件。嫌疑人刘某被采取刑事强制措施,案件还在进一步办理中。

1、贴心男友为爱写代码
适用于16-26岁女性的HPV九价疫苗让许多年轻女性十分焦虑,经常出现“一苗难求”的现象,这不仅催生了各式各样的黄牛,甚至还有专业技术和团队为此走上违法犯罪的道路。
江西南昌某大学研究生刘某在得知女友因约不上HPV九价疫苗感到烦恼时,决定替女友排忧解难。2021年11月4日,他登录南昌某医院APP,帮女友代抢,没想到女友大半年都没抢到的疫苗,他一次便预约成功!
高兴之余,刘某便发了个小红书进行炫耀。不料引来许多同城网民私聊,询问能不能帮忙代抢,面对高额佣金,小刘在评估自己的编程技术后,决定用“特殊手段”开启发财道路。
他使用编码程序编写了如下代码,并在各大平台发布代抢信息。


2、事情败露
然而没过多久,原本畅通无阻的发财之路却被医院方面觉察到了异常。
医院的工作人员发现,其院九价疫苗预约成功患者大部分都是通过黄牛途径取得挂号的,且其医院系统存在被破坏干扰的痕迹,遂立即前往南昌市公安局网安部门报案。
警方迅速立案侦查,锁定犯罪嫌疑人刘某。经警方工作,2021年12月26日晚,刘某前往大队投案自首,并对其违法行为供认不讳。
目前,刘某因涉嫌破坏计算机信息系统罪已被公安机关依法采取刑事强制措施,案件还在进一步办理中。

3、网友:高端的犯罪只需要最普通的脚本?
1月18日,话题#研究生编代码有偿帮抢HPV九价疫苗#登上微博热搜,阅读讨论数达2.1亿次。有网友认为这就是典型的“知识改变命运,没点学问,连个疫苗都抢不到”。
外行看热闹,内行看门道。不少细心的程序员发现刘某用的代码竟然是vbs!
网友@老李滔滔不绝:一看到findcolor,这是安卓上的点击助手啊,档次未免太低了点,大概率是按键精灵的vbs脚本~
网友@左横有撇:这啥脚本?写个Python不香吗
网友@压电蜂鸣片:如果只是脚本autojs之类的,能算破坏计算机信息系统罪吗?秒杀器也不犯法啊,只是程序帮人点罢了
网友@胖胖的我:Github上就有代抢脚本的开源项目。而且作者是免费分享交流。已经修改了好几版了。怕不是就是照搬过来修改了一下。
网友@哈喽:好家伙,还是VB代码
网友@奋斗啊:但凡学过Python的应该都会整这种APP吧
网友@1米65的高大男子:虽然但是,计算机男朋友真香
你会自己写代码抢东西么?欢迎参与投票~

参考链接:

https://mp.weixin.qq.com/s/Umq6UjeKD0kwgyZgVA28zA

https://weibo.com/5044281310/LbhRgewXz

    整理 | 王晓曼

收起阅读 »

掉了两根头发,可算是把volatile整明白了

本来想着快过年了偷个懒休息下,没想到被兄弟们连续催更,没办法,博主暖男嘛,掐着人中也要更,兄弟们卷起来volatile关键字可以说是Java虚拟机提供的最轻量级的同步机制,但对于为什么它只能保证可见性,不保证原子性,它又是如何禁用指令重排的,还有很多同学没彻底...
继续阅读 »

本来想着快过年了偷个懒休息下,没想到被兄弟们连续催更,没办法,博主暖男嘛,掐着人中也要更,兄弟们卷起来

volatile关键字可以说是Java虚拟机提供的最轻量级的同步机制,但对于为什么它只能保证可见性,不保证原子性,它又是如何禁用指令重排的,还有很多同学没彻底理解

相信我,坚持看完这篇文章,你将牢牢掌握一个Java核心知识点

先说它的两个作用:

  • 保证变量在内存中对线程的可见性

  • 禁用指令重排

每个字都认识,凑在一起就麻了

这两个作用通常很不容易被我们Java开发人员正确、完整地理解,以至于许多同学不能正确地使用volatile

关于可见性

不多bb,码来

public class VolatileTest {
   private static volatile int count = 0;
   
   private static void increase() {
   count++;
  }

   public static void main(String[] args) throws InterruptedException {
       for (int i = 0; i < 10; i++) {
           new Thread(() -> {
               for (int j = 0; j < 10000; j++) {
                   increase();
              }
          }).start();
      }
       // 所有线程累加完成后输出
       while (Thread.activeCount() > 2) Thread.yield();
       System.out.println(count);
  }
}

代码很好理解,开了十个线程对同一个共享变量count做累加,每个线程累加1w次

count我们已经用volatile修饰,已经保证了count对十个线程在内存中的可见性,按理说十个线程执行完毕count的值应该10w

然鹅,运行多次,结果都远小于期望值


是哪个环节出了问题?


你肯定听过一句话:volatile只保证可见性,不保证原子性

这句话就是答案,但是依旧很多人没搞懂其中的奥秘

说来话长我长话短说,简单来讲就是 count++这个操作不是原子的,它是分三步进行

  1. 从内存读取 count 的值

  2. 执行 count + 1

  3. 将 count 的新值写回

要彻底搞懂这个问题,我们得从字节码入手

下面是increase方法编译后的字节码


看不懂没关系,我们一行一行来看:

  1. GETSTATIC:读取 count 的当前值

  2. ICONST_1:将常量 1 加载到栈顶

  3. IADD:执行+1

  4. PUTSTATIC:写入count最新值

ICONST_1和IADD其实就是真正的++操作

关键点来了,volatile只能保证线程在GETSTATIC这一步拿到的值是最新的,但当该线程执行到下面几行指令时,这期间可能就有其它线程把count的值修改了,最终导致旧值把真正的新值覆盖

懂我意思吗

所以,并发编程中,只靠volatile修饰共享变量是不可靠的,最终还是要通过对关键方法加锁来保证线程安全

就如上面的demo,稍加修改就能实现真正的线程安全

最简单的,给increase方法加个synchronized (synchronized怎么实现线程安全的我就不啰嗦了,我以前讲过 synchronized底层实现原理)

private synchronized static void increase() {
   ++count;
}

run几下


这不就妥了嘛

到现在,对于以下两点你应该有了新的认知

  1. volatile保证变量在内存中对线程的可见性

  2. volatile只保证可见性,不保证原子性

关于指令重排

并发编程中,cpu自身和虚拟机为了提高执行效率,都会采用指令重排(在保证不影响结果的前提下,将某些代码乱序执行)

  1. 关于cpu:为了从分利用cpu,实际执行指令时会做优化;

  2. 关于虚拟机:在HotSpot vm中,为了提升执行效率,JIT(即时编译)模式也会做指令优化

指令重排在大部分场景下确实能提升执行效率,但有些场景对代码执行顺序是强依赖的,此时我们需要禁用指令重排,如下面这个场景


伪代码取自《深入理解Java虚拟机》:

其描述的场景是开发中常见配置读取过程,只是我们在处理配置文件时一般不会出现并发,所以没有察觉这会有问题。
试想一下,如果定义initialized变量时没有使用volatile修饰,就可能会由于指令重排序的优化,导致位于线程A中最后一条代码“initialized=true”被提前执行(这里虽然使用Java作为伪代码,但所指的重排序优化是机器级的优化操作,提前执行是指这条语句对应的汇编代码被提前执行),这样在线程B中使用配置信息的代码就可能出现错误,而volatile通过禁止指令重排则可以避免此类情况发生

禁用指令重排只需要将变量声明为volatile,是不是很神奇

我们来看看volatile是如何实现禁用指令重排的

也借用《深入理解Java虚拟机》的一个例子吧,比较好理解


这是个单例模式的实现,下面是它的部分字节码,红框中 mov%eax,0x150(%esi) 是对instance赋值


可以看到,在赋值后,还执行了 lock addl$0x0,(%esp) 指令,关键点就在这儿,这行指令相当于此处设置了个 内存屏障 ,有了内存屏障后,cpu或虚拟机在指令重排时就不能把内存屏障后面的指令提前到内存屏障前面,好好捋一下这段话

最后,留一个能加深大家对volatile理解的问题,兄弟们好好思考下:

Java代码明明是从上往下依次执行,为什么会出现指令重排这个问题?

ok我话说完
————————————————
作者:负债程序猿
来源:https://blog.csdn.net/qq_33709582/article/details/122415754

收起阅读 »

什么样的问题应该使用动态规划

说起动态规划,我不知道你有没有这样的困扰,在掌握了一些基础算法和数据结构之后,碰到一些较为复杂的问题还是无从下手,面试时自然也是胆战心惊。如果我说动态规划是个玄幻的问题其实也不为过。究其原因,我觉得可以归因于这样两点:你对动态规划相关问题的套路和思想还没有完全...
继续阅读 »

说起动态规划,我不知道你有没有这样的困扰,在掌握了一些基础算法和数据结构之后,碰到一些较为复杂的问题还是无从下手,面试时自然也是胆战心惊。如果我说动态规划是个玄幻的问题其实也不为过。究其原因,我觉得可以归因于这样两点:

  • 你对动态规划相关问题的套路和思想还没有完全掌握;

  • 你没有系统地总结过究竟有哪些问题可以用动态规划解决。

知己知彼,你想把动态规划作为你的面试武器之一,就得足够了解它;而应对面试,总结、归类问题其实是个不错的选择,这在我们刷题的时候其实也能感觉得到。那么,我们就针对以上两点,系统地谈一谈究竟什么样的问题可以用动态规划来解。

一、动态规划是一种思想

动态规划算法,这种叫法我想你应该经常听说。嗯,从道理上讲这么叫我觉得也没错,首先动态规划它不是数据结构,这一点毋庸置疑,并且严格意义上来说它就是一种算法。但更加准确或者更加贴切的提法应该是说动态规划是一种思想。那算法和思想又有什么区别呢?

一般来说,我们都会把算法和数据结构放一起来讲,这是因为它们之间密切相关,而算法也往往是在特定数据结构的基础之上对解题方案的一种严谨的总结。

比如说,在一个乱序数组的基础上进行排序,这里的数据结构指的是什么呢?很显然是数组,而算法则是所谓的排序。至于排序算法,你可以考虑使用简单的冒泡排序或效率更高的快速排序方法等等来解决问题。

没错,你应该也感觉到了,算法是一种简单的经验总结和套路。那什么是思想呢?相较于算法,思想更多的是指导你我来解决问题。

比如说,在解决一个复杂问题的时候,我们可以先将问题简化,先解决简单的问题,再解决难的问题,那么这就是一种指导解决问题的思想。另外,我们常说的分治也是一种简单的思想,当然它在诸如归并排序或递归算法当中会常常被提及。

而动态规划就是这样一个指导我们解决问题的思想:你需要利用已经计算好的结果来推导你的计算,即大规模问题的结果是由小规模问题的结果运算得来的

总结一下:算法是一种经验总结,而思想则是用来指导我们解决问题的。既然动态规划是一种思想,那它实际上就是一个比较抽象的概念了,也很难和实际的问题关联起来。所以说,弄清楚什么样的问题可以使用动态规划来解就显得十分重要了。

二、动态规划问题的特点

动态规划作为运筹学上的一种最优化解题方法,在算法问题上已经得到广泛应用。接下来我们就来看一下动归问题所具备的一些特点。

2.1 最优解问题

除非你碰到的问题是简单到找出一个数组中最大的值这样,对这种问题来说,你可以对数组进行排序,然后取数组头或尾部的元素,如果觉得麻烦,你也可以直接遍历得到最值。不然的话,你就得考虑使用动态规划来解决这个问题了。这样的问题一般都会让你求最大子数组、求最长递增子数组、求最长递增子序列或求最长公共子串、子序列等等。

如果碰到求最值问题,我们可以使用下面的套路来解决问题:

  • 优先考虑使用贪心算法的可能性;

  • 然后是暴力递归进行穷举,针对数据规模不大的情况;

  • 如果上面两种都不适合,那么再选择动态规划。

可以看到,求解动态规划的核心问题其实就是穷举。当然了,动态规划问题也不会这么简单了事,我们还需要考虑待解决的问题是否存在重叠子问题、最优子结构等特性。

清楚了动态规划算法的特点,接下来我们就来看一下哪些问题适合用动态规划思想来解题。

1. 乘积最大子数组

给你一个整数数组 numbers,找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字),返回该子数组的乘积。

示例1:
输入: [2,7,-2,4]
输出: 14
解释: 子数组 [2,7] 有最大乘积 14。


示例2:
输入: [-5,0,3,-1]
输出: 3
解释: 结果不能为 15, 因为 [-5,3,-1] 不是子数组,是子序列。

首先,很明显这个题目当中包含一个“最”字,使用动态规划求解的概率就很大。这个问题的目的就是从数组中寻找一个最大的连续区间,确保这个区间的乘积最大。由于每个连续区间可以划分成两个更小的连续区间,而且大的连续区间的结果是两个小连续区间的乘积,因此这个问题还是求解满足条件的最大值,同样可以进行问题分解,而且属于求最值问题。同时,这个问题与求最大连续子序列和比较相似,唯一的区别就是你需要在这个问题里考虑正负号的问题,其它就相同了。

对应实现代码:

class Solution {
public:
   int maxProduct(vector<int>& nums) {
       if(nums.empty()) return 0;

       int curMax = nums[0];
       int curMin = nums[0];
       int maxPro = nums[0];
       for(int i=1; i<nums.size(); i++){
           int temp = curMax;    // 因为curMax在下一行可能会被更新,所以保存下来
           curMax = max(max(curMax*nums[i], nums[i]), curMin*nums[i]);
           curMin = min(min(curMin*nums[i], nums[i]), temp*nums[i]);
           maxPro = max(curMax, maxPro);
       }
       return maxPro;
   }
};

2. 最长回文子串

问题:给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。

示例1:
输入: "babad"
输出: "bab"


示例2:
输入: "cbbd"
输出: "bb"

【回文串】是一个正读和反读都一样的字符串,比如“level”或者“noon”等等就是回文串。这个问题依然包含一个“最”字,同样由于求解的最长回文子串肯定包含一个更短的回文子串,因此我们依然可以使用动态规划来求解这个问题。

对应实现代码:

class Solution {
      public boolean isPalindrome(String s, int b, int e){//判断s[b...e]是否为回文字符串
      int i = b, j = e;
      while(i <= j){
          if(s.charAt(i) != s.charAt(j)) return false;
          ++i;
          --j;
      }
      return true;
  }
  public String longestPalindrome(String s) {
      if(s.length() <=1){
          return s;
      }
      int l = 1, j = 0, ll = 1;
      for(int i = 1; i < s.length(); ++i){
            //下面这个if语句就是用来维持循环不变式,即ll恒表示:以第i个字符为尾的最长回文子串的长度
            if(i - 1 - ll >= 0 && s.charAt(i) == s.charAt(i-1-ll)) ll += 2;
            else{
                while(true){//重新确定以i为边界,最长的回文字串长度。确认范围为从ll+1到1
                    if(ll == 0||isPalindrome(s,i-ll,i)){
                        ++ll;
                        break;
                    }
                    --ll;
                }
            }
            if(ll > l){//更新最长回文子串信息
              l = ll;
              j = i;
          }
      }
      return s.substring(j-l+1, j+1);//返回从j-l+1到j长度为l的子串
  }
}

3. 最长上升子序列

问题:给定一个无序的整数数组,找到其中最长上升子序列的长度。可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。

示例:
输入: [10,9,2,5,3,7,66,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,66],它的长度是 4。

这个问题依然是一个最优解问题,假设我们要求一个长度为 5 的字符串中的上升自序列,我们只需要知道长度为 4 的字符串最长上升子序列是多长,就可以根据剩下的数字确定最后的结果。
对应实现代码:

class Solution {
   public int lengthOfLIS(int[] nums) {
       if(nums.length == 0) return 0;
       int[] dp = new int[nums.length];
       int res = 0;
       Arrays.fill(dp, 1);
       for(int i = 0; i < nums.length; i++) {
           for(int j = 0; j < i; j++) {
               if(nums[j] < nums[i]) dp[i] = Math.max(dp[i], dp[j] + 1);
           }
           res = Math.max(res, dp[i]);
       }
       return res;
   }
}

2.2 求可行性

如果有这样一个问题,让你判断是否存在一条总和为 x 的路径(如果找到了,就是 True;如果找不到,自然就是 False),或者让你判断能否找到一条符合某种条件的路径,那么这类问题都可以归纳为求可行性问题,并且可以使用动态规划来解。

1. 凑零兑换问题

问题:给你 k 种面值的硬币,面值分别为 c1, c2 … ck,每种硬币的数量无限,再给一个总金额 amount,问你最少需要几枚硬币凑出这个金额,如果不可能凑出,算法返回 -1 。

示例1:
输入: c1=1, c2=2, c3=5, c4=7, amount = 15
输出: 3
解释: 11 = 7 + 7 + 1。


示例2:
输入: c1=3, amount =7
输出: -1
解释: 3怎么也凑不到7这个值。

这个问题显而易见,如果不可能凑出我们需要的金额(即 amount),最后算法需要返回 -1,否则输出可能的硬币数量。这是一个典型的求可行性的动态规划问题。

对于示例代码:

class Solution {
   public int coinChange(int[] coins, int amount) {
       if(coins.length == 0)
           return -1;
       //声明一个amount+1长度的数组dp,代表各个价值的钱包,第0个钱包可以容纳的总价值为0,其它全部初始化为无穷大
       //dp[j]代表当钱包的总价值为j时,所需要的最少硬币的个数
       int[] dp = new int[amount+1];
       Arrays.fill(dp,1,dp.length,Integer.MAX_VALUE);
       for (int coin : coins) {
           for (int j = coin; j <= amount; j++) {
               if(dp[j-coin] != Integer.MAX_VALUE) {
                   dp[j] = Math.min(dp[j], dp[j-coin]+1);
              }
          }
      }
       if(dp[amount] != Integer.MAX_VALUE)
           return dp[amount];
       return -1;
  }
}

2. 字符串交错组成问题

问题:给定三个字符串 s1, s2, s3, 验证 s3 是否是由 s1 和 s2 交错组成的。

示例1:
输入: s1="aabcc",s2 ="dbbca",s3="aadbbcbcac"
输出: true
解释: 可以交错组成。


示例2:
输入: s1="aabcc",s2="dbbca",s3="aadbbbaccc"
输出: false
解释:无法交错组成。

这个问题稍微有点复杂,但是我们依然可以通过子问题的视角,首先求解 s1 中某个长度的子字符串是否由 s2 和 s3 的子字符串交错组成,直到求解整个 s1 的长度为止,也可以看成一个包含子问题的最值问题。
对应示例代码:

class Solution {
  public boolean isInterleave(String s1, String s2, String s3) {
      int length = s3.length();
      // 特殊情况处理
      if(s1.isEmpty() && s2.isEmpty() && s3.isEmpty()) return true;
      if(s1.isEmpty()) return s2.equals(s3);
      if(s2.isEmpty()) return s1.equals(s3);
      if(s1.length() + s2.length() != length) return false;

      int[][] dp = new int[s2.length()+1][s1.length()+1];
      // 边界赋值
      for(int i = 1;i < s1.length()+1;i++){
          if(s1.substring(0,i).equals(s3.substring(0,i))){
              dp[0][i] = 1;
          }
      }
      for(int i = 1;i < s2.length()+1;i++){
          if(s2.substring(0,i).equals(s3.substring(0,i))){
              dp[i][0] = 1;
          }
      }
       
      for(int i = 2;i <= length;i++){
          // 遍历 i 的所有组成(边界除外)
          for(int j = 1;j < i;j++){
              // 防止越界
              if(s1.length() >= j && i-j <= s2.length()){
                  if(s1.charAt(j-1) == s3.charAt(i-1) && dp[i-j][j-1] == 1){
                      dp[i-j][j] = 1;
                  }
              }
              // 防止越界
              if(s2.length() >= j && i-j <= s1.length()){
                  if(s2.charAt(j-1) == s3.charAt(i-1) && dp[j-1][i-j] == 1){
                      dp[j][i-j] = 1;
                  }
              }
          }
      }
      return dp[s2.length()][s1.length()]==1;
  }
}

2.3 求总数

除了求最值与可行性之外,求方案总数也是比较常见的一类动态规划问题。比如说给定一个数据结构和限定条件,让你计算出一个方案的所有可能的路径,那么这种问题就属于求方案总数的问题。

1. 硬币组合问题

问题:英国的英镑硬币有 1p, 2p, 5p, 10p, 20p, 50p, £1 (100p), 和 £2 (200p)。比如我们可以用以下方式来组成 2 英镑:1×£1 + 1×50p + 2×20p + 1×5p + 1×2p + 3×1p。问题是一共有多少种方式可以组成 n 英镑? 注意不能有重复,比如 1 英镑 +2 个 50P 和 50P+50P+1 英镑是一样的。

示例1:
输入: 2
输出: 73682

这个问题本质还是求满足条件的组合,只不过这里不需要求出具体的值或者说组合,只需要计算出组合的数量即可。

public class Main {
  public static void main(String[] args) throws Exception {
       
      Scanner sc = new Scanner(System.in);
      while (sc.hasNext()) {
           
          int n = sc.nextInt();
          int coin[] = { 1, 5, 10, 20, 50, 100 };
           
          // dp[i][j]表示用前i种硬币凑成j元的组合数
          long[][] dp = new long[7][n + 1];
           
          for (int i = 1; i <= n; i++) {
              dp[0][i] = 0; // 用0种硬币凑成i元的组合数为0
          }
           
          for (int i = 0; i <= 6; i++) {
              dp[i][0] = 1; // 用i种硬币凑成0元的组合数为1,所有硬币均为0个即可
          }
           
          for (int i = 1; i <= 6; i++) {
               
              for (int j = 1; j <= n; j++) {
                   
                  dp[i][j] = 0;
                  for (int k = 0; k <= j / coin[i - 1]; k++) {
                       
                      dp[i][j] += dp[i - 1][j - k * coin[i - 1]];
                  }
              }
          }
           
          System.out.print(dp[6][n]);
      }
      sc.close();
  }
}

2. 路径规划问题

问题:一个机器人位于一个 m x n 网格的左上角。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角,共有多少路径?

示例1:
输入: 2 2
输出: 2


示例1:
输入: 3 3
输出: 6

这个问题还是一个求满足条件的组合数量的问题,只不过这里的组合变成了路径的组合。我们可以先求出长宽更小的网格中的所有路径,然后再在一个更大的网格内求解更多的组合。这和硬币组合的问题相比没有什么本质区别。

这里有一个规律或者说现象需要强调,那就是求方案总数的动态规划问题一般都指的是求“一个”方案的所有具体形式。如果是求“所有”方案的具体形式,那这种肯定不是动态规划问题,而是使用传统递归来遍历出所有方案的具体形式。

为什么这么说呢?因为你需要把所有情况枚举出来,大多情况下根本就没有重叠子问题给你优化。即便有,你也只能使用备忘录对遍历进行一个简单加速。但本质上,这类问题不是动态规划问题。

对应示例代码:

package com.qst.Tesst;

import java.util.Scanner;

public class Test12 {
  public static void main(String[] args) {
      Scanner scanner = new Scanner(System.in);
      while (scanner.hasNext()) {
          int x = scanner.nextInt();
          int y = scanner.nextInt();

          //设置路径
          long[][] path = new long[x + 1][y + 1];
          //设置领导数量
          int n = scanner.nextInt();

          //领导位置
          for (int i = 0; i < n; i++) {
              int a = scanner.nextInt();
              int b = scanner.nextInt();
              path[a][b] = -1;
          }

          for (int i = 0; i <= x; i++) {
              path[i][0] = 1;
          }
          for (int j = 0; j <= y; j++) {
              path[0][j] = 1;
          }

          for (int i = 1; i <= x; i++) {
              for (int j = 1; j <= y; j++) {
                  if (path[i][j] == -1) {
                      path[i][j] = 0;
                  } else {
                      path[i][j] = path[i - 1][j] + path[i][j - 1];
                  }

              }

          }
          System.out.println(path[x][y]);
      }
  }
}

三、 如何确认动态规划问题

从前面我所说来看,如果你碰到了求最值、求可行性或者是求方案总数的问题的话,那么这个问题就八九不离十了,你基本可以确定它就需要使用动态规划来解。但是,也有一些个别情况需要注意:

3.1 数据不可排序

假设我们有一个无序数列,希望求出这个数列中最大的两个数字之和。很多初学者刚刚学完动态规划会走火入魔到看到最优化问题就想用动态规划来求解,事实上,这个问题不是简单做一个排序或者做一个遍历就可以求解出来的。对于这种问题,我们应该先考虑一下能不能通过排序来简化问题,如果不能,才极有可能是动态规划问题。

最小的 k 个数

问题:输入整数数组 arr ,找出其中最小的 k 个数。例如,输入 4、5、1、6、2、7、3、8 这 8 个数字,则最小的 4 个数字是 1、2、3、4。

示例1:
输入:arr = [3,2,1], k = 2
输出:[1,2] 或者 [2,1]


示例2:
输入:arr = [0,1,2,1], k = 1
输出:[0]

我们发现虽然这个问题也是求“最”值,但其实只要通过排序就能解决,所以我们应该用排序、堆等算法或者数据结构就可以解决,而不应该用动态规划。

对应的示例代码:

public class Solution {
  public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
              int t;
      boolean flag;
      ArrayList result = new ArrayList();
      if(k>input.length){
          return result;
      }
      for(int i =0;i<input.length;i++){
          flag = true;
          for(int j = 0; j < input.length-i;j++)
              if(j<input.length-i-1){
                  if(input[j] > input[j+1]) {
                      t = input[j];
                      input[j] = input[j+1];
                      input[j+1] = t;
                      flag = false;
                  }
              }
          if(flag)break;
      }
      for(int i = 0; i < k;i++){
          result.add(input[i]);
      }
      return result;
  }
}

3.2 数据不可交换

还有一类问题,可以归类到我们总结的几类问题里去,但是不存在动态规划要求的重叠子问题(比如经典的八皇后问题),那么这类问题就无法通过动态规划求解。

全排列

问题:给定一个没有重复数字的序列,返回其所有可能的全排列。

示例:
输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]

这个问题虽然是求组合,但没有重叠子问题,更不存在最优化的要求,因此可以使用回溯方法处理。

对应的示例代码:

public class Main {
   public static void main(String[] args) {
       perm(new int[]{1,2,3},new Stack<>());
  }
   public static void perm(int[] array, Stack<Integer> stack) {
       if(array.length <= 0) {
           //进入了叶子节点,输出栈中内容
           System.out.println(stack);
      } else {
           for (int i = 0; i < array.length; i++) {
               //tmepArray是一个临时数组,用于就是Ri
               //eg:1,2,3的全排列,先取出1,那么这时tempArray中就是2,3
               int[] tempArray = new int[array.length-1];
               System.arraycopy(array,0,tempArray,0,i);
               System.arraycopy(array,i+1,tempArray,i,array.length-i-1);
               stack.push(array[i]);
               perm(tempArray,stack);
               stack.pop();
          }
      }
  }
}

总结一下,哪些问题可以使用动态规划呢,通常含有下面情况的一般都可以使用动态规划来解决:

  • 求最优解问题(最大值和最小值);

  • 求可行性(True 或 False);

  • 求方案总数;

  • 数据结构不可排序(Unsortable);

  • 算法不可使用交换(Non-swappable)。

如果面试题目出现这些特征,那么在 90% 的情况下你都能断言它就是一个动归问题。除此之外,还需要考虑这个问题是否包含重叠子问题与最优子结构,在这个基础之上你就可以 99% 断言它是否为动归问题,并且也顺势找到了大致的解题思路。

作者:xiangzhihong

来源:https://segmentfault.com/a/1190000041300090

收起阅读 »

Flutter线上监控说明

概要移动端Apm系统作用:1、我们可以快速定位到线上App的实际使用情况,了解到App的奔溃、异常数据,从而针对潜在的风险问题进行预警,并进行相应的处理。2、了解App的真实使用信息,提高用户使用黏性。一、移动端常用apm指标1、崩溃率崩溃分析,是将 Andr...
继续阅读 »

概要

移动端Apm系统作用:

1、我们可以快速定位到线上App的实际使用情况,了解到App的奔溃、异常数据,从而针对潜在的风险问题进行预警,并进行相应的处理。

2、了解App的真实使用信息,提高用户使用黏性。

一、移动端常用apm指标

1、崩溃率

崩溃分析,是将 Android 和 iOS 平台常见的 APP 崩溃问题进行归类分析,帮助企业根据崩溃指标快速发现、定位问题。


2、UI卡顿

拿Android来说:大多数用户感知到的卡顿等性能问题的最主要根源都是因为渲染性能。Android系统每隔大概16.6ms发出VSYNC信号,触发对UI进行渲染,如果每次渲染都成功,这样就能够达到流畅的画面所需要的60fps,为了能够实现60fps,这意味着程序的大多数操作都必须在16ms内完成。

 

3、线上日志

可以快速定位某个用户的日志数据,及时根据用户反馈的情况进行快速排查。

4、网络监控

由于网络环境错综复杂,对于网络接口性能方面需要关注 接口响应时间,网络错误,http状态码,网络劫持等

 

三、Flutter apm现状

闲鱼自研(未开方源码)

再无其他第三方

  

收起阅读 »

Flow 操作符 shareIn 和 stateIn 使用须知

Flow.shareIn 与 Flow.stateIn 操作符可以将冷流转换为热流: 它们可以将来自上游冷数据流的信息广播给多个收集者。这两个操作符通常用于提升性能: 在没有收集者时加入缓冲;或者干脆作为一种缓存机制使用。注意&n...
继续阅读 »

Flow.shareIn 与 Flow.stateIn 操作符可以将冷流转换为热流: 它们可以将来自上游冷数据流的信息广播给多个收集者。这两个操作符通常用于提升性能: 在没有收集者时加入缓冲;或者干脆作为一种缓存机制使用。

注意 冷流 是按需创建的,并且会在它们被观察时发送数据;热流 则总是活跃,无论是否被观察,它们都能发送数据。

本文将会通过示例帮您熟悉 shareIn 与 stateIn 操作符。您将学到如何针对特定用例配置它们,并避免可能遇到的常见陷阱。

底层数据流生产者

继续使用我 之前文章 中使用过的例子——使用底层数据流生产者发出位置更新。它是一个使用 callbackFlow 实现的 冷流。每个新的收集者都会触发数据流的生产者代码块,同时也会将新的回调加入到 FusedLocationProviderClient。

class LocationDataSource(
private val locationClient: FusedLocationProviderClient
) {
val locationsSource: Flow<Location> = callbackFlow<Location> {
val callback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult?) {
result ?: return
try { offer(result.lastLocation) } catch(e: Exception) {}
}
}
requestLocationUpdates(createLocationRequest(), callback, Looper.getMainLooper())
.addOnFailureListener { e ->
close(e) // in case of exception, close the Flow
}
// 在 Flow 结束收集时进行清理
awaitClose {
removeLocationUpdates(callback)
}
}
}

让我们看看在不同的用例下如何使用 shareIn 与 stateIn 优化 locationsSource 数据流。

shareIn 还是 stateIn?

我们要讨论的第一个话题是 shareIn 与 stateIn 之间的区别。shareIn 操作符返回的是 SharedFlow 而 stateIn 返回的是 StateFlow

注意 : 要了解有关 StateFlow 与 SharedFlow 的更多信息,可以查看 我们的文档 。

StateFlow 是 SharedFlow 的一种特殊配置,旨在优化分享状态: 最后被发送的项目会重新发送给新的收集者,并且这些项目会使用 Any.equals 进行合并。您可以在 StateFlow 文档 中查看更多相关信息。

两者之间的最主要区别,在于 StateFlow 接口允许您通过读取 value 属性同步访问其最后发出的值。而这不是 SharedFlow 的使用方式。

提升性能

通过共享所有收集者要观察的同一数据流实例 (而不是按需创建同一个数据流的新实例),这些 API 可以为我们提升性能。

在下面的例子中,LocationRepository 消费了 LocationDataSource 暴露的 locationsSource 数据流,同时使用了 shareIn 操作符,从而让每个对用户位置信息感兴趣的收集者都从同一数据流实例中收集数据。这里只创建了一个 locationsSource 数据流实例并由所有收集者共享:

class LocationRepository(
private val locationDataSource: LocationDataSource,
private val externalScope: CoroutineScope
) {
val locations: Flow<Location> =
locationDataSource.locationsSource.shareIn(externalScope, WhileSubscribed())
}

WhileSubscribed 共享策略用于在没有收集者时取消上游数据流。这样一来,我们便能在没有程序对位置更新感兴趣时避免资源的浪费。

Android 应用小提醒! 在大部分情况下,您可以使用 WhileSubscribed(5000),当最后一个收集者消失后再保持上游数据流活跃状态 5 秒钟。这样在某些特定情况 (如配置改变) 下可以避免重启上游数据流。当上游数据流的创建成本很高,或者在 ViewModel 中使用这些操作符时,这一技巧尤其有用。

缓冲事件

在下面的例子中,我们的需求有所改变。现在要求我们保持监听位置更新,同时要在应用从后台返回前台时在屏幕上显示最后的 10 个位置:

class LocationRepository(
private val locationDataSource: LocationDataSource,
private val externalScope: CoroutineScope
) {
val locations: Flow<Location> =
locationDataSource.locationsSource
.shareIn(externalScope, SharingStarted.Eagerly, replay = 10)
}

我们将参数 replay 的值设置为 10,来让最后发出的 10 个项目保持在内存中,同时在每次有收集者观察数据流时重新发送这些项目。为了保持内部数据流始终处于活跃状态并发送位置更新,我们使用了共享策略 SharingStarted.Eagerly,这样就算没有收集者,也能一直监听更新。

缓存数据

我们的需求再次发生变化,这次我们不再需要应用处于后台时 持续 监听位置更新。不过,我们需要缓存最后发送的项目,让用户在获取当前位置时能在屏幕上看到一些数据 (即使数据是旧的)。针对这种情况,我们可以使用 stateIn 操作符。

class LocationRepository(
private val locationDataSource: LocationDataSource,
private val externalScope: CoroutineScope
) {
val locations: Flow<Location> =
locationDataSource.locationsSource.stateIn(externalScope, WhileSubscribed(), EmptyLocation)
}

Flow.stateIn 可以缓存最后发送的项目,并重放给新的收集者。

注意!不要在每个函数调用时创建新的实例

切勿 在调用某个函数调用返回时,使用 shareIn 或 stateIn 创建新的数据流。这样会在每次函数调用时创建一个新的 SharedFlow 或 StateFlow,而它们将会一直保持在内存中,直到作用域被取消或者在没有任何引用时被垃圾回收。

class UserRepository(
private val userLocalDataSource: UserLocalDataSource,
private val externalScope: CoroutineScope
) {
// 不要像这样在函数中使用 shareIn 或 stateIn
// 这将在每次调用时创建新的 SharedFlow 或 StateFlow,而它们将不会被复用。
fun getUser(): Flow<User> =
userLocalDataSource.getUser()
.shareIn(externalScope, WhileSubscribed())

// 可以在属性中使用 shareIn 或 stateIn
val user: Flow<User> =
userLocalDataSource.getUser().shareIn(externalScope, WhileSubscribed())
}

需要入参的数据流

需要入参 (如 userId) 的数据流无法简单地使用 shareIn 或 stateIn 共享。以开源项目——Google I/O 的 Android 应用 iosched 为例,您可以在 源码中 看到,从 Firestore 获取用户事件的数据流是通过 callbackFlow 实现的。由于其接收 userId 作为参数,因此无法简单使用 shareIn 或 stateIn 操作符对其进行复用。

class UserRepository(
private val userEventsDataSource: FirestoreUserEventDataSource
) {
// 新的收集者会在 Firestore 中注册为新的回调。
// 由于这一函数依赖一个 `userId`,所以在这个函数中
// 数据流无法通过调用 shareIn 或 stateIn 进行复用.
// 这样会导致每次调用函数时,都会创建新的 SharedFlow 或 StateFlow
fun getUserEvents(userId: String): Flow<UserEventsResult> =
userLocalDataSource.getObservableUserEvents(userId)
}

如何优化这一用例取决于您应用的需求:

  • 您是否允许同时从多个用户接收事件?如果答案是肯定的,您可能需要为 SharedFlow 或 StateFlow 实例创建一个 map,并在 subscriptionCount 为 0 时移除引用并退出上游数据流。
  • 如果您只允许一个用户,并且收集者需要更新为观察新的用户,您可以向一个所有收集者共用的 SharedFlow 或 StateFlow 发送事件更新,并将公共数据流作为类中的变量。

shareIn 与 stateIn 操作符可以与冷流一同使用来提升性能,您可以使用它们在没有收集者时添加缓冲,或者直接将其作为缓存机制使用。小心使用它们,不要在每次函数调用时都创建新的数据流实例——这样会导致资源的浪费及预料之外的问题!


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

同一个app不同activity显示多任务(仿微信小程序切换效果)

如题,这种效果类似微信小程序显示的效果,就是打开微信跳一跳后,切换安卓多任务窗口(就是清理内存窗口),会看到如下页面 微信小程序会在其中显示两个单独的页面,点击跳一跳会进入跳一跳小程序,点击后面的微信,即会进入微信聊天主页面。在安卓中如何实现呢?这里...
继续阅读 »

如题,这种效果类似微信小程序显示的效果,就是打开微信跳一跳后,切换安卓多任务窗口(就是清理内存窗口),会看到如下页面 多任务图1.jpg

微信小程序会在其中显示两个单独的页面,点击跳一跳会进入跳一跳小程序,点击后面的微信,即会进入微信聊天主页面。

在安卓中如何实现呢?

这里有两种方法实现:

第一种:代码动态实现

Intent intent = new Intent(this, SecondActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
startActivity(intent);

添加上面的两个Flag即可,有些文章说关闭的时候要使用

finishAndRemoveTask();

方法,我这边没使用该方法也没发现问题,如果存在潜在问题,知道的人麻烦告知下,谢谢!!!

第二种:在AndroidManifest.xml中配置属性

参考链接:在近期任务列表显示单个APP的多个Activity

第二种方法由于需要写死配置,可能对于我来说作用不大,所以也没有测试,需要了解的人可以查看上面地址。

注意:这里来说下处理第一种方法的问题

使用上面的方法确实是实现了微信小程序多任务窗口的效果,但你会发现两个窗口在文章开头的图中的地方显示的是相同的名字,即你APP的名字,这里就跟小程序有区别了,下面来说下如何实现这种效果:

首先:经过测试,在manifest.xml中给要显示的activity设置android:lable,这种方法是可行的,但会相当于是固定了,不可变了。

然后:在manifest.xml中给该activity设置android:icon也是可以的,这样就实现了显示"跳一跳"文字和logo了。

最后:当然还是同样需要在代码中动态设置,不然固定死对于程序员来说有瑕疵。

在需要显示的activity中调用下面的代码即可显示不同文字

setTaskDescription(new ActivityManager.TaskDescription("跳一跳"));

聪明的程序员都会看下该方法的源码以及需要参数的构造方法,所以同时显示图片和文字以及需要适配就需要用下面的代码了

if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
setTaskDescription(new ActivityManager.TaskDescription("跳一跳", mBitmap));
}

没错,需要5.0以上才能实现,参数的构造就需要传入bitmap才能显示图片了。

最终效果图:

最终效果图.png

存在的问题:当添加flag打开activity之后,如果切换了任务窗口,这时返回是不能返回到之前调用startActivity的方法的页面了,如果没有切换就不会存在这个问题,微信也是一样,像微信大佬都没有解决(也可能没这个需求),反正我是没有办法滴。


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

Android Activity Result API

最近准备开始新的项目,在编写base类复写onActivityResult方法时,发现已经提示deprecation了。于是去官网查找了一下,发现现在官方推荐做法是使用 Activity Result API。本篇文章用来记录一下 Activity Resul...
继续阅读 »

最近准备开始新的项目,在编写base类复写onActivityResult方法时,发现已经提示deprecation了。于是去官网查找了一下,发现现在官方推荐做法是使用 Activity Result API。

本篇文章用来记录一下 Activity Result API 如何使用。

以往的实现方式

以往,A Activity获取B Activity的返回值的实现方法是,A通过startActivityForResult()来启动B,然后B在finish()之前通过setResult()方法来设置结果值,A就可以在onActivityResult()方法中获取到B在setResult()方法中设置的参数。简单示例如下:

class A : Activity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startActivityForResult(Intent(this,B::class.java))
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
//从B返回后这里就可以获取到resultCode为Activity.RESULT_OK
}
}

class B: Activity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setResult(Activity.RESULT_OK)
finish()
}
}

Activity Result API

现在,在Activity和Fragment中,Activity Result API提供了registerForActivityResult()方法,该方法用于注册获取结果的回调。

registerForActivityResult可以传入ActivityResultContract、ActivityResultRegistry、ActivityResultCallback等3个参数,并返回ActivityResultLauncher。

  • ActivityResultContract:ActivityResult合约,约定输入的参数和输出的参数。包含默认合约和自定义合约。
  • ActivityResultRegistry:存储已注册的ActivityResultCallback的记录表,可以在非Activity和Fragment的类中借用此类获取ActivityResult。
  • ActivityResultCallback:ActivityResult回调,用于获取返回结果。
  • ActivityResultLauncher:启动器,根据合约规定的输入参数来启动页面。

Activity Result API的简单使用示例:

class A : Activity() {

//默认合约
var forActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult ->
launcher?.text="lanuncher callback value : resultCode:$resultCode data$data"
}

//自定义合约(输入输出类型均为String)
var forActivityResultLauncher1 = registerForActivityResult(object : ActivityResultContract<String, String>() {
override fun createIntent(context: Context, input: String?): Intent {
return Intent(this@AActivity, B::class.java).apply {
putExtra("inputParams", input)
}
}

override fun parseResult(resultCode: Int, intent: Intent?): String {
return if (resultCode == Activity.RESULT_OK) {
intent?.getStringExtra("result") ?: "empty result"
} else {
""
}
}
}) { resultString ->
launcher1?.text = "lanuncher1 callback value : reslutString:$reslutString"
}

var launcher: TextView? = null
var launcher1: TextView? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.layout_a_activity)
launcher = findViewById(R.id.tv_launcher_callback)
launcher1 = findViewById(R.id.tv_launcher_callback1)
val btnLauncher = findViewById<Button>(R.id.launcher)
val btnLauncher1 = findViewById<Button>(R.id.launcher1)
btnLauncher.setOnClickListener {
//默认合约
forActivityResultLauncher.launch(Intent(this@AActivity, B::class.java))
}

btnLauncher1.setOnClickListener {
//自定义合约
forActivityResultLauncher1.launch("inputParams from A")
}
}
}

class B: Activity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.layout_b_activity)
val btnBack = findViewById<Button>(R.id.back)
val inputParams = intent.getStringExtra("inputParams")
btnBack.setOnClickListener {
if (TextUtils.isEmpty(inputParams)) {
setResult(Activity.RESULT_OK, Intent())
} else {
setResult(Activity.RESULT_OK, Intent().apply {
putExtra("result", "result from A")
})
}
finish()
}
}
}

示例效果图: 1642499246411691.gif

在非Activity和Fragment的类中接收ActivityResult

示例代码如下:

class A : Activity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val myActivityResultObserver = MyActivityResultObserver(activityResultRegistry)
lifecycle.addObserver(myActivityResultObserver)
}
}

class MyActivityResultObserver(private val registry: ActivityResultRegistry) : DefaultLifecycleObserver {

override fun onCreate(owner: LifecycleOwner) {
super.onCreate(owner)
registry.register("MyActivityResultReceiver", owner, ActivityResultContracts.StartActivityForResult()) {

}
}
}

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