【Android爬坑周记】用SplashScreen做一个会动的开屏!
Android 12以上加入了SplashScreen,并且支持开屏动画了!因此我在【小鹅事务所】项目中加入了一个开屏动画,如下(为方便动图展示,我故意延长了几秒钟):
SplashScreen
简单介绍一下SplashScreen,仅在冷启动或者温启动的时候会展示SplashScreen,支持VAD动画、帧动画。我就先使用帧动画实现这个开屏动画,后面会考虑换成VAD动画。关于SplashScreen具体就不细讲啦,我讲这些讲不明白,没有官方文档讲得好,直接进入实战!!
注意裁切
ICON在设计的时候只能够占用三分之二大小的圆,超出这部分的会被裁切掉,所以这点需要注意!
设计
首先打开UI设计软件,我此处用Figma,新建一个方形的框框,方形的框框里面整一个三分二大小的圆圈,像这样。
然后呢,就把设计好的Icon放进去
这个时候一张静态图就做好啦,但是帧动画需要让图片动起来的话,就需要多张静态图。怎么设计它动起来呢?我的思路是让它扭头!像这样。
然后再把框框的颜色隐藏掉,我们只需要透明背景的Icon
注意,为了展示外边需要留空间,我给它们的框框加上描边,实际不需要!这个时候就可以导出图片啦,我这边选择导出矢量图,也就是SVG格式。
导入动画
打开Android Studio,右键点击res → new → Vector Asset,再导入图片,将静态图都导进去就可以做动画啦。
新建anim_little_goose.xml
,根标签是animation-list
,并在里面放4个item。
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false">
<item
android:drawable="@drawable/ic_little_goose"
android:duration="50" />
<item
android:drawable="@drawable/ic_little_goose_back"
android:duration="150" />
<item
android:drawable="@drawable/ic_little_goose"
android:duration="50" />
<item
android:drawable="@drawable/ic_little_goose_fore"
android:duration="150" />
</animation-list>
根据命名可以看出
第一帧为正常的小鹅,展示50毫秒
第二帧为向后扭头的小鹅,展示150毫秒
第三帧为正常的小鹅,展示50毫秒
第四帧为向前扭头的小鹅,展示150毫秒
一次循环就是400毫秒,点开Split界面,就能在右边预览动画了,这个时候,动画就简简单单做好了。
SplashScreen
引入依赖
由于SplashScreen是Android12以上才有的,而Android12以下需要适配,但是!Jetpack提供了同名适配库,去gradle引用就好了。
//SplashScreen
implementation 'androidx.core:core-splashscreen:1.0.0'
设置开屏主题
然后在res/values/themes
中新建一个style标签,并将其父标签设为Theme.SplashScreen
,需要注意的是,如果适配了黑夜模式的话,也可以在values-night/themes
文件下单独配置。
<style name="Theme.AppSplashScreen" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/primary_color</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/anim_little_goose</item>
<item name="windowSplashScreenAnimationDuration">3000</item>
<item name="postSplashScreenTheme">@style/Theme.Account</item>
</style>
我这边配置一个无ICON背景的动画,因此不用windowSplashScreenIconBackgroundColor
标签设置ICON背景。
简单介绍一下我设置的4个标签
windowSplashScreenBackground
设置整个开屏动画的背景颜色。
windowSplashScreenAnimatedIcon
设置的是开屏动画播放的动画文件,也就是上面写的动画文件。
windowSplashScreenAnimationDuration
设置的是动画的播放时长,也就是说小鹅抖三秒钟头就会停止播放。
postSplashScreenTheme
这个设置的是开屏动画播放完需要回到的主题,此处设置了我的主题。
<style name="Theme.Account" parent="Theme.MaterialComponents.DayNight.NoActionBar">
...
</style>
在Manifest注册
<application
android:label="@string/app_name"
...
android:theme="@style/Theme.Account">
...
<activity
android:name=".ui.MainActivity"
android:theme="@style/Theme.AppSplashScreen"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
可以看到在打开应用打开的第一个Activity,即MainActivity中设置了开屏主题,而在Application中设置了自己的主题。在Application设置主题的话,这个Application中的除了特殊设置Theme的Activity,其它都默认使用Application主题。
去MainActivity吧!
class MainActivity : BaseActivity() {
private val binding by viewBinding(ActivityMainBinding::inflate)
override fun onCreate(savedInstanceState: Bundle?) {
val splashScreen = installSplashScreen()
splashScreen.setKeepOnScreenCondition { !isAppInit }
super.onCreate(savedInstanceState)
initView()
}
...
}
重写onCreate
函数,并在调用super.onCreate
之前加载SplashScreen,即调用installSplashScreen
,获得一个splashScreen实例,理论上来说调用installSplashScreen
函数已经可以实现开屏动画了,可是我想等到一部分数据加载完再进入APP怎么办?
可以看到我调用了setKeepOnScreenCondition
函数,传入一个接口,这个接口返回一个Boolean值,如果返回true则继续展示开屏,如果返回false则进入APP。而此函数在每次绘制之前都会调用,是主线程调用的,因此不能在这里处理太多东西阻塞主线程!
我这边就设置了一个顶层变量,每次都去看看这个顶层变量的值,不会阻塞主线程。
class AccountApplication : Application() {
val supervisorScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
//初始化数据 防止第一次打开还要加载
private fun initData() {
supervisorScope.launch {
val initIconDataDeferred = async { TransactionIconHelper.initIconData() }
val initTransactionDeferred = async { TransactionHelper.initTransaction() }
val initScheduleDeferred = async { ScheduleHelper.initSchedule() }
val initNoteDeferred = async { NoteHelper.initNote() }
val initMemorialsDeferred = async { MemorialHelper.initMemorials() }
val initTopMemorialDeferred = async { MemorialHelper.initTopMemorial() }
val initDataStoreDeferred = async { DataStoreHelper.INSTANCE.initDataStore() }
initIconDataDeferred.await()
initTransactionDeferred.await()
initScheduleDeferred.await()
initNoteDeferred.await()
initMemorialsDeferred.await()
initTopMemorialDeferred.await()
initDataStoreDeferred.await()
isAppInit = true
}
}
}
var isAppInit = false
我在Application中对所有需要初始化的东西先初始化一遍,初始化完之后再将isAppInit
设置为true,此时在闪屏那边获取的为false,也就是说就会进入APP了。
到这里就结束了,去运行一下吧!
总结
说实话,在我看来,SplashScreen其实用处不大,因为我们的闪屏一般是用来放advertisement的,而不是放有趣的动画的!
参考
SplashScreen: developer.android.google.cn/develop/ui/…
链接:https://juejin.cn/post/7150692699350237191
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Android APT实战学习技巧
简介
APT(Annotation Processing Tool)即注解处理器,在编译的时候可以处理注解然后搞一些事情,也可以在编译时生成一些文件之类的。ButterKnife和EventBus都使用了APT技术,如果不会APT技术就很难看懂这两个框架的源码。
作用
使用APT可以在编译时来处理编译时注解,生成额外的Java文件,有如下效果:
- 可以达到减少重复代码手工编写的效果。
如ButterKnife,我们可以直接使用注解来减少findviewbyid这些代码,只需要通过注解表示是哪个id就够了。
- 功能封装。将主要的功能逻辑封装起来,只保留注解调用。
- 相对于使用Java反射来处理运行时注解,使用APT有着更加良好的性能。
Android基本编译流程
Android中的代码编译时需要经过:Java——>class ——> dex 流程,代码最终生成dex文件打入到APK包里面。
编译流程如图所示:
- APT是在编译开始时就介入的,用来处理编译时注解。
- AOP(Aspect Oridnted Programming)是在编译完成后生成dex文件之前,通过直接修改.class文件的方式,来对代码进行修改或添加逻辑。常用在在代码监控,代码修改,代码分析这些场景。
基本使用
基本使用流程主要包括如下几个步骤:
- 创建自定义注解
- 创建注解处理器,处理Java文件生成逻辑
- 封装一个供外部调用的API
- 项目中调用
整理思路
- 首先我们需要创建两个JavaLibrary
- 一个用来定义注解,一个用来扫描注解
- 获取到添加注解的成员变量名
- 动态生成类和方法用IO生成文件
实战
创建一个空项目
创建两个JavaLibrary
- 注解的Lib: apt-annotation
- 扫描注解的Lib: apt-processor
创建完之后
app模块依赖两个Library
implementation project(path: ':apt-annotation')
annotationProcessor project(path: ':apt-processor')
注解Lib中创建一个注解类
如果还不会自定义注解的同学,可以先去看我之前写的一篇Java自定义注解入门到实战
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface Print {
}
扫描注解的Lib添加依赖
dependencies {
//自动注册,动态生成 META-INF/...文件
implementation 'com.google.auto.service:auto-service:1.0-rc6'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
//依赖apt-annotation
implementation project(path: ':apt-annotation')
}
创建扫描注解的类
重写init方法,输出Hello,APT
注意: 这里是JavaLib,所以不能使用Log打印,这里可以使用Java的println()或注解处理器给我们提供的方法,建议使用注解处理器给我们提供的
现在我们已经完成了APT的基本配置,现在我们可以build一下项目了,成败在此一举
如果你已经成功输出了文本,说明APT已经配置好,可以继续下一步了
继续完成功能
现在我们可以继续完成上面要实现的功能了,我们需要先来实现几个方法
/**
* 要扫描扫描的注解,可以添加多个
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
HashSet<String> hashSet = new HashSet<>();
hashSet.add(Print.class.getCanonicalName());
return hashSet;
}
/**
* 编译版本,固定写法就可以
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return processingEnv.getSourceVersion();
}
定义注解
我们先在MianActivity中添加两个成员变量并使用我们定义的注解
定义注解
真正解析注解的地方是在process方法,我们先试试能不能拿到被注解的变量名
/**
* 扫描注解回调
*/
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
//拿到所有添加Print注解的成员变量
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Print.class);
for (Element element : elements) {
//拿到成员变量名
Name simpleName = element.getSimpleName();
//输出成员变量名
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE,simpleName);
}
return false;
}
编译试一下
生成类
既然能拿到被注解的变量名,后面就简单了,我们只需要用字符串拼出来一个工具类,然后用IO流写到本地就ok了
查看效果
现在点击一下编译,然后我们可以看到app模块下的build文件已经有我们生成的类了
调用方法
现在我们回到MainActivity,就可以直接调用这个动态生成的类了
总结
优点:
它可以做任何你不想做的繁杂的工作,它可以帮你写任何你不想重复代码,将重复代码抽取出来,用AOP思想去编写。 它可以生成任何java代码供你在任何地方使用。
难点:
在于设计模式和解耦思想的灵活应用。在于代理类代码生成的繁琐:你可以手动进行字符串拼接,也可以用squareup公司的javapoet库来构建出任何你想要的java代码。
链接:https://juejin.cn/post/7158450306680881160
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
安卓之如何优雅的处理Activity回收突发事件
情景与原因
前面的文章说过,我的一个业务要从页面A进入页面B,也就意味着我的应用出现了在ActivityA的基础上启动了ActivityB的情景,这个时候ActivityA就进入了停止状态,但这个时候如果出现系统内存不足的情况,就会把ActivityA回收掉,此时用户按下Back键返回A,仍然会正常显示A,但此时的A是执行onCreate()方法加载的,而不是执行onRestart()方法,也就是说我们的ActivityA页面是被重新创建出来的。
那有什么区别呢,这就是我们平时网上填表之类的最讨厌的情景会浮现了,比如你好不容易把信息填好,点击下一步,然后发现想修改上个页面信息,哎,这时候你会发现由于系统内存不足回收掉了A,于是你辛辛苦苦填好的信息都没了,这太痛了。这一情景的出现就说明回收了A再重新创建会导致我们在A当中存的一些临时数据与状态都可能被丢失。
这就是我们今天要解决的问题。
解决方法
虽然出现这个问题是是否影响用户体验的,但解决方法是非常简单的,因为我们的Activity中还提供了一个onSaveInstanceState()回调方法,它就可以保证在Activity被回收之前一定被调用,而我们就可以利用这一特性去解决上述问题。
方法介绍
onSaveInstanceState()方法中会携带一个Bundle类型的参数,而Bundle提供了一系列方法用于保存我们想要的数据,其中如putString()就是用于保存字符串的,而putInt()方法显而易见也是保存整型数据的,而每个保存方法都需要传入两个参数,其中第一个参数我们称之为键,是用于在Bundle中取值的特定标记,第二个参数是我们要保存的内容,属于键值对类型,学过数据库的一般都知道。
写法
如下,我们可以这么去写:
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
val tempData = "your temp data"
outState.putString("data_key", tempData)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (savedInstanceState != null) {
val tempData = savedInstanceState.getString("data_key")
}
...
}
在onSaveInstanceState()中保存你想要的数据,在onCreate()中给取出来,这样就不用害怕再创建的时候数据丢失了,不过如果是横竖屏切换造成数据丢失还是依照前文ViewModel的写法更好。
结语
其实基础往往是最容易忽视的,我们之前光在那说高楼大厦如何建成,但地基并不牢靠,所以还是需要落实到基础上最好。
链接:https://juejin.cn/post/7158096746583687205
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
聊聊ART虚拟机_对象的分配问题
前置知识
- 有Android开发基础
- 了解 Java 语法和 JVM
前言
ART 虚拟机(下图 Runtime 层),相信各位搞 Android 开发的同学都有知道,总体的印象呢就是:ART 与 JVM 不同,其不符合 JVM 规范不属于 JVM ,且为 Dalvik 的进阶版。
但是,我们有必要对 ART 进行更加深入的了解,其有助于我们对 Android 的更深层次的理解。所以,本文将和聊一聊 ART 虚拟机,以及 ART 中一个对象是如何分配的。
何为ART虚拟机
在开始阶段,我们还是需要来聊一下什么是 ART 虚拟机,其不同在何处。
在探析Android中的四类性能优化一文中,我们有提到 ART 虚拟机是 Google 在 Android4.4 的时候引入的,其用于替代 Dalvik 虚拟机。而在替代 Dalvik 虚拟机的同时,他也是兼容之前的 dex 格式的。ART 与 Dalvik 的不同点如下所示。
ART特性
1. 预编译
Dalvik 中的应用每次运行时,字节码都需要通过即时编译器 JIT 转换为机器码,这会使得应用的运行效率降低。在 ART 中,系统在安装应用时会进行一次预编译(AOT,Ahead-Of-Time),将字节码预先编译成机器码并存储在本地,这样应用就不用在每次运行时执行编译了,运行效率也大大提高。
2. 垃圾回收算法
在 Dalvik 采用的垃圾回收算法是标记-清除算法,启动垃圾回收机制会造成两次暂停(一次在遍历阶段,另一次在标记阶段)。而在 ART 下,GC 速度比 Dalvik 要快,这是因为应用本身做了垃圾回收的一些工作,启动 GC 后,不再是两次暂停,而是一次暂停,而且 ART 使用了一种新技术(packard pre-cleaning),在暂停前做了许多事情,减轻了暂停时的工作量。
3. 64 位
Dalvik 是为 32 位 CPU 设计的,而 ART 支持 64 位并兼容 32 位 CPU,这也是 Dalvik 被淘汰的主要原因。
由此可知,ART 让 Android 的性能有了很大的提升,从 2015 直到现在,我们使用的都还是 ART 虚拟机。
下图为 ART 的整体架构,我们可以看出,上层是执行层,负责直接对书写的代码进行处理,而下层则为运行时刻对 Java 语法的支持。
对象的分配
对于对象的分配问题,实际上是 ART 对于类的管理问题。而类中则是描述了一个对象的内存布局(类成员的大小、类型和排布)和其函数信息。
例如 Object 类,包含以下的信息:
一个保存的是类型定义,一个保存的是锁的信息。
类加载
一个类分配的对象的大小,是由继承链所决定的。当 Java 中的类首次使用的时候,就会进行类加载。例如首次使用到一个子类的时候,会自动将继承链上面的所有父类都进行加载,而整个继承链上面的类的总和就是该子类的大小。
例如下文中的子类的大小就是 AWork
+ BaseWork
两者合起来的大小。
puvlic class AWork extends BaseWork{
public AWork(WorkBean workBean){
super(work);
}
@Override
public process(Processbean processbean){
workBean.getA().actionA(processbean.getProcessA);
}
}
内存布局
如下图所示,当有 A->B->Object
这个继承关系的时候,其内存布局是父类在上,子类在下的方式进行排布的。而在每一个类里面,则是将引用类型置于最上方,而其他的类型则按字母顺序进行排序。
双亲继承(双亲委派)
何为双亲继承呢?
双亲委派的意思是如果一个类加载器需要加载类,那么首先它会把这个类请求委派给父类加载器去完成,每一层都是如此。一直递归到顶层,当父加载器无法完成这个请求时,子类才会尝试去加载。
这么做的好处有一点,那就是不会出现假的委派父类,我们在委派的时候按照既定的逻辑寻找、只有在继承链上面的才是正确的,使得不会有虚假的父类出现。
这类底层的逻辑,反映出合理的继承链是有利于设计和执行的。其实由此我们也可以看到,其实很多设计原则的道理和这些底层逻辑设计也是相同的,例如迪米特原则和接口隔离原则,都是反映出继承链要合理,不要贪多的思维。
链接:https://juejin.cn/post/7158466528013697038
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
我爸53岁了,居然还能找到年薪25万的管理岗位,突然很羡慕传统行业!
在35岁焦虑席卷许多打工人的时候,一位53岁的老父亲竟然找到了年薪25万的管理岗位,他的儿子不禁感叹“突然有点羡慕传统行业”!
有人问楼主父亲是什么行业?
楼主回答:造船。
楼主说,父亲之前在央企做项目经理,年薪也有四十几万,后来得罪人被降职,辞职后失业两年,尝试过很多职业,经历了这么大落差,心态还这么好,真的很佩服他。
许多网友纷纷出来爆料自己的家人也有类似经历,大多都是五六十岁还能找到不错的工作,或者挣的钱比自己还多。
网友感叹:治好了自己的精神内耗。
有人说,这才是正常的,在一个行业耕耘多年,有经验的人不该失业,在传统行业里,三四十岁正是挑大梁的时候,年龄越大挣得越多。
有人说,深耕一个领域的人不缺offer ,因为有不可替代性。打铁还需自身硬,只要是人才,走到哪里都是人才。
有人建议应届生选一个能在一个赛道做久的行业,不要因为一点涨幅就频繁换行业。
但也有人说,就算年薪25万,应届生依然不愿意去一些行业,因为传统行业真的很苦。
不是每一个行业都是吃青春饭,也不是每一个行业都有35岁红线,相反,许多行业是越老越值钱,比如医生、教师、律师、会计、制造等。在这些行业里,年龄大意味着更丰富的经验和阅历,可以担当更重要的责任,承担更重要的工作,自然也能拿到更高的薪资。
可能是互联网行业的声音更容易被听到,时间久了,人们觉得高薪和大龄被裁是所有行业的现状。其实在我们不注意的地方,在许多低调的传统行业里,那些大龄打工人也生活得很好,甚至比互联网行业从业者还好。
所以,在选择行业和赛道时,别总盯着眼前的一亩三分地,多去了解了解那些不起眼的行业,说不定会有意外之喜。
作者:行者
来源:devabc
收起阅读 »安卓之如何优雅的处理Activity回收突发事件
情景与原因
前面的文章说过,我的一个业务要从页面A进入页面B,也就意味着我的应用出现了在ActivityA的基础上启动了ActivityB的情景,这个时候ActivityA就进入了停止状态,但这个时候如果出现系统内存不足的情况,就会把ActivityA回收掉,此时用户按下Back键返回A,仍然会正常显示A,但此时的A是执行onCreate()方法加载的,而不是执行onRestart()方法,也就是说我们的ActivityA页面是被重新创建出来的。
那有什么区别呢,这就是我们平时网上填表之类的最讨厌的情景会浮现了,比如你好不容易把信息填好,点击下一步,然后发现想修改上个页面信息,哎,这时候你会发现由于系统内存不足回收掉了A,于是你辛辛苦苦填好的信息都没了,这太痛了。这一情景的出现就说明回收了A再重新创建会导致我们在A当中存的一些临时数据与状态都可能被丢失。
这就是我们今天要解决的问题。
解决方法
虽然出现这个问题是是否影响用户体验的,但解决方法是非常简单的,因为我们的Activity中还提供了一个onSaveInstanceState()回调方法,它就可以保证在Activity被回收之前一定被调用,而我们就可以利用这一特性去解决上述问题。
方法介绍
onSaveInstanceState()方法中会携带一个Bundle类型的参数,而Bundle提供了一系列方法用于保存我们想要的数据,其中如putString()就是用于保存字符串的,而putInt()方法显而易见也是保存整型数据的,而每个保存方法都需要传入两个参数,其中第一个参数我们称之为键,是用于在Bundle中取值的特定标记,第二个参数是我们要保存的内容,属于键值对类型,学过数据库的一般都知道。
写法
如下,我们可以这么去写:
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
val tempData = "your temp data"
outState.putString("data_key", tempData)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (savedInstanceState != null) {
val tempData = savedInstanceState.getString("data_key")
}
...
}
在onSaveInstanceState()中保存你想要的数据,在onCreate()中给取出来,这样就不用害怕再创建的时候数据丢失了,不过如果是横竖屏切换造成数据丢失还是依照前文ViewModel的写法更好。
结语
其实基础往往是最容易忽视的,我们之前光在那说高楼大厦如何建成,但地基并不牢靠,所以还是需要落实到基础上最好。
作者:ObliviateOnline
来源:juejin.cn/post/7158096746583687205
整体学历较高,硕士占比达 40%,周星驰也开始招募Web3人才!
编辑:Datawhale
近期,周星驰发布人才招募令。
人才要求:熟悉Web3,有项目管理经验,有头脑还又宅心仁厚;工作范围:助我建造创意未来;还提醒对此职位感兴趣的候选人投简历时请贴出个人简介影片或Web3作品并tag,他本人会亲自拣人。
随着数字经济的发展,Web3 成为了新的风口,各大厂商和投资人纷纷将目光聚集其上。在部分人眼中,Web3 能够重塑数字金融交易体系,改变全球竞争格局,在未来互联网上实现弯道超车。其中,不乏顶级投资机构红杉和 A16Z,前者甚至一度将简介更改为 DAO。
不过,时不时出现的裁员新闻又让行业之外的人对 Web3 望而生畏。如 Coinbase 此前宣布裁员 18%,规模高达上千人。当然,Web3 也存在正不断开放招聘岗位的企业。
传说中可以跨国分布式从业的 Web3 是精英遍布还是草根丛生,什么样的人才是目前 Web3 企业所需要的成为了大部分用户想了解的信息呢?
Web3企业需要的是什么人才?
中国人才增速较低,但需求强劲
从宏观上来看,截至 2022 年 6 月,区块链人才总量同比增加了 76%,其中,印度、新加坡和美国增速最高,分别为 122%、92%、62%,中国相对较低,仅仅只有 12%。
在人才总量增加的同时,人才需求量呈现出远超供应的增长。根据领英人才大数据洞察获得的数据来看,2021 年相较 2020 年人才需求呈倍数级增长,其中,加拿大增速最高,达到了 560%。印度、新加坡、美国、中国的增速分别为 145%、180%、82%、78%。
虽然供需缺口,但事实上,除了科技和金融公司以外,大部分区块链的人才主要又以内部流动为主。领英人才大数据洞察显示,2021 年至今,人才主要在 Coinbase、Crypto.com、Gemeni、Rippl e 等区块链企业间交叉流动。而外部流入的人才主要来自华尔街和硅谷等地知名巨头如高盛、JPMorgan、HSBS、谷歌、微软、Facebook 等。
从人才的需求端和供应端来看,区块链的人才受到地区限制少,在全球各地都呈现需求量增长的趋势。但是从绝对数量上来看,美国、法国、英国等发达国家依旧占据着优势。对于金融业和 IT 业较为繁荣的国家而言,切入区块链和 Web3 存在着不小的产业优势。
具体到中国的区块链产业上,根据 IDC 研究预测,中国 2020-2025 年区块链市场规模年复合增长率将达 54.6%,增速位居全球第一。而全球区块链市场规模年复合增长率将达 48%。
换言之,中国目前的区块链产业对人才的需求量大,但进入该行业的人才少,同时,中国区块链产业后续增长强劲。从报告来看,换方向从事 Web3 行业的工作对于个人的发展而言或许是一个不错的选择。
核心人才需求主要以金融和研发为主
相较于 Web1 和 Web2 而言,Web3 的定义更宽泛。目前业内对于 Web3 并没有严格的定义,不过其有几个较为明显的特征,比如数据的确权与授权、隐私保护、去中心化运行等等。而这些明显的特征决定了行业主要人才的构成。
从全球区块链领域人才构成上分析,金融、研发、业务开发、信息技术、销售人才为全球区块链前五大人才类型。
全球区块链领域前五大人才类型中,最热门细分职业分别为加密货币交易员、软件工程师、分析师、支持分析师及客户经理。
从人才增速来看,测试工程师、密码逻辑技术专家、合规分析师、设计师和支持分析师分别位列前五,其中,测试工程师增速高达 713%。
从人才构成和需求来看,可以发现,区块链行业发展依旧处于早期阶段,大量的基础设施正在搭建。区块链人才的构成成分最主要还是取决于行业的发展。在行业发展的初期阶段往往需要大量基础性的工作职位,如研发、开发、产品构建。如想要等区块链行业发展更加成熟之后再参与这个行业,或许可以锻炼自己运营、营销、市场等方面的能力。
同时,需要注意的是,不同的国家和地区对于人才的需求也有较为明显的差异,人才容易在地域上产生集聚效应,如大量工程师聚集于硅谷。在考虑城市和职业方向的同时,或许还得思考城市和职业的契合度。如在区块链领域,新加坡侧重于产品经理、软件工程师的招聘,而中国香港更侧重于产品设计师、用户体验作者等。
人才竞争初始,硕士从业人数占比 40%
由于市场对区块链人才的需求远远超过供应,区块链从业者的平均薪资已经超过了大部分行业。
据 Glassdoor 报告,美国区块链开发人员的平均年基本工资为 9.1 万美元。而 2020 年美国社会安全署数据显示,美国民众平均年薪 5.3 万美元,中位数为 3.4 万美元。同样,北京人社局于 2021 年 11 月发布的《2021 年北京市人力资源市场薪酬大数据报告》,在 30 个新职业薪酬排行榜中,区块链工程技术人员最高,年度薪酬中位值达 48.7 万人民币。北、上、广、深等重点城市区块链产业人才平均年薪水平大幅领先城市整体产业人才平均年薪水平。
高薪促使着优秀的人才向区块链行业聚拢,目前全球区块链领域中学士群体占 59%,硕士占比达 40%,整体学历较高。同时,数据显示,全球区块链人才排名前十的学校均为世界知名院校,其中包括加州大学伯克利分校、斯坦福大学、哈佛大学等顶级大学。
与此同时,中国包括中央财经大学、同济大学、浙江大学等在内的多所双一流大学也开设了区块链课程。
综合来看,目前区块链行业依旧处于起始阶段,各国的政策扶持力度正在不断加大,人才流动频繁,需求量巨大。作为从业者,除了学历等硬性指标外,还需要持续拓展延伸自己的能力,从而持续构建核心竞争力。
来源:Datawhale
我的灿烂前端人生
本人是 95 前端菜鸟一枚,目前在广州打工混口饭吃。刚好换了工作,感觉生活节奏变得慢了下来,打了这么多年工总觉得想纪录些什么,怕以后自己老了忘记自己还有这么一些风流往事。书接上回
公司太子
北京时间 18 点 50 分,离下班时间还有十分钟,本该是令人愉悦的时刻,我心里的雾霾又浓郁了一分。因为我在公司当太子
当了大半年了。
能力出众
遥想今年年初,领着上家公司大礼包四处求职碰壁,踏破铁鞋寻寻觅觅,靠着投机取巧的八股文背诵,终于求得广州一家高大上小企业
公司的岗位。入职不到一周立刻加入新的项目团队,做一个抽奖小程序,技术栈是 typescript+taro
,我之前没有深入开发过,十分的开心,又可以边工作边学习了。花费三个多月,与团队之间不断擦枪走火,这个项目也是勉强完成,开发完成之余,我有空也加入了测试大军,生怕自己第一个项目上线后因为自己的 bug 造成毁灭的影响毕竟以前经常发生
。万万没想到,这个项目最终没有落地,老板总结就是我们做的打不过别人竞品,没啥创新,让团队去搞商城小程序去了。我万分失落惊喜
,心里想着这样岂不是等于我做了三个月的项目稳定在线上运行,没有bug,不会被用户投诉,也不会被影响绩效,安稳白嫖三个月薪资?
美滋滋!。
度过三个月的试用期,因为项目线上无 bug,能力出众,我也如愿以偿拿下转正。
虚空需求
完成了上一任务,接下来 leader 给我分配了一个大 project,重构以前管理后台的权限。这波重构任务,是 leader 直接文字需求下达指令了,我有点头皮发麻,好几年没遇到这种需求了,真的是梦回 S1 赛季,本来和我合作的小伙伴说他要做个原型出来,结果因为分配任务我负责管理后台前端,他负责管理后台 nodejs 的代码,他也就没有做出来,让原型图随风而去,跟我说了句一把梭
。我也想一把梭,但我发现 leader 的需求十分灵性
,加之我对之前的业务也不熟悉,想着还是花点时间加班把原型图做一下吧。
我战战兢兢的把原型图发到群里,leader 已读并回了没啥问题了,可以开工。我悬着的心放了下来,撸起袖子大胆干。说实话,我心里其实很慌的,首先对 React+Typescript
不熟悉,且这套管理后台十分深奥,用的是自研的核心框架,各种 typescript abstract
抽象类,复杂的类型泛型,对我这个半吊子前端还是比较吃力的。但好在我是拷贝忍者,写业务代码先找下之前代码是怎么写的,CCCV,改个英文单词,就是我的杰作
。
TX leader 真的很严格
我的 leader 是腾讯大厂出来的,我也是打心底里对他有一丝敬畏,毕竟大厂大佬恐怖如斯,技术水平肯定不是我这种切图仔比拟的。
任务花费了三周多一点,包含联调自测,自测完后就提个 MQ
上去了,信心十足。万万没想到,leaderCode Review
对着我的杰作
一顿输出,大概有二十几个修改建议,我都有仔细去看,发现很多都是代码规范,代码优化,leader 都给了一定的建议。说实话,一开始我的心里多多少少有些芥蒂,但是谁让别人是领导呢?开个玩笑
。但是 leader 指出来的问题的确是不容忽视的,程序员就是要有更好的追求,其实有人把问题指出来,才是对我最大的帮助,我也是花了不少时间去更改这些问题。下面就放一些 bad code
出来献丑。
之前一直想不明白,传进来的组件是在 children 里面,我如何去改变组件的点击函数,想来想去想不懂,脑门一热直接在组件上加一层蒙层,通过蒙层阻碍组件点击,当时设计完出来我还挺高兴,leader 也直呼天才,送了我两个字 ———— 重做
因为我技术能力确实平庸,只能请教我的良师百度
,不断去寻找 children 是否有什么方法或钩子处理事件,功夫不负有心人,果真被我找到了。下面就是修改后的方法
// after
return permission ? children : React.Children.map(children, child => React.cloneElement(child as React.ReactElement, { onClick: () => { message.error('无权限'); } }));
ps:leader 也勉为其难的接受这个方法,可能他不知道有什么更好的方法。如果观众大佬们知道,可以提下意见,不胜感激。
设计组织架构图
先让大伙看看原来的功能图吧,之后我们开了一个会议,这里要重做。
我心想我发原型图出来的时候,大佬您可是没有半个不字,怎么 codereview 直接改了一个方向了啊?
不过,毕竟他是我的 leader,我的生死全由他掌控
,我也不敢多言,上网找了一个 npm 库 react-organizational-chart
。react 的社区就是强~下面是更改后的视图
不得不说,的确是更饱满更清晰直观了一些,leader 还是很有远见的怕他也上掘金,吹了再说
。
这个项目陆陆续续做了三个月了,因为 leader 平时也很忙,两个城市飞,导致这个项目的进度也进展缓慢,而我就在空闲时间上上掘金学习技术,刷刷 leetcode。
来了大半年,我深刻明白我对公司的建设为 0,所做项目为公司带来 0 收入,就是我的价值完全没有体现,公司把我当太子
养了大半年,我非常感谢公司。然后每天都会浏览 boss 直聘,深怕下午就被拉进小黑屋,在这个大环境下,我也时刻准备着,毕竟也有前车之鉴,我明白我只是个平庸的程序员,只能尽力做好自己的本分,随时做好最坏的打算,当真正的打击来临之时,我也不会手忙脚乱。
灿烂?摆烂!
最近 IT 的 HRBP 要我一个新入职的去做一场技术分享,我在这里呆了大半年,没有等来其他前端大佬的分享,竟然是要我亲自上阵,小丑竟是我自己
。
空虚寂寞冷
回想了一下这六个月,其实自己的水平真的没有半点进步,我想不到有什么可以拿来分享的。而且从入职以来,我在这个公司说的话可能没有超过 100句
,其实有时我也纳闷,我印象中自己不是一个这么闷的一个人,在上家公司我吹 * 技术游走于天地之间,能很好的融入团队,并能展开身心为其奋斗前期战神,后期老油条
。但是来了新公司之后,我只会干完手头上的活,也没有跟其他同事聊聊天,不过我附近的同事也极少聊天,感觉稍微有点死气沉沉。
以前年轻的时候,看到一些新入职的同事,闷葫芦一个,找他搭话或者说骚话,他都没啥兴趣,现在的我,好像成为了自己以前眼中的怪人
。我苦思久已,只能得出几个结论,第一点可能是我以前投入太多,经历过分离,不想再投入更多的感情,投入的越深,离开时就越痛 1000-7=? 痛,太痛了
。第二点是因为现在的大环境,让我精神焦虑,我深怕我和某位同事今天刚去饭堂吃个饭,明天人就没了。想看我之前为啥被裁,可以看我往期文章。
不过,我觉得出来工作,重点是挣钱,以这个为核心,其他一切都是空谈。而且,解决我的聊天需求还有一大神器,不是陌陌,而是网易狼人杀APP
。自从入职新公司以来,每天下班回到家根本不想学习,不想运动,只想躺着,然后冲进大师场厮杀,里面个个都是人才,说话又好听,我喜欢这个游戏,因为它能锻炼提高我的骗人能力当然是表达能力啦!
而且它还夹杂着些许人性的味道,人性的魅力也让我欲罢不能。网易打钱
。所以要我分享,我真不知道分享什么,难道分享如何悍跳吃警徽,狼查杀狼打板子做高狼同伴身份?
保持平常心
最终 leader 让我去分享一下这个重构项目,我想了一下也可以,其实它不是一次分享,可以把它当做一次项目复盘,把自己的问题抛出来给到大家欣赏
,虽然有点丢人,但是赚钱嘛,不寒碜
。而且自己的技术也拉胯,可以让自己加深这些问题的印象,对自己成长的路也是有极大帮助的。
不止是大环境,最近社会也出现了许多光怪陆离的事情,心态也有些许变化,我不再绞尽脑汁去想着如何跳槽获得高薪,我只想取悦自己,做自己认为让自己开心而正确的事情,心累了就去外面走走,馋了就去吃点美食,觉得知识匮乏了就化身小厂做题家
刷刷 leetcode,看看别人的源码见解虽然多数都看不懂
。偶尔什么都想学,什么都学不进去的时候,也会焦虑,解决焦虑的办法,我常常是...... 奖励自己
。
当下所面临的的困难、焦虑,都会被时间而抚平,我作为一个平庸程序员,面对每天新开始的人生,我只能对自己说一句,啊,又是新的一天
。
来源:稀土掘金
组员大眼瞪小眼,forEach 处理异步任务遇到的坑
一位组员遇到一个问题,几个同事都没能帮忙解决,我在这边就开门见山直接描述当时他遇到的问题。他在 forEach 处理了异步,但是始终不能顺序执行,至此想要的数据怎么都拿不到,组员绞尽脑汁,不知道问题发生在哪里。此篇文章我们就来探究下 forEach 循环下处理异步会发生什么样的情况。
探索
我们先看一段简单的 forEach 处理异步的代码
//forEach 处理
let promiseTasek = (num) => {
return new Promise((resolve, rejece) => {
setTimeout(() => {
console.log(num)
resolve(true)
}, 4000)
})
}
function toTaskByForEach() {
const arr = [1, 2, 3, 4, 5, 6]
arr.forEach(async (item) => {
await promiseTasek(item)
})
}
toTaskByForEach()
执行结果 注意执行输出的变化,他会直接打印出 1,2,3,4,5,6 本来想录制一个 gif 的,确实没找到一个好的工具录制浏览器的控制台
我们尝试换一种循环 for of 看一下效果对比一下
let promiseTasek = (num) => {
return new Promise((resolve, rejece) => {
setTimeout(() => {
console.log(num)
resolve(true)
}, 4000)
})
}
async function toTaskByForOf(){
const arr = [1,2,3,4,5,6]
for (let i of arr) {
await promiseTasek(i)
}
}
toTaskByForOf()
来看下执行结果 他会按顺序执行依次打印出 1,2,3,4,5,6
所以这是为啥呢
后来我们研究了一下 map
//forEach 处理
let promiseTasek = (num) => {
return new Promise((resolve, rejece) => {
setTimeout(() => {
console.log(num)
resolve(true)
}, 4000)
})
}
function toTaskByMap() {
const arr = [1, 2, 3, 4, 5, 6]
arr.map(async (item) => {
await promiseTasek(item)
})
}
toTaskByMap()
输出结果和 forEach 一样
后来我们发现 Array.prototype.forEach 不是一个 async 函数,即使 Array.prototype.forEach 的参数 callback 是 async 函数,也暂停不了 Array.prototype.forEach 函数,map 也是同理
await Promise.all(arr.map(async (item) => { /** ... */ }))
安卓关于Bitmap.isRecycled()空指针报错的解决方案
前言
起因是我在开发功能需要使用Bitmap的方法:
BitmapFactory.decodeResource(my.main.getResources(),R.drawable.vector_my_need);
结果就倒大霉,运行时直接报错:
java.lang.NullPointerException: Attempt to invoke virtual method 'boolean android.graphics.Bitmap.isRecycled()' on a null object reference
从日志分析,我们知道是出现了空指针,当时我先是想自己找原因,结果定位到view的源代码里,是在draw方法中,但没找到,我还没放弃,于是又定位了updateDisplayListIfDirty() 方法以及其他报错对应点,于是终于发现了是我bitmap的使用出了问题:
找到问题固然是好事,可是如何解决呢?这就要靠搜索了,接下来让我们看看解决方法。
正篇
正确的搜索方法
其实我在搜索上吃了许多亏,一开始在国内搜索上一直给我推C站的结果虽然有点相似但其实都相差甚远,最后我在StackOverflow上找到了答案,这也是曾经让别人困惑的一个问题:
可以看到有31K人浏览过此问题,所以该问题早有认可的答案:
意思说,我们用的vector矢量可绘制对象需要创建位图,而不是对其进行解码,且方法在下一个帖子中。
完美的解决方案
其实说到这,我已经明白,是我用SVG图资源放到安卓项目中转成Vector的xml文件,这种文件解码无法获得正确的bitmap,于是我恍然大悟的点开了下个帖子:
我为这个标准答案标记了中文解释,给出的是将我们的vector资源实例成Drawable对象,然后通过Bitmap的创建方法去创建成一个新的bitmap,代码如下:
Drawable d = getResources().getDrawable(R.drawable.your_drawable, your_app_theme);
这一步就是变成Drawable对象,接下来:
public static Bitmap drawableToBitmap (Drawable drawable) {
if (drawable instanceof BitmapDrawable) {
return ((BitmapDrawable)drawable).getBitmap();
}
Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return bitmap;
}
这段代码是将Drawable对象转换成Bitmap封装成了工具方法,便于直接应用于主代码。
而方法内部,也分成两层:
首先第一层,我们用instanceof测试它左边的对象是否是它右边的类的实例,如果是真命题则直接返回强制转成BitmapDrawable,并直接调用它的getBitmap()方法即可。
而如果第一层没有成功,则由第二层处理,我们先实例化Bitmap对象,利用Bitmap的createBitmap()方法输入drawable对象的固有宽高和BItmap通道配置获取bitmap
然后调用canvas绘制bitmap,最后先用drawable的setBounds()方法为Drawable对象指定一个边界矩形,这是为了调用 draw() 方法前可以确定绘制对象将绘制的位置,接着用draw()方法完成绘制,返回最终的bitmap即完成。
结语
这还是我第一次这么容易就获取到了明确的正确解决方案,所以特地记录下来,当然,如果你出现了这个空指针就不需要去看英文结果了,让我们更方便的解决这个问题吧。
链接:https://juejin.cn/post/7156981458400509966
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Android动态更换应用图标
一、背景
近日,微博官方发布了一项新功能,即可以在App设置中动态更换微博的显示图标样式。根据微博官方的说法,除了最原始的图标外,微博还推出了另外10种不同的样式,既有3D微博、炫彩微博等保留了眼睛造型的新样式,也有奶酪甜馨、巧克力等以食物命名的“新口味”,还有梦幻紫、幻想星空等抽象派新造型,给了微博用户多种选择的自由。
不过需要注意的是,这一功能并不是面对所有人开放的,只有微博年费会员才能享受。此外,iOS 10.3及以上和Android 10及以上系统版本支持该功能,但是iPad与一加8Pro手机无法使用该功能。因部分手机存在系统差异,会导致该功能不可用,微博方面后续还会对该功能进行进一步优化。
二、技术实现
其实,说到底,上述功能用到的是动态更换桌面图标的技术。如果说多年以前,实现图标的切换还是一种时髦的技术,那么,我们可以直接使用PackageManager就可以实现动态更换桌面图标。
实现的细节是,在Manifest文件中使用标签准备多个Activity入口,没个activity都指向入口Activity,并且为每个拥有标签的activity设置单独的icon和应用名,最后调用SystemService 服务kill掉launcher,并执行launcher的重启操作。
首先,我们在AndroidManifest.xml文件中添加如下代码:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.xzh.demo">
<!-- 权限-->
<uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES"/>
<application
android:allowBackup="true"
android:icon="@mipmap/wb_default_logo"
android:label="@string/app_name"
android:roundIcon="@mipmap/wb_default_logo"
android:supportsRtl="true"
android:theme="@style/Theme.AndroidDemo">
...//省略其他代码
<!-- 默认微博-->
<activity-alias
android:name="com.xzh.demo.default"
android:targetActivity=".MainActivity"
android:label="@string/app_name"
android:enabled="false"
android:icon="@mipmap/wb_default_logo"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<!-- 3D微博-->
<activity-alias
android:name=".threedweibo"
android:targetActivity=".MainActivity"
android:label="@string/wb_3d"
android:enabled="false"
android:icon="@mipmap/wb_3dweibo"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
... //省略其他
</application>
</manifest>
上面配置中涉及到的属性如下:
- android:name:注册的组件名字,启动组件的名称。
- android:enabled:是否启用这个组件,也就是是否显示这个入口。
- android:icon:图标
- android:label:名称
- android:targetActivity:默认的activity没有这个属性,指定目标activity,与默认的activity中的name属性是一样的,需要有相应的java类文件。
接着,我们在MainActivity触发Logo图标更换逻辑,代码如下:
class MainActivity : AppCompatActivity() {
var list: List<LogoBean> = ArrayList()
var recyclerView: RecyclerView? = null
var adapter: LogoAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initView()
initData()
initRecycle()
}
private fun initView() {
recyclerView = findViewById(R.id.recycle_view)
}
private fun initData() {
list = Arrays.asList(
LogoBean(R.mipmap.wb_default_logo, "默认图标", true),
LogoBean(R.mipmap.wb_3dweibo, "3D微博", false),
LogoBean(R.mipmap.wb_cheese_sweetheart, "奶酪甜心", false),
LogoBean(R.mipmap.wb_chocolate_sweetheart, "巧克力", false),
LogoBean(R.mipmap.wb_clear_colorful, "清透七彩", false),
LogoBean(R.mipmap.wb_colorful_sunset, "多彩日落", false),
LogoBean(R.mipmap.wb_colorful_weibo, "炫彩微博", false),
LogoBean(R.mipmap.wb_cool_pool, "清凉泳池", false),
LogoBean(R.mipmap.wb_fantasy_purple, "梦幻紫", false),
LogoBean(R.mipmap.wb_fantasy_starry_sky, "幻想星空", false),
LogoBean(R.mipmap.wb_hot_weibo, "热感微博", false),
)
}
private fun initRecycle() {
adapter =LogoAdapter(this,list);
val layoutManager = GridLayoutManager(this, 3)
recyclerView?.layoutManager = layoutManager
recyclerView?.adapter = adapter
adapter?.setOnItemClickListener(object : OnItemClickListener {
override fun onItemClick(view: View?, position: Int) {
if(position==1){
changeLogo("com.xzh.demo.threedweibo")
}else if (position==2){
changeLogo("com.xzh.demo.cheese")
}else if (position==3){
changeLogo("com.xzh.demo.chocolate")
}else {
changeLogo("com.xzh.demo.default")
}
}
})
}
fun changeLogo(name: String) {
val pm = packageManager
pm.setComponentEnabledSetting(
componentName,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP
)
pm.setComponentEnabledSetting(
ComponentName(this, name),
PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP
)
reStartApp(pm)
}
fun reStartApp(pm: PackageManager) {
val am = getSystemService(ACTIVITY_SERVICE) as ActivityManager
val intent = Intent(Intent.ACTION_MAIN)
intent.addCategory(Intent.CATEGORY_HOME)
intent.addCategory(Intent.CATEGORY_DEFAULT)
val resolveInfos = pm.queryIntentActivities(intent, 0)
for (resolveInfo in resolveInfos) {
if (resolveInfo.activityInfo != null) {
am.killBackgroundProcesses(resolveInfo.activityInfo.packageName)
}
}
}
}
注意上面的changeLogo()方法中的字符串需要和AndroidManifest.xml文件中的<activity-alias>
的name相对应。运行上面的代码,然后点击应用中的某个图标,就可以更换应用的桌面图标,如下图所示。
不过,测试的时候也遇到一些适配问题:
- 小米9:版本升级时,新版本在AndroidManifest中删除A3,老版本切换图标到A3,为卸载直接覆盖安装新版本,手机桌面图标消失。
- magic 4:版本升级时,新版本在AndroidManifest中删除A3,老版本切换图标到A3,为卸载直接覆盖安装新版本,手机桌面图标切换到默认图标,但点击之后未能打开APP。
链接:https://juejin.cn/post/7115413271946985480
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Android gradle迁移至kts
背景
在kotlin语言已经渗透至各领域的环境下,比如服务端,android,跨平台Kmm,native for kotlin,几乎所有的领域都可以用kotlin去编写了,当然还有不成熟的地方,但是JB的目标是很一致的!我们最常用的gradle构建工具,也支持kotlin好久了,但是由于编译速度或者转换成本的原因,真正实现kts转换的项目很少。在笔者的mac m1 中使用最新版的AS去编译build.gradle.kts,速度已经是和用groovy写的gradle脚本不相上下了,所以就准备写了这篇文章,希望做一个记录与分享。
groovy | kotlin |
---|---|
好处:构建速度较快,运用广泛,动态灵活 | 好处:编译时完成所有,语法简洁,android项目中可用一套语言开发构建脚本与app编写 |
坏处:语法糖的作用下,很难理解gradle运行的全貌,作用单一,维护成本较高 | 坏处:编译略慢于groovy,学习资料较少 |
虽然主流的gradle脚本编写依旧是groovy,但是android开发者官网也在推荐迁移到kotlin
编译前准备
这里推荐看看这篇文章,里面也涵盖了很多干货,
全局替换‘’为“”
在kotlin中,表示一个字符串用“”,不同于groovy的‘ ’,所以我们需要全局替换。可以通过快捷方式command/control+shift+R 全局替换,选中匹配正则表达式并设定file mask 为 *.gradle:
正则表达式
'(.*?[^\\])'
作用范围为
"$1"
全局替换方法调用
在groovy中,方法是可以隐藏(),举个例子
apply plugin: "com.android.application"
这里实际上是调用apply方法,然后命名参数是plugin,内容围为"com.android.application",然而在kotlin语法中,我们需要以()或者invoke的方式才能调用一个方法,所以我们要给所有的groovy函数调用添加()
正则表达式
(\w+) (([^=\{\s]+)(.*))
作用范围为
$1($2)
很遗憾的是,这个对于多行来说还是存在不足的,所以我们全局替换后还需要手动去修正部分内容即可,这里我们只要记得一个原则即可,想要调用一个kotlin函数,把参数包裹在()内即可,比如调用一个task函数,那么参数即为
task(sourcesJar(type: Jar) {
from(android.sourceSets.main.java.srcDirs)
classifier = "sources"
})
gradle kt化
接下来我们只需要把build.gradle 更改为文件名称为build.gradle.kts 即可,由于我们修改文件为了build.gradle.kts,所以当前就以kts脚本进行编译,所以很多的参数都是处于找不到状态的,即使sync也会报错,所以我们需要把报错的地方先注释掉,然后再进行sync操作,如果成功的话,AS就会帮我们进行一次编译,此时就可以有代码提示了。
开始前准备
以kotlin的方式编译,此时函数就处于可点击查看状态,区别于groovy,因为groovy是动态类型语言,所以很多做了很多语法糖,但是也给我们在debug阶段带来了很多困难,比如没有提示等等,因为groovy只需要保证在运行时找到函数即可,而kotlin却不一样,所以很多动态生成的函数就无法在编译时直接使用了,比如
对于这种动态函数,kotlin for gradle 其实也给我们内置了很多参数来对应着groovy的动态函数,下面我们来从以下方面去实践吧,tip:以下是gradle脚本编写常用
ext
我们在groovy脚本中,可以定义额外的变量在ext{}中,那么这个在kotlin中可以使用吗?嘿嘿,能用我就不会提到对吧!对的,不可以,因为ext也是一个动态函数,我们kotlin可没法用呀!那怎么办!别怕,kts中给我们定义了一个类似的变量,即extra,我们可以通过by extra去定义,然后就可以自由用我们的myNewProperty变量啦!
val myNewProperty by extra("initial value")
但是,如果我们在其他的gradle.kts脚本中用myNewProperty这个变量,那么也会找不到,因为myNewProperty这个的作用域其实只在当前文件中,确切来说是我们的build.gradle 最后会被编译生成一个Build_Init的类,这个类里面的东西能用的前提是,被先编译过!如果当前编译中的module引用了未被编译的module的变量,这当然不可行啦!当然,还是有对策的,我们可以在BuildScr这个module中定义自定义的函数,因为BuildScr这个module被定义在第一个先执行的module,所以我们后面的module就可以引用到这个“第一个module”的变量的方式去引用自定义的变量!
task
- 新建task
groovy版本
task clean(type: Delete) {
delete rootProject.buildDir
}
比如clean就是一个我们自定义的task,转换为kotlin后其实也很简单,task是一个函数名,Delete是task的类型,clean是自定义名称
task("clean",{
delete(rootProject.buildDir)
})
当然,我们的task类型可能在编写的由于泛型推断,隐藏了具体的类型,这个时候我们可以通过
./gradlew help --task task名
去查看相应的类型
- 已有task修改
对于有些是已经在gradle编译时存在的函数任务,比如
groovy版本
wrapper{
gradleVersion = "7.1.1"
distributionType = Wrapper.DistributionType.BIN
}
这个我们kotlin版本的build.gradle能不能识别呢?其实是不可以的,因为编译器也不知道从哪里去找wrapper的定义,因为这个函数在groovy中隐藏了作用域,其实它存在于TaskContainerScope这个作用域中,所以对于所有的的task,其实都是执行在这里面的,我们可以通过tasks去找到
tasks {
named("wrapper") {
gradleVersion = "7.1.1"
distributionType = Wrapper.DistributionType.BIN
}
}
这种方式,去找到一个我们想要的task,并配置其内容
- 生命周期函数
我们可以通过函数调用的方式去配置相应的生命周期函数,比如doLast
tasks.create("greeting") {
doLast { println("Hello, World!") }
}
再比如dependOn
task("javadocJar", {
dependsOn(tasks.findByName("javadoc"))
})
动态函数
sourceSets就是一个典型的动态函数,为什么这么说,因为很多plugin都有自己的设置,比如Groovy的sourceSets,再比如Android的SourceSets,它其实是一个接口,正在实现其实是在plugin中。如果我们需要自定义配置一些东西,比如配置jniLibs的libs目录,直接迁移到kts就会出现main找不到的情况,这里是因为main不是一个内置的函数,但是存在相应的成员,这个时候我们可以通过by getting方式去获取,只要我们的变量在作用域内是存在的(编译阶段会添加),就可以获取到。如果我们想要生成其他成员,也可以通过by creating{}方式去生成一个没有的成员
sourceSets{
val main by getting{
jniLibs.srcDirs("src/main/libs")
jni.srcDirs()
}
}
也可以通过getByName方式去获取
sourceSets.getByName("main")
plugins
在比较旧的版本中,我们AS默认创建引入一个plugin的方式是
apply plugin: 'com.android.application'
其实这也是依赖了groovy的动态编译机制,这里针对的是,比如android{}作用域,如果我们转换成了build.gradle.kts,我们会惊讶的发现,android{}这个作用域居然爆红找不到了!这个时候我们需要改写成
plugins {
id("com.android.application")
}
就能够找到了,那么这背后的原理是什么呢?我们有必要去探究一下gradle的内部实现。
说了这么多的应用层写法,了解我的小伙伴肯定知道,原理解析肯定是放在最后啦!但是gradle是一个庞大的工程,单单靠着干唠是写不完的,所以我选出了最重要的一个例子,即plugins的解析,希望能够抛砖引玉,一起学习下去吧!
Plugins解析
我们可以通过在gradle文件中设置断点,然后debug运行gradle调试来学习gradle,最终在编译时,我们会走到DefaultScriptPluginFactory中进行相应的任务生成,我们来看看
DefaultScriptPluginFactory
final ScriptTarget initialPassScriptTarget = initialPassTarget(target);
ScriptCompiler compiler = scriptCompilerFactory.createCompiler(scriptSource);
// 第一个阶段Pass 1, extract plugin requests and plugin repositories and execute buildscript {}, ignoring (i.e. not even compiling) anything else
CompileOperation initialOperation = compileOperationFactory.getPluginsBlockCompileOperation(initialPassScriptTarget);
Class scriptType = initialPassScriptTarget.getScriptClass();
ScriptRunner initialRunner = compiler.compile(scriptType, initialOperation, baseScope, Actions.doNothing());
initialRunner.run(target, services);
PluginRequests initialPluginRequests = getInitialPluginRequests(initialRunner);
PluginRequests mergedPluginRequests = autoAppliedPluginHandler.mergeWithAutoAppliedPlugins(initialPluginRequests, target);
PluginManagerInternal pluginManager = topLevelScript ? initialPassScriptTarget.getPluginManager() : null;
pluginRequestApplicator.applyPlugins(mergedPluginRequests, scriptHandler, pluginManager, targetScope);
// 第二个阶段Pass 2, compile everything except buildscript {}, pluginManagement{}, and plugin requests, then run
final ScriptTarget scriptTarget = secondPassTarget(target);
scriptType = scriptTarget.getScriptClass();
CompileOperation operation = compileOperationFactory.getScriptCompileOperation(scriptSource, scriptTarget);
final ScriptRunner runner = compiler.compile(scriptType, operation, targetScope, ClosureCreationInterceptingVerifier.INSTANCE);
if (scriptTarget.getSupportsMethodInheritance() && runner.getHasMethods()) {
scriptTarget.attachScript(runner.getScript());
}
if (!runner.getRunDoesSomething()) {
return;
}
Runnable buildScriptRunner = () -> runner.run(target, services);
boolean hasImperativeStatements = runner.getData().getHasImperativeStatements();
scriptTarget.addConfiguration(buildScriptRunner, !hasImperativeStatements);
}
可以看到,源码中特别注释了,编译时的两个阶段,我们可以看到,所有的script(指函数调用),都是分别经过了阶段1和阶段2之后才真正生效的。
那么为什么android作用域在apply plugin的方式不行,plugins方式却可以呢?其实就是两个运行阶段不一致的问题。groovy可以在运行时动态找到android 这个函数,即使两者都在阶段2运行,因为groovy语法本身的特性,即使android这个函数没有定义我们也可以引用,也是在运行时阶段报错。而kotlin不一样,kotlin需要在编译的时候需要找到我们要引用的函数,即android,所以同一个阶段即plugin都没有生效(需要执行完阶段才生效),我们当然也找不到android函数,那为什么plugins又可以呢?其实很容易想到,因为plugins是在第一阶段中执行并生效的,而android引用在第二个阶段,我们接着看源码
重点关注一下compileOperationFactory.getPluginsBlockCompileOperation方法,这个方法的实现类是DefaultCompileOperationFactory,在这里我们可以看到里面定义了两个阶段
public class DefaultCompileOperationFactory implements CompileOperationFactory {
private static final StringInterner INTERNER = new StringInterner();
private static final String CLASSPATH_COMPILE_STAGE = "CLASSPATH";
private static final String BODY_COMPILE_STAGE = "BODY";
private final BuildScriptDataSerializer buildScriptDataSerializer = new BuildScriptDataSerializer();
private final DocumentationRegistry documentationRegistry;
public DefaultCompileOperationFactory(DocumentationRegistry documentationRegistry) {
this.documentationRegistry = documentationRegistry;
}
public CompileOperation getPluginsBlockCompileOperation(ScriptTarget initialPassScriptTarget) {
InitialPassStatementTransformer initialPassStatementTransformer = new InitialPassStatementTransformer(initialPassScriptTarget, documentationRegistry);
SubsetScriptTransformer initialTransformer = new SubsetScriptTransformer(initialPassStatementTransformer);
String id = INTERNER.intern("cp_" + initialPassScriptTarget.getId());
return new NoDataCompileOperation(id, CLASSPATH_COMPILE_STAGE, initialTransformer);
}
public CompileOperation getScriptCompileOperation(ScriptSource scriptSource, ScriptTarget scriptTarget) {
BuildScriptTransformer buildScriptTransformer = new BuildScriptTransformer(scriptSource, scriptTarget);
String operationId = scriptTarget.getId();
return new FactoryBackedCompileOperation<>(operationId, BODY_COMPILE_STAGE, buildScriptTransformer, buildScriptTransformer, buildScriptDataSerializer);
}
}
getPluginsBlockCompileOperation中创建了一个InitialPassStatementTransformer类对象,我们关注transform方法的内容,即如果找到了plugins,我们就进行接下来的transform操作transformPluginsBlock,这就验证了,plugins的确在第一个阶段即classpath阶段运行
@Override
public Statement transform(SourceUnit sourceUnit, Statement statement) {
...
if (scriptBlock.getName().equals(PLUGINS)) {
return transformPluginsBlock(scriptBlock, sourceUnit, statement);
}
...
总结
文章列出来了几个关键的迁移了,相信大部分的问题都可以解决了,的确在迁移到kotlin之后,还是存在一定的迁移成本的,大部分就只能生啃官网介绍,希望看完都有收获吧!
链接:https://juejin.cn/post/7116333902435893261
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
使用 Flutter 轻松搞定短视频上滑翻页效果
前言
我们在短视频应用中经常会看到不停上滑浏览下一条视频的沉浸式交互效果,这种交互能够让用户不停地翻页,直到找到喜欢的视频内容,从而营造一种不断“搜寻目标”的感觉,让用户欲罢不能。这种交互形式在 Flutter 中可以轻松使用 PageView
组件实现。
PageView 组件介绍
PageView
组件专门设计用来实现翻页效果,类定义如下:
PageView({
Key? key,
this.scrollDirection = Axis.horizontal,
this.reverse = false,
PageController? controller,
this.physics,
this.pageSnapping = true,
this.onPageChanged,
List<Widget> children = const <Widget>[],
this.dragStartBehavior = DragStartBehavior.start,
this.allowImplicitScrolling = false,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
this.scrollBehavior,
this.padEnds = true,
})
其中常用的属性说明如下:
scrollDirection
:滑动方向,可以支持纵向翻页或横向翻页,默认是横向翻页。controller
:翻页控制器,可以通过控制器来制定初始页,以及跳转到具体的页面。onPageChanged
:翻页后的回调函数,会告知翻页后的页码。reverse
:是否反向翻页,默认是false
。如果横向滑动翻页的话,如果开启反向翻页,则是从右到左翻页。如果是纵向翻页的话,就是从顶部到底部翻页。children
:在翻页中的组件列表,每一页都以自定义组件内容,因此这个组件也可以用于做引导页,或是类似滑动查看详情的效果。
使用示例
PageView
使用起来非常简单,我们先定义一个PageView
翻页的内容组件,简单地将接收的图片文件满屏显示。代码如下,实际应用的时候可以根据需要换成其他自定义组件。
class ImagePageView extends StatelessWidget {
final String imageName;
const ImagePageView({Key? key, required this.imageName}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: Image.asset(
imageName,
fit: BoxFit.fitHeight,
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
),
);
}
}
之后是定义一个 PageViewDemo
来应用 PageView
翻页应用示例,代码如下:
class PageViewDemo extends StatefulWidget {
const PageViewDemo({Key? key}) : super(key: key);
@override
State<PageViewDemo> createState() => _PageViewDemoState();
}
class _PageViewDemoState extends State<PageViewDemo> {
late PageController _pageController;
int _pageIndex = 1;
@override
void initState() {
_pageController = PageController(
initialPage: _pageIndex,
viewportFraction: 1.0,
);
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: PageView(
scrollDirection: Axis.vertical,
onPageChanged: (index) {
_pageIndex = index;
},
controller: _pageController,
allowImplicitScrolling: false,
padEnds: true,
reverse: false,
children: const [
ImagePageView(imageName: 'images/earth.jpeg'),
ImagePageView(imageName: 'images/island-coder.png'),
ImagePageView(imageName: 'images/mb.jpeg'),
],
),
);
}
}
这个示例里,我们的 pageController
只是演示了设置初始页码。我们看到的 viewportFraction
可以理解为一页内容占据屏幕的比例,比如我们可以设置该数值为1/3,支持一个屏幕分段显示3个页面内容。
PageController 应用
PageController
可以控制滑动到指定位置,比如我们可以调用 animateToPage
方法实现一个快速滑动到顶部的悬浮按钮。
floatingActionButton: FloatingActionButton(
onPressed: () {
_pageController.animateToPage(
0,
duration: const Duration(
milliseconds: 1000,
),
curve: Curves.easeOut,
);
},
backgroundColor: Colors.black.withAlpha(180),
child: const Icon(
Icons.arrow_upward,
color: Colors.white,
),
),
实现效果如下。
PageController
还有如下控制翻页的方法:
jumpToPage
:跳转到指定页面,但是没有动画。注意这里不会校验页码是否会超出范围。nextPage
:滑动到下一页,实际上调用的是animateToPage
方法。previousPage
:滑动到上一页,实际上调用的是animateToPage
方法。
总结
本篇介绍了 Flutter 的翻页组件 PageView
的使用,通过 PageView
可以轻松实现类似短视频的纵向上滑翻页的效果,也可以实现横向翻页效果(如阅读类软件)。在接下来的系列文章中,本专栏将会介绍更多 Flutter 实用的组件。
链接:https://juejin.cn/post/7157342437248073742
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
❤️Android 快别用Toast了,来试试Snackbar❤️
🔥 应用场景
Toast提示默认显示在界面底部,使用Toast.setGravity()将提示显示在中间,如下:
Toast toast = Toast.makeText(this, str, Toast.LENGTH_SHORT);
toast.setGravity(Gravity.CENTER, 0, 0);
toast.show();
运行在在Android 12上无法显示,查看Logcat提示如下:
Toast: setGravity() shouldn't be called on text toasts, the values won't be used
意思就是:你不能使用toast调用setGravity,调用无效。哎呀,看给牛气的,咱看看源码找找原因
🔥 源码
💥 Toast.setGravity()
/**
* 设置Toast出现在屏幕上的位置。
*
* 警告:从 Android R 开始,对于面向 API 级别 R 或更高级别的应用程序,此方法在文本 toast 上调用时无效。
*/
public void setGravity(int gravity, int xOffset, int yOffset) {
if (isSystemRenderedTextToast()) {
Log.e(TAG, "setGravity() shouldn't be called on text toasts, the values won't be used");
}
mTN.mGravity = gravity;
mTN.mX = xOffset;
mTN.mY = yOffset;
}
妥了,人家就告诉你了 版本>=Android R(30),调用该方法无效。无效就无效呗,还不给显示了,过分。
Logcat的提示居然是在这里提示的,来都来了,咱们看看isSystemRenderedTextToast()方法。
💥 Toast.isSystemRenderedTextToast()
/**
*Text Toast 将由 SystemUI 呈现,而不是在应用程序内呈现,因此应用程序无法绕过后台自定义 Toast 限制。
*/
@ChangeId
@EnabledAfter(targetSdkVersion = Build.VERSION_CODES.Q)
private static final long CHANGE_TEXT_TOASTS_IN_THE_SYSTEM = 147798919L;
private boolean isSystemRenderedTextToast() {
return Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM) && mNextView == null;
}
重点了。Text Toast 将由 SystemUI 呈现,而不是在应用程序内呈现。
清晰明了,可以这样玩,但是你级别不够,不给你玩。
事情整明白了,再想想解决解决方案。他说了Text Toast 将由 SystemUI 呈现,那我不用 Text 不就行了。
🔥 Toast 提供的方法
先看看Tast提供的方法:
有这几个方法。咱们实践一下。保险起见看看源码
💥 Toast.setView() 源码
/**
* 设置显示的View
* @deprecated 自定义 Toast 视图已弃用。 应用程序可以使用 makeText 方法创建标准文本 toast,
* 或使用 Snackbar
*/
@Deprecated
public void setView(View view) {
mNextView = view;
}
这个更狠,直接弃用。
要么老老实实的用默认的Toast。
要么使用 Snackbar。
🔥 Snackbar
Snackbar 就是一个类似Toast的快速弹出消息提示的控件(我是刚知道,哈哈)。
与Toast相比:
一次只能显示一个
与用户交互
- 在右侧设置按钮来添加事件,根据 Material Design 的设计原则,只显示 1 个按钮 (添加多个,以最后的为准)
提供Snackbar显示和关闭的监听事件
- BaseTransientBottomBar.addCallback(BaseCallback)
💥 代码实现
showMessage(findViewById(android.R.id.content), str, Snackbar.LENGTH_INDEFINITE);
public static void showMessage(View view, String str, int length) {
Snackbar snackbar = Snackbar.make(view, str, length);
View snackbarView = snackbar.getView();
//设置布局居中
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(snackbarView.getLayoutParams().width, snackbarView.getLayoutParams().height);
params.gravity = Gravity.CENTER;
snackbarView.setLayoutParams(params);
//文字居中
TextView message = (TextView) snackbarView.findViewById(R.id.snackbar_text);
//View.setTextAlignment需要SDK>=17
message.setTextAlignment(View.TEXT_ALIGNMENT_GRAVITY);
message.setGravity(Gravity.CENTER);
message.setMaxLines(1);
snackbar.addCallback(new BaseTransientBottomBar.BaseCallback<Snackbar>() {
@Override
public void onDismissed(Snackbar transientBottomBar, int event) {
super.onDismissed(transientBottomBar, event);
//Snackbar关闭
}
@Override
public void onShown(Snackbar transientBottomBar) {
super.onShown(transientBottomBar);
//Snackbar显示
}
});
snackbar.setAction("取消", new View.OnClickListener() {
@Override
public void onClick(View v) {
//显示一个默认的Snackbar。
Snackbar.make(view, "我先走", BaseTransientBottomBar.LENGTH_LONG).show();
}
});
snackbar.show();
}
Snackbar.make的三个参数:
- View:从View中找出当前窗口最外层视图,然后在其底部显示。
- 第二个参数(text):
- CharSequence
- StringRes
- duration(显示时长)
- Snackbar.LENGTH_INDEFINITE 从 show()开始显示,直到它被关闭或显示另一个 Snackbar。
- Snackbar.LENGTH_SHORT 短时间
- Snackbar.LENGTH_LONG 长时间
- 自定义持续时间 以毫秒为单位
💥 效果
Android 12
Android 5.1
💥 工具类
如果觉得设置麻烦可以看看下面这边文章,然后整合一套适合自己的。
链接:https://juejin.cn/post/7029216153469714445
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Vue.js 3 开源组件推荐:代码差异查看器插件
一个Vue.js差异查看器插件,可以用来比较两个代码片断之间的差异。
Github地址:github.com/hoiheart/vu…
支持语言:
css
xml: xml, html, xhtml, rss, atom, xjb, xsd, xsl, plist, svg
markdown: markdown, md, mkdown, mkd
javascript: javascript, js, jsx
json
plaintext: plaintext, txt, text
typescript: typescript, ts
如何使用:
导入并注册diff查看器。
import VueDiff from 'vue-diff'
import 'vue-diff/dist/index.css'
app.use(VueDiff);
2.向模板中添加组件。
<Diff />
3.可用的组件props。
mode: {
type: String as PropType<Mode>,
default: 'split' // or unified
},
theme: {
type: String as PropType<Theme>,
default: 'dark' // or light
},
language: {
type: String,
default: 'plaintext'
},
prev: {
type: String,
default: ''
},
current: {
type: String,
default: ''
},
inputDelay: {
type: Number,
default: 0
},
virtualScroll: {
type: [Boolean, Object] as PropType<boolean|VirtualScroll>,
default: false
}
4.使用 highlight.js 扩展插件。
// 注册一门新语言
import yaml from 'highlight.js/lib/languages/yaml'
VueDiff.hljs.registerLanguage('yaml', yaml)
作者:杭州程序员张张
来源:juejin.cn/post/7156839676677423112
随机裁员?Meta用算法随机裁掉60名“劳务派遣”员工
Facebook 母公司 Meta 最近使用算法“随机”解雇了 60 名来自埃森哲的劳务派遣人员。
此前 Meta 与埃森哲签订了近 5 亿美元的合同,由隶属于后者的劳务派遣人员到 Meta 位于奥斯汀的办公室工作,主要开展内容审核和商业诚信等业务。
Meta 通过视频电话会议告知被裁的 60 名员工,裁员将于 9 月 2 日正式生效,10 月 3 日结束工资发放。除了明确是“随机”选择之外,Meta 没有给出裁员的具体原因。
埃森哲没有立即向这些劳务派遣人员提供其他工作机会,但这些员工被告知可以在未来两周内重新申请新职位。
在今年 6 月 30 日公司举行的一次全体员工大会上,Meta 首席执行官马克・扎克伯格(Mark Zuckerberg)警告员工,最近的市场低迷“可能是我们近年来看到的最严峻的挑战之一”,因此需要通过“积极的业绩评估”来淘汰表现不佳的员工。从扎克伯格的话来看,这次裁员也许并不令人意外。
扎克伯格说:“实际上,公司里可能有很多人不该留在这里。”
扎克伯格补充道:“通过提高期望值,制定更有进取心的目标,并稍微加大压力,我想这可能会让你们中的一些人觉得这个地方不适合自己。我觉得这种自我选择没问题。”
在举行这次全员大会之时,Meta 已经采取了冻结招聘和其他削减成本措施,主要是因为公司股票今年以来已经下跌过半。
就在 Meta 裁减劳务派遣人员几天前,苹果解雇了 100 名负责招聘新员工的人事专员。苹果此前确实警告称,公司将控制支出并放缓招聘。
去年 8 月份,游戏行业支付处理公司 Xsolla 也使用算法裁掉了 150 名员工,所以让机器人解雇员工可能是未来的一种趋势。
来源:IT之家
收起阅读 »1024程序员节,别人家的公司真香!羡慕ing~
今天是传说中属于程序猿的节日,各大互联网公司已经开整,小编已经在朋友圈里感受到了不同氛围的节日氛围,为大家整合了以下几类:
一、掏心窝子型
有哪个程序员能对漂亮小姐姐说不?!天天在办公室撸代码的码农而言,在黑白的代码间,小姐姐就是天使一样的存在~
没看错,是真人女仆出现了。
我见过好事成双, 却没想过能站在女团中央~
还有献舞的小姐姐,一起蹦虾咔啦咔
同时还有男人的终极梦想,你相信光吗?
二、驱魔保命型
程序员的梦想是什么 No Code No Bug,此符居家旅行,建议常备。
虽说是防bug,可这猫仔何意?防BUG灵兽?
三、紧张兮兮型
不是所有的符都有用,比如这块氛围感糖饼的出现,让舒缓神经再次紧绷起来,瞬间觉得手里的符咒不香了。
如果有比这个还让人紧张的,那就是抠破了~
这个拔河游戏,看得D哥虎躯一震,往前一步是孤独,退后一步是幸福
四、扎得不行型
开开心心过节不行吗?这波操作,扎疼了码农的心。
比如:这个平平无奇的小黑盒竟读懂了我的内心,不过这个应该送给老板吧
泪崩,你以为我不想有个对象吗?
谁能拒绝一个奔三的秃头小宝贝?爱护头顶,从防脱开始,所以接下来是防脱专场:
单瓶装:防脱就防脱,旁边的青春永驻,是何意?
礼盒装:防脱产品都是成双成对,你呢?
套装:我宣布,今年这篇头顶被我承包了
嗯嗯,终于明白,霸王才是真真的程序员之友。
五、“特殊”服务型
肩颈不适是程序员们的通病,一顿贴心的按摩服务,也能让程序员朋友短暂放松,看这架势,专业~
不过,有些公司的定制化服务,简直服务到工位,反手就是一个赞~
其实,舒不舒服不重要,就是想体验差别化服务。
六、斗智斗勇型
不少公司开启游园会项目,打卡所有项目,就能兑换礼品,游戏项目包括但不限于:
穿越火线(这游戏搁夜市必火)
赌场风云(赌啥,KPI吗?)
数字coding(呵呵,怕这个就枉为程序猿)
也可窥见,很多人事绞尽脑汁,只为大家欢愉一刻,这个必须加鸡腿儿。
七、吃饱喝足型
不少公司准备了精致下午茶,慰藉代码兄弟们,昨天已经被朋友圈投喂饱了,独乐乐不如众乐乐,上图(菜):
精致可口的甜品,琳琅满目的零食,啧啧啧···
零食就算了,大闸蟹就过分了!
八、彰显身份型
一些公司虽然准备的是日常用品,但是····我们一定要透过现象看本质,体味公司的一番深意,比如:
公司送衬衣,称(衬)心如意(衣)。好兆头,这么正式的衣服,恨不能现在就穿上,感受节日氛围。
公司送双肩包,寓意:双减(双肩)别想了,但保(包)你有饭吃。
公司送键盘,沉吟片刻,我悟到了:见(键)一个,盘一个,淦!
礼物或大或小,心意或深或浅,1024,希望大家都能1G棒~
欢迎评论区留言,说出你的程序员礼物~
注:文章素材来源于网络。如侵,请联系删除
过几年你不看,就不用胡椒盐
法规及法规vbnmbnm,bn鼓风机发个人fghjghffg不会难看美女吧
fghjgf8ytuj复工后的非官方的个
和对方过后就VB你吧VNfghjghffg好看吗帮你们
非过户结果符合复工后很过分
法国的红酒地方各个很舒服fghjghffg搞好看皇冠
小客户更健康分工会经费
发个机会规范fghjghffg发个机会功夫就能发个和
Flutter 小技巧之优化你的代码性能
又到了小技巧系列更新时间,今天我们分享一个比较轻松的内容:Flutter 里的代码优化,优化的目的主要是为了提高性能和可维护性,放心,本篇我们不讲深入的源码分析,就是分享最最最基础的布局代码优化。
我们先从一个简单的例子开始,相信大家对于 Flutter 的 UI 构建不会陌生,那么如下代码所示,日常开发过程中 A
和 B
这两种代码组织方式,你更常用的是哪一种?
A (函数方式) | B (Component Class 方式) |
---|---|
如果是从代码运行之后的 UI 效果来看,这两个方式运行之后的布局效果并不会有什么差异,而通常因为可以写更少代码和参数调用更方便等原因,我们可能在编写页面的内部控件时,会更经常使用 A (函数方式)
这种写法,也有称之为 Helper Method 的叫法。
那使用函数方式构建 UI 有没有问题?答案肯定是没问题,但是某些场景下,对比使用 B (Component Class 方式)
,可能性能表现上相对没那么优秀。
举个例子,如下代码所示,在 renderA
函数里我们通过点击按键修改 count
,在修改之后触发 UI 渲染时就需要用到 setState
,也就是我们每点一下,当前整个页面就是触发一次 rebuild ,但是我们只是想要改变当前 renderA
里的 count
文本而已。
这就是使用函数构建内部控件最常见的问题之一,因为子控件更新时是通过父容器的 setState
,所以每次子控件比如 renderA
发生变化时,就会触发整个 Widget 都出现 rebuild ,这其实并不是特别符合我们的预期。
科普一个众所周知的知识点,
setState
其实就是调用StatefulWidget
对应的StatefulElement
里的markNeedsBuild
方法,也就是对Element
(BuildContext
) 里的_dirty
标识为设置为true
,仅此而已, 然后等待下次渲染更新。
当然,你说像 renderA
这种写法会引起很严重的性能问题吗?事实上并不会,因为众所周知 Flutter 里的 UI 构建是通过多个不同的树来完成的,而 Widget 并不是真实的控件,所以一般情况下 renderA
这种写法导致的 rebuild 是不会产生严重的性能缺陷。
但是,如果同级下你的 renderB
是如下所示这样的情况呢?虽然这段代码毫无意义,但是我们在 renderA
点击改变 count
的时候,其实并没有改变 renderB
的用到的 status
参数,但是因为 renderA
里调用了 setState
,导致 renderB
每次都会进行重复进行浮点计算。
当然你可以说我写个变量进行缓存提前判断也可以解决,但这并不是这个例子的关键,那如果把上面这个例子变成 Component Class 的方式会有什么好处:
- A 在点击更新
count
时不会影响其他控件 - B 控件通过
didUpdateWidget
可以用更优雅的方式决定更新条件
这样看起来是不是更合理一些?另外 Component Class 的实现方式,也能在一定层度解决代码层级嵌套的问题,有时候实现一些 Component Class 的模版也可以成为 Flutter 里提高效率的工具,这个后面我们会聊到。
当然使用 Component Class 在无形之中会需要你写更多的代码,同时控件之间的状态联动成本也会有所提高,例如你需要在 B 控件关联 A 的 count
变化去改变高度,这时候可能就需要加入 InheritedWidget
或者 ValueNotifier
等方式来实现。
例如 Flutter 里 DefaultTabController
配合 TabBar
和 TabBarView
的实现就是一个很好的参考。
Widget build(BuildContext context) {
return DefaultTabController(
length: myTabs.length,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
tabs: myTabs,
),
),
body: TabBarView(
children: myTabs.map((Tab tab) {
final String label = tab.text.toLowerCase();
return Center(
child: Text(
'This is the $label tab',
style: const TextStyle(fontSize: 36),
),
);
}).toList(),
),
),
);
}
所以到这里我们理解一个小技巧:在不偷懒的情况下,使用 Component Class 的方式实现子控件会比使用函数方式可能得到更好的性能和代码结构。
当然,使用 Component Class 实现的方式,在调试时也会比函数方式更方便,如下图所示,当使用函数方式布局时,你在 Flutter Inspector 里看到的 Widget Tree 和 Details Tree 是完全铺平的情况,也没办法定制调试参数。
但是当你 Component Class 组织布局的时候,你就可以通过 override debugFillProperties
方法来可视化一些参数状态,例如 ItemA
里可以把 count 添加到 debugFillProperties
里,这样在 Details Tree 里也可以直观看到目前的 count
状态信息。
所以这里又有一个小技巧:通过
override debugFillProperties
,可以定制一些 Debug 时的可视化参数来帮助我们更好调试布局。
既然讲到利用 Component Class 组织布局,那就不得不聊一个典型的控件:AnimatedBuilder
。
AnimatedBuilder
可以是最常说到的一个性能优化的例子, 一般情况下在页面的子控件里使用动画,特别是循环动画的话,我们都会建议使用前面介绍的 Component Class 方式,不然动画导致当前页面不停 rebuild 肯定会导致性能影响。
但是有时候我就不想用 Component Class 该怎么办?我就是想写在当前 Page 里,那就可以使用 AnimatedBuilder
,你只要把需要执行动画的部分放到 builder
方法里就好了。
因为 AnimatedBuilder
的内部会有一个 _AnimatedState
用于独立触发 setState
,从而执行外部 builder 方法执行动画效果。
类似 AnimatedBuilder
的模版实现,可以在一定程度上解决使用 Component Class 的痛点,当然,在使用 AnimatedBuilder
还是有一些需要注意, 比如 child 如果不需要跟随动画进行其他变化,一般是要放到 AnimatedBuilder
的 child
配置里,因为如果直接放在 builder
方法里,那就会出现 child 也跟随动画重新 rebuild 的情况,但是如果是放到 child
配置项里,那就是调用了 child
的对象缓存。
不正确使用 | 正确使用 |
---|---|
如果对于这个缓存概念不理解,可以参考 《MediaQuery 和 build 优化你不知道的秘密》 里的“缓存区域不随帧变化,以便得到最小化的构建”。
当然类似 AnimatedBuilder
的构建方式还要注意 context
问题,不要拿错 context
,这也是很多时候会犯的潜在错误,特别是在调用 of(context)
的时候。
那有的人可能到这里会觉得,那你之前一直说 Widget 很轻,Widget 不是真正的控件,那 rebuild 多几次有什么问题?
一般情况下确实不会有太大问题,但是当你的控件有 Opacity
、ColorFilter
、 ShaderMash
或者 ClipRect
(Clip.antiAliasWithSaveLayer
)时,就可能会有较大的性能影响,因为他们都是可能会触发 saveLayer
的操作。
为什么
saveLayer
对性能影响很大?因为需要在 GPU 绘制是需要增加额外的缓冲区域,粗俗点说就是需要做图层的保存和合成,这就会对 GPU 渲染时产生较大影响的耗时。
而这里面最常遇到的应该就是 Opacity
带来的性能问题,因为它看起来是那么的轻便,但是从官方的介绍里,除非真的有必要,不然可以使用效果类似的实现去做场景替代,例如:
你需要对图片做透明度相关的动画是,那么使用 AnimatedOpacity
或 FadeInImage
代替 Opacity
会对性能更有帮助。
AnimatedOpacity
和Opacity
不一样吗?某种程度上还真不大一样,Opacity
的内部是pushOpacity
的操作,而AnimatedOpacity
里虽然有OpacityLayer
,但是变动时是updateCompositedLayer
;而FadeInImage
会使用 GPU 的 fragment shader 去处理透明度的问题,所以性能也会更好一些。
或者在类似有颜色透明度的场景时,可以通过 Color.fromRGBO
来替代 Opacity
,除非你需要将不透明度应用到一大组较为复杂的 child 里,你才会需要使用 Opacity
。
/// no
Opacity(opacity: 0.5, child: Container(color: Colors.red))
/// yes
Container(color: Color.fromRGBO(255, 0, 0, 0.5))
另外还有 IntrinsicHeight
/ IntrinsicWidth
的场景,因为它们是可以通过 child 的内部宽高来调整 child 的大小,但是这个推算布局的过程会比较费时,可能会到 O(N²),虽然 Flutter 里针对这部分计算结果做了缓存,但是不妨碍它的耗时。
这么说可能有点抽象,举一个官方介绍过的例子,如下代码所示,当你在 ListView
里对 Row
的 children
进行 Align
排列时,你可能会发现它没有效果,因为此时通过 Border
可以看到,绿色和蓝色方框的父容器大小一致。
但是在加上 IntrinsicHeight
之后, 因为通过 IntrinsicHeight
的测算之后再返回 size,Row
里的三个 Item 现在高度一致,,这时候 Align
就可以生效了,但是正如前面所说,这个操作性对性能来说相对昂贵,虽然系统有缓存参数,但是如果出现动画 rebuild ,也会对性能造成影响。
对这部分感兴趣的可以看 : 《带你了解不一样的 Flutter》
到这里我们就理解了 (函数方式) 和 (Component Class 方式)组织布局的不同之处,同时也知道了 Component Class 方式可以帮助我们更好地调试布局代码,也举例了一些 UI 布局里常见的耗时场景。
那本篇的小技巧到这里就结束了,如果你还有什么感兴趣或者有疑惑的,欢迎留言评论~
链接:https://juejin.cn/post/7156792097754841095
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
聊一聊Kotlin之data class
Kotlin是由JetBrains开发的针对JVM、Android和浏览器的静态编程语言,是Android的官方语言。Kotlin拥有较多高级而又简洁的语法特性,提升了我们的开发效率,减少了代码量。
在使用 java 的时候,我们在用class定义一个entity时除了写get、set方法(使用Kotlin后省了这部分工作),经常还会重写类的 equals
、hashCode
和toString
方法,这些方法往往都是模板化的。在 kotlin 中提供了data class
搞定这些模版化代码。
data class与class的区别:
实现方式
- class类
class ClassUser(val name: String, var age: Int)
- data class类
data class DataClassUser(val name: String, var age: Int)
自动重写toString
方法
- data类的
toString
方法会打印出属性的值 - 非data类的
toString
方法则打印出内存地址val classUser = ClassUser("classuser", 18)
val dataClassUser = DataClassUser("dataclassuser", 20)
println("ClassUser -> ${classUser.toString()}")
// ClassUser -> com.imock.vicjava.keyuse.ClassUser@11026067
println("DataClassUser -> ${dataClassUser.toString()}")
// DataClassUser -> DataClassUser(name=dataclassuser, age=20)
新增componentN
方法
- data类新增属性的
componentN
方法,component1
代表第一个属性,component2
代表第二个属性。(常用于解构声明)val dataClassUser = DataClassUser("dataclassuser", 20)
println("DataClassUser component1() -> ${dataClassUser.component1()}")
// DataClassUser component1() -> dataclassuser
println("DataClassUser component2() -> ${dataClassUser.component2()}")
// DataClassUser component2() -> 20
新增copy
方法
- data类新增
copy
方法,可以用来修改部分属性,但是保持其他不变。val dataClassUser = DataClassUser("dataclassuser", 20)
println("ClassUser toString() -> ${classUser.toString()}")
// DataClassUser -> DataClassUser(name=dataclassuser, age=20)
val newDataClassUser = dataClassUser.copy(age = 22)
println("DataClassUser copy -> ${newDataClassUser.toString()}")
// DataClassUser copy -> DataClassUser(name=dataclassuser, age=22)
重写hashCode
和 equals
方法
- data类重写
hashCode
方法,equals
方法可以稍后看下源码,先判断两个是否是同一个对象,如果不是则进行类型判断,是相同类型则逐个比较属性的值。val classUserLisa1 = ClassUser("lisa", 20)
val classUserLisa2 = ClassUser("lisa", 20)
println("ClassUser equals -> ${classUserLisa1.equals(classUserLisa2)}")
// ClassUser equals -> false
println("classUserLisa1 hashCode -> ${classUserLisa1.hashCode()}")
// classUserLisa1 hashCode -> 2081652693
println("classUserLisa2 hashCode -> ${classUserLisa2.hashCode()}")
// classUserLisa2 hashCode -> 406765571
val dataClassUserLisa1 = DataClassUser("lisa", 20)
val dataClassUserLisa2 = DataClassUser("lisa", 20)
println("DataClassUser equals -> ${dataClassUserLisa1.equals(dataClassUserLisa2)}")
// DataClassUser equals -> true
println("dataClassUserLisa1 hashCode -> ${dataClassUserLisa1.hashCode()}")
// dataClassUserLisa1 hashCode -> 102981865
println("dataClassUserLisa2 hashCode -> ${dataClassUserLisa2.hashCode()}")
// dataClassUserLisa2 hashCode -> 102981865
data class为何如此神奇
data class DataClassUser(val name: String, var age: Int)
class ClassUser(var name: String, var age: Int)
单独看实现上两者没有太大的区别,一个使用data class,一个使用class,为何data class却多出那么多能力?得益于Kotlin高级的语法特性。我们都知道kotlin最终还是要编译成 java class 在 JVM 上运行的,为了更好的理解Kotlin高级而又简洁的语法特性,有时我们需要看看用kotlin写完的代码编译后是什么样子。Talk is cheap, show me the code.
class类编译后的java代码
Kotlin写法如下:
class ClassUser(var name: String, var age: Int)
查看编译后的java代码如下,可以看到帮我们自动生成了get、set和构造方法:
public final class ClassUser {
@NotNull
private final String name;
private int age;
@NotNull
public final String getName() {
return this.name;
}
public final int getAge() {
return this.age;
}
public final void setAge(int var1) {
this.age = var1;
}
public ClassUser(@NotNull String name, int age) {
Intrinsics.checkNotNullParameter(name, "name");
super();
this.name = name;
this.age = age;
}
}
data class类编译后的java代码
Kotlin写法如下:
data class DataClassUser(val name: String, var age: Int)
查看其编译后的java代码如下,会发现比class类编译后的代码多了部分方法,新增了components
和copy
方法,重写了equals
、hashCode
和toString
方法。
public final class DataClassUser {
@NotNull
private final String name;
private int age;
@NotNull
public final String getName() {
return this.name;
}
public final int getAge() {
return this.age;
}
public final void setAge(int var1) {
this.age = var1;
}
public DataClassUser(@NotNull String name, int age) {
Intrinsics.checkNotNullParameter(name, "name");
super();
this.name = name;
this.age = age;
}
// 新增方法
@NotNull
public final String component1() {
return this.name;
}
// 新增方法
public final int component2() {
return this.age;
}
// 新增方法
@NotNull
public final DataClassUser copy(@NotNull String name, int age) {
Intrinsics.checkNotNullParameter(name, "name");
return new DataClassUser(name, age);
}
// 新增方法
// $FF: synthetic method
public static DataClassUser copy$default(DataClassUser var0, String var1, int var2, int var3, Object var4) {
if ((var3 & 1) != 0) {
var1 = var0.name;
}
if ((var3 & 2) != 0) {
var2 = var0.age;
}
return var0.copy(var1, var2);
}
// 重写该方法
@NotNull
public String toString() {
return "DataClassUser(name=" + this.name + ", age=" + this.age + ")";
}
// 重写该方法
public int hashCode() {
String var10000 = this.name;
return (var10000 != null ? var10000.hashCode() : 0) * 31 + Integer.hashCode(this.age);
}
// 重写该方法
public boolean equals(@Nullable Object var1) {
if (this != var1) {
if (var1 instanceof DataClassUser) {
DataClassUser var2 = (DataClassUser)var1;
if (Intrinsics.areEqual(this.name, var2.name) && this.age == var2.age) {
return true;
}
}
return false;
} else {
return true;
}
}
}
总结
知道data class
干了啥
- 重写
toString
方法。 - 新增
componentN
方法(component1()、component2()、...、componentN()
),其对应属性的声明顺序(常用于解构声明)。 - 新增
copy
方法,可以用来修改部分属性,但是保持其他不变。
特别提下copy方法,可能有些同学疑问很少见到这个方法使用场景,慢慢地等你用上了MVI框架就知道State必须使用 Kotlin data class,copy方法的应用自然少不了。 - 重写
equals
和hasCode
方法,equals()
方法不再单一比较对象引用,而是先判断两个是否是同一个对象,如果不是则进行类型判断,是相同类型则逐个比较属性的值。
使用data class
需要注意啥
- 主构造函数必须要至少有一个参数。
- 主构造函数中的所有参数必须被标记为val或者var。
- 数据类不能有以下修饰符:abstract、inner、open、sealed。
链接:https://juejin.cn/post/7156233100199100447
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
一看就懂!图解 Kotlin SharedFlow 缓存系统
前言
Kotlin 为我们提供了两种创建“热流”的工具:StateFlow 和 SharedFlow。StateFlow 经常被用来替代 LiveData 充当架构组件使用,所以大家相对熟悉。其实 StateFlow 只是 SharedFlow 的一种特化形式,SharedFlow 的功能更强大、使用场景更多,这得益于其自带的缓存系统,本文用图解的方式,带大家更形象地理解 SharedFlow 的缓存系统。
创建 SharedFlow 需要使用到 MutableSharedFlow()
方法,我们通过方法的三个参数配置缓存:
fun <T> MutableSharedFlow(
replay: Int = 0,
extraBufferCapacity: Int = 0,
onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
): MutableSharedFlow<T>
接下来,我们通过时序图的形式介绍这三个关键参数对缓存的影响。正文之前让我们先统一一下用语:
- Emitter:Flow 数据的生产者,从上游发射数据
- Subcriber:Flow 数据的消费者,在下游接收数据
replay
当 Subscriber 订阅 SharedFlow 时,有机会接收到之前已发送过的数据,replay 指定了可以收到 subscribe 之前数据的数量。replay 不能为负数,默认值为 0 表示 Subscriber 只能接收到 subscribe 之后 emit 的数据:
上图展示的是 replay = 0 的情况,Subscriber 无法收到 subscribe 之前 emit 的 ❶,只能接收到 ❷ 和 ❸。
当 replay = n ( n > 0)时,SharedFlow 会启用缓存,此时 BufferSize 为 n,意味着可以缓存发射过的最近 n 个数据,并发送给新增的 Subscriber。
上图以 n = 1 为例 :
- Emitter 发送 ❶ ,并被 Buffer 缓存
- Subscriber 订阅 SharedFlow 后,接收到缓存的 ❶
- Emitter 相继发送 ❷ ❸ ,Buffer 缓存的数据相继依次被更新
在生产者消费者模型中,有时消费的速度赶不及生产,此时要加以控制,要么停止生产,要么丢弃数据。SharedFlow 也同样如此。有时 Subscriber 的处理速度较慢,Buffer 缓存的数据得不到及时处理,当 Buffer 为空时,emit 默认将会被挂起 ( onBufferOverflow = SUSPEND)
上面的图展示了 replay = 1 时 emit 发生 suspend 场景:
- Emitter 发送 ❶ 并被缓存
- Subscriber 订阅 SharedFlow ,接收 replay 的 ❶ 开始处理
- Emitter 发送 ❷ ,缓存数据更新为 ❷ ,由于 Subscriber 对 ❶ 的处理尚未结束,❷ 在缓存中没有及时被消费
- Emitter 发送 ❸,由于缓存的 ❷ 尚未被 Subscriber 消费,emit 发生挂起
- Subscriber 开始消费 ❷ ,Buffer 缓存 ❸ , Emitter 可以继续 emit 新数据
注意 SharedFlow 作为一个多播可以有多个 Subscriber,所以上面例子中,❷ 被消费的时间点,取决于最后一个开始处理的 Subscriber。
extraBufferCapacity
extraBufferCapacity 中的 extra 表示 replay-cache 之外为 Buffer 还可以额外追加的缓存。
若 replay = n, extraBufferCapacity = m,则 BufferSize = m + n。
extraBufferCapacity 默认为 0,设置 extraBufferCapacity 有助于提升 Emitter 的吞吐量
在上图的基础之上,我们再设置 extraBufferCapacity = 1,效果如下图:
上图中 BufferSize = 1 + 1 = 2 :
- Emitter 发送 ❶ 并得到 Subscriber1 的处理 ,❶ 作为 replay 的一个数据被缓存,
- Emitter 发送 ❷,Buffer 中 replay-cache 的数据更新为 ❷
- Emitter 发送 ❸,Buffer 在存储了 replay 数据 ❷ 之上,作为 extra 又存储了 ❸
- Emitter 发送 ❹,此时 Buffer 已没有空余位置,emit 挂起
- Subscriber2 订阅 SharedFlow。虽然此时 Buffer 中存有 ❷ ❸ 两个数据,但是由于 replay = 1,所以 Subscriber2 只能收到最近的一个数据 ❸
- Subscriber1 处理完 ❶ 后,依次处理 Buffer 中的下一个数据,开始消费 ❷
- 对于 SharedFlow 来说,已经不存在没有消费 ❷ 的 Subscriber,❷ 移除缓存,❹ 的 emit 继续,并进入缓存,此时 Buffer 又有两个数据 ❸ ❹ ,
- Subscriber1 处理完 ❷ ,开始消费 ❸
- 不存在没有消费 ❸ 的 Subscriber, ❸ 移除缓存。
onBufferOverflow
前面的例子中,当 Buffer 被填满时,emit 会被挂起,这都是建立在 onBufferOverflow 为 SUSPEND 的前提下的。onBufferOverflow 用来指定缓存移除时的策略,除了默认的 SUSPEND,还有两个数据丢弃策略:
- DROP_LATEST:丢弃最新的数据
- DROP_OLDEST:丢弃最老的数据
需要特别注意的是,当 BufferSize = 0 时,extraBufferCapacity 只支持 SUSPEND,其他丢弃策略是无效的。这很好理解,因为 Buffer 中没有数据,所以丢弃无从下手,所以启动丢弃策略的前提是 Buffer 至少有一个缓冲区,且数据被填满
上图展示 DROP_LATEST 的效果。假设 replay = 2,extra = 0
- Emitter 发送 ❸ 时,由于 ❶ 已经被消费,所以 Buffer 数据从 ❶❷ 变为 ❷❸
- Emitter 发送 ❹ 时,由于 ❷ 还未被消费,Buffer 处于填满状态, ❹ 直接被丢弃
- Emitter 发送 ❺ 时,由于 ❷ 已经被费,可以移除缓存,Buffer 数据变为 ❸❺
上图展示了 DROP_OLDEST 的效果,与 DROP_LATEST 比较后非常明显,缓存中永远会储存最新的两个数据,但是较老的数据不管有没有被消费,都可能会从 Buffer 移除,所以 Subscriber 可以消费当前最新的数据,但是有可能漏掉中间的数据,比如图中漏掉了 ❷
注意:当 extraBufferCapacity 设为 SUSPEND 可以保证 Subscriber 一个不漏的消费掉所有数据,但是会影响 Emitter 的速度;当设置为 DROP_XXX 时,可以保证 emit 调用后立即返回,但是 Subscriber 可能会漏掉部分数据。
如果我们不想让 emit 发生挂起,除了设置 DROP_XXX 之外,还有一个方法就是调用 tryEmit
,这是一个非 suspend 版本的 emit
abstract suspend override fun emit(value: T)
abstract fun tryEmit(value: T): Boolean
tryEmit 返回一个 boolean 值,你可以这样判断返回值,当使用 emit 会挂起时,使用 tryEmit 会返回 false,其余情况都是 true。这意味着 tryEmit 返回 false 的前提是 extraBufferCapacity 必须设为 SUSPEND,且 Buffer 中空余位置为 0 。此时使用 tryEmit 的效果等同于 DROP_LATEST。
SharedFlow Buffer
前面介绍的 MutableSharedFlow 的三个参数,其本质都是围绕 SharedFlow 的 Buffer 进行工作的。那么这个 Buffer 具体结构是怎样的呢?
上面这个图是 SharedFlow 源码中关于 Buffer 的注释,这个图形象地告诉了我们 Buffer 是一个线性数据结构(就是一个普通的数组 Array<Any?>
),但是这个图不能直观反应 Buffer 运行机制。下面通过一个例子,看一下 Buffer 在运行时的具体更新过程:
val sharedFlow = MutableSharedFlow<Int>(
replay = 2,
extraBufferCapacity = 2,
onBufferOverflow = BufferOverflow.SUSPEND
)
var emitValue = 1
fun main() {
runBlocking {
launch {
sharedFlow.onEach {
delay(200) // simulate the consume of data
}.collect()
}
repeat(12) {
sharedFlow.emit(emitValue)
emitValue++
delay(50)
}
}
}
上面的代码很简单,SharedFlow 的 BufferSize = 2+2 = 4,Emitter 生产的速度大于 Subscriber 消费的速度,所以过程中会出现 Buffer 的填充和更新,下面依旧用图的方式展示 Buffer 的变化
先看一下代码对应的时序图:
有前面的介绍,相信这个时序图很容易理解,这里就不再赘述了,下面重点图解一下 Buffer 的内存变化。SharedFlow 的 Buffer 本质上是一个基于 Array 实现的 queue,通过指针移动从往队列增删元素,避免了元素在实际数组中的移动。这里关键的指针有三个:
- head:队列的 head 指向 Buffer 的第一个有效数据,这是时间上最早进入缓存的数据,在数据被所有的 Subscriber 消费之前不会移除缓存。因此 head 也代表了最慢的 Subscriber 的处理进度
- replay:Buffer 为 replay-cache 预留空间的其实位置,当有新的 Subscriber 订阅发生时,从此位置开始处理数据。
- end:新数据进入缓存时的位置,end 这也代表了最快的 Subscriber 的处理进度。
如果 bufferSize 表示当前 Buffer 中存储数据的个数,则我们可知三指针 index 符合如下关系:
- replay <= head + bufferSize
- end = head + bufferSize
了解了三指针的含义后,我们再来看上图中的 Buffer 是如何工作的:
最后,总结一下 Buffer 的特点:
- 基于数组实现,当数组空间不够时进行 2n 的扩容
- 元素进入数组后的位置保持不变,通过移动指针,决定数据的消费起点
- 指针移动到数组尾部后,会重新指向头部,数组空间可循环使用
链接:https://juejin.cn/post/7156408785886511111
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Android完美处理输入框被挡问题
前言
前段时间出现了webview的输入框被软键盘挡住的问题,处理之后顺便对一些列的输入框被挡住的情况进行一个总结。
正常情况下的输入框被挡
正常情况下,输入框被输入法挡住,一般给window设softInputMode就能解决。
window.getAttributes().softInputMode = WindowManager.LayoutParams.XXX
有3种情况:
(1)SOFT_INPUT_ADJUST_RESIZE: 布局会被软键盘顶上去
(2)SOFT_INPUT_ADJUST_PAN:只会把输入框给顶上去(就是只顶一部分距离)
(3)SOFT_INPUT_ADJUST_NOTHING:不做任何操作(就是不顶)
SOFT_INPUT_ADJUST_PAN和SOFT_INPUT_ADJUST_RESIZE的不同在于SOFT_INPUT_ADJUST_PAN只是把输入框,而SOFT_INPUT_ADJUST_RESIZE会把整个布局顶上去,这就会有种布局高度在输入框展示和隐藏时高度动态变化的视觉效果。
如果你是出现了输入框被挡的情况,一般设置SOFT_INPUT_ADJUST_PAN就能解决。如果你是输入框没被挡,但是软键盘弹出的时候会把布局往上顶,如果你不希望往上顶,可以设置SOFT_INPUT_ADJUST_NOTHING。
softInputMode是window的属性,你给在Mainifest给Activity设置,也是设给window,你如果是Dialog或者popupwindow这种,就直接getWindow()来设置就行。正常情况下设置这个属性就能解决问题。
Webview的输入框被挡
但是Webview的输入框被挡的情况下,设这个属性有可能会失效。
Webview的情况下,SOFT_INPUT_ADJUST_PAN会没效果,然后,如果是Webview并且你还开沉浸模式的情况的话,SOFT_INPUT_ADJUST_RESIZE和SOFT_INPUT_ADJUST_PAN都会不起作用。
我去查看资料,发现这就是经典的issue 5497, 网上很多的解决方案就是通过AndroidBug5497Workaround,这个方案很容易能查到,我就不贴出来了,原理就是监听View树的变化,然后再计算高度,再去动态设置。这个方案的确能解决问题,但是我觉得这个操作不可控的因素比较多,说白了就是会不会某种机型或者情况下使用会出现其它的BUG,导致你需要写一些判断逻辑来处理特殊的情况。
解法就是不用沉浸模式然后使用SOFT_INPUT_ADJUST_RESIZE就能解决。但是有时候这个window显示的时候就需要沉浸模式,特别是一些适配刘海屏、水滴屏这些场景。
setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
那我的第一反应就是改变布局
window. setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
这样是能正常把弹框顶上去,但是控件内部用的也是WRAP_CONTENT导致SOFT_INPUT_ADJUST_RESIZE改变布局之后就恢复不了原样,也就是会变形。而不用WRAP_CONTENT用固定高度的话,SOFT_INPUT_ADJUST_RESIZE也是失效的。
没事,还要办法,在MATCH_PARENT的情况下我们去设置fitSystemWindows为true,但是这个属性会让出一个顶部的安全距离,效果就是向下偏移了一个状态栏的高度。
这种情况下你可以去设置margin来解决这个顶部偏移的问题。
params.topMargin = statusHeight == 0 ? -120 : -statusHeight;
view.setLayoutParams(params);
这样的操作是能解除顶部偏移的问题,但是布局有可能被纵向压缩,这个我没完全测试过,我觉得如果你布局高度是固定的,可能不会受到影响,但我的webview是自适应的,webview里面的内容也是自适应的,所以我这出现了布局纵向压缩的情况。举个例子,你的view的高度是800,状态栏高度是100,那设fitSystemWindows之后的效果就是view显示700,paddingTop 100,这样的效果,设置params.topMargin =-100,之后,view显示700,paddingTop 100。大概是这个意思:能从视觉上消除顶部偏移,但是布局纵向被压缩的问题没得到处理
所以最终的解决方法是改WindowInsets的Rect (这个我等下会再解释是什么意思)
具体的操作就是在你的自定义view中加入下面两个方法
@Override
public void setFitsSystemWindows(boolean fitSystemWindows) {
fitSystemWindows = true;
super.setFitsSystemWindows(fitSystemWindows);
}
@Override
protected boolean fitSystemWindows(Rect insets) {
Log.v("mmp", "测试顶部偏移量: "+insets.top);
insets.top = 0;
return super.fitSystemWindows(insets);
}
小结
解决WebView+沉浸模式下输入框被软键盘挡住的步骤:
- window.getAttributes().softInputMode设置成SOFT_INPUT_ADJUST_RESIZE
- 设置view的fitSystemWindows为true,我这里是webview里面的输入框被挡住,设的就是webview而不是父View
- 重写fitSystemWindows方法,把insets的top设为0
WindowInsets
根据上面的3步操作,你就能处理webview输入框被挡的问题,但是如果你想知道为什么,这是什么原理。你就需要去了解WindowInsets。我们的沉浸模式的操作setSystemUiVisibility和设置fitSystemWindows属性,还有重写fitSystemWindows方法,都和WindowInsets有关。
WindowInsets是应用于窗口的系统视图的插入。例如状态栏STATUS_BAR和导航栏NAVIGATION_BAR。它会被view引用,所以我们要做具体的操作,是对view进行操作。
还有一个比较重要的问题,WindowInsets的不同版本都是有一定的差别,Android28、Android29、Android30都有一定的差别,例如29中有个android.graphics.Insets类,这是28里面没有的,我们可以在29中拿到它然后查看top、left等4个属性,但是只能查看,它是final的,不能直接拿出来修改。
但是WindowInsets这块其实能讲的内容比较多,以后可以拿出来单独做一篇文章,这里就简单介绍下,你只需要指定我们解决上面那些问题的原理,就是这个东西。
源码解析
大概对WindowInsets有个了解之后,我再带大家简单过一遍setFitsSystemWindows的源码,相信大家会印象更深。
public void setFitsSystemWindows(boolean fitSystemWindows) {
setFlags(fitSystemWindows ? FITS_SYSTEM_WINDOWS : 0, FITS_SYSTEM_WINDOWS);
}
它这里只是设置一个flag而已,如果你看它的注释(我这里就不帖出来了),他会把你引导到protected boolean fitSystemWindows(Rect insets)这个方法(我之后会说为什么会到这个方法)
@Deprecated
protected boolean fitSystemWindows(Rect insets) {
if ((mPrivateFlags3 & PFLAG3_APPLYING_INSETS) == 0) {
if (insets == null) {
// Null insets by definition have already been consumed.
// This call cannot apply insets since there are none to apply,
// so return false.
return false;
}
// If we're not in the process of dispatching the newer apply insets call,
// that means we're not in the compatibility path. Dispatch into the newer
// apply insets path and take things from there.
try {
mPrivateFlags3 |= PFLAG3_FITTING_SYSTEM_WINDOWS;
return dispatchApplyWindowInsets(new WindowInsets(insets)).isConsumed();
} finally {
mPrivateFlags3 &= ~PFLAG3_FITTING_SYSTEM_WINDOWS;
}
} else {
// We're being called from the newer apply insets path.
// Perform the standard fallback behavior.
return fitSystemWindowsInt(insets);
}
}
(mPrivateFlags3 & PFLAG3_APPLYING_INSETS) == 0 这个判断后面会简单讲,你只需要知道正常情况是执行fitSystemWindowsInt(insets)
而fitSystemWindows又是哪里调用的?往前跳,能看到是onApplyWindowInsets调用的,而onApplyWindowInsets又是由dispatchApplyWindowInsets调用的。其实到这里已经没必要往前找了,能看出这个就是个分发机制,没错,这里就是WindowInsets的分发机制,和View的事件分发机制类似,再往前找就是viewgroup调用的。前面说了WindowInsets在这里不会详细说,所以WindowInsets分发机制这里也不会去展开,你只需要先知道有那么一回事就行。
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) {
try {
mPrivateFlags3 |= PFLAG3_APPLYING_INSETS;
if (mListenerInfo != null && mListenerInfo.mOnApplyWindowInsetsListener != null) {
return mListenerInfo.mOnApplyWindowInsetsListener.onApplyWindowInsets(this, insets);
} else {
return onApplyWindowInsets(insets);
}
} finally {
mPrivateFlags3 &= ~PFLAG3_APPLYING_INSETS;
}
}
假设mPrivateFlags3是0,PFLAG3_APPLYING_INSETS是20,0和20做或运算,就是20。然后判断是否有mOnApplyWindowInsetsListener,这个Listener就是我们有没有在外面做
setOnApplyWindowInsetsListener(new OnApplyWindowInsetsListener() {
@Override
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
......
return insets;
}
});
假设没有,调用onApplyWindowInsets
public WindowInsets onApplyWindowInsets(WindowInsets insets) {
if ((mPrivateFlags3 & PFLAG3_FITTING_SYSTEM_WINDOWS) == 0) {
// We weren't called from within a direct call to fitSystemWindows,
// call into it as a fallback in case we're in a class that overrides it
// and has logic to perform.
if (fitSystemWindows(insets.getSystemWindowInsetsAsRect())) {
return insets.consumeSystemWindowInsets();
}
} else {
// We were called from within a direct call to fitSystemWindows.
if (fitSystemWindowsInt(insets.getSystemWindowInsetsAsRect())) {
return insets.consumeSystemWindowInsets();
}
}
return insets;
}
mPrivateFlags3 & PFLAG3_FITTING_SYSTEM_WINDOWS就是20和40做与运算,那就是0,所以调用fitSystemWindows。
而fitSystemWindows的(mPrivateFlags3 & PFLAG3_APPLYING_INSETS) == 0)就是20和20做与运算,不为0,所以调用fitSystemWindowsInt。
分析到这里,就需要结合我们上面解决BUG的思路了,我们其实是要拿到Rect insets这个参数,并且修改它的top。
setOnApplyWindowInsetsListener(new OnApplyWindowInsetsListener() {
@Override
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
......
return insets;
}
});
setOnApplyWindowInsetsListener回调中的insets可以拿到android.graphics.Insets这个类,但是你只能看到top是多少,没办法修改。当然你可以看到top是多少,然后按我上面的做法Margin设置一下
params.topMargin = -top;
如果你的布局不发生纵向变形,那倒没有多大关系,如果有变形,那就不能用这个做法。从源码看,这个过程主要涉及3个方法。我们能看出最好下手的地方就是fitSystemWindows。因为onApplyWindowInsets和dispatchApplyWindowInsets是分发机制的方法,你要在这里下手的话可能会出现流程混乱等问题。
所以我们这样做来解决fitSystemWindows = true出现的顶部偏移。
@Override
public void setFitsSystemWindows(boolean fitSystemWindows) {
fitSystemWindows = true;
super.setFitsSystemWindows(fitSystemWindows);
}
@Override
protected boolean fitSystemWindows(Rect insets) {
Log.v("mmp", "测试顶部偏移量: "+insets.top);
insets.top = 0;
return super.fitSystemWindows(insets);
}
扩展
上面已经解决问题了,这里是为了扩展一下思路。
fitSystemWindows方法是protected,导致你能重写它,但是如果这个过程我们没办法用继承来实现呢?
其实这就是一个解决问题的思路,我们要知道为什么会出现这种情况,原理是什么,比如这里我们知道这个fitSystemWindows导致的顶部偏移是insets的top导致的。你得先知道这一点,不然你不知道怎么去解决这个问题,你只能去网上找别人的方法一个一个试。那我怎么知道是insets的top导致的呢?这就需要有一定的源码阅读能力,还要知道这个东西设计的思想是怎样的。当你知道有这么一个东西之后,再想办法去拿到它然后改变数据。
这里我我们是利用继承protected方法这个特性去获取到insets,那如果这个过程没办法通过继承实现怎么办?比如这里是因为fitSystemWindows是view的方法,而我们自定义view正好继承view。如果它是内部自己写的一个类去实现这个操作呢?
这种情况下一般两种操作比较万金油:
- 你写一个类去继承它那个类,然后在你写的类里面去改insets,然后通过反射的方式把它注入给View
- 动态代理
我其实一开始改这个的想法就是用动态代理,所以马上把代码撸出来。
public class WebViewProxy implements InvocationHandler {
private Object relObj;
public Object newProxyInstance(Object object){
this.relObj = object;
return Proxy.newProxyInstance(relObj.getClass().getClassLoader(), relObj.getClass().getInterfaces(), this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if ("fitSystemWindows".equals(method.getName()) && args != null && args.length == 1){
Log.v("mmp", "测试代理效果 "+args);
}
}catch (Exception e){
e.printStackTrace();
}
return proxy;
}
}
WebViewProxy proxy = new WebViewProxy();
View viewproxy = (View) proxy.newProxyInstance(mWebView);
然后才发现fitSystemWindows不是接口方法,白忙活一场,但是如果fitSystemWindows是接口方法的话,我这里就可以用通过动态代理加反射的操作去修改这个insets,虽然用不上,但也是个思路。最后发现可以直接重写这个方法就行,我反倒还把问题想复杂了。
链接:https://juejin.cn/post/7154378005035352100
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Android 搜索框架使用
App中搜索功能是必不可少的,搜索功能可以帮助用户快速获取想要的信息。对此,Android提供了一个搜索框架,本文介绍如何通过搜索框架实现搜索功能。
搜索框架简介
Android 搜索框架提供了搜索弹窗和搜索控件两种使用方式。
搜索弹窗:系统控制的弹窗,激活后显示在页面顶部,输入的内容提交后会通过
Intent
传递到指定的搜索Activity
中处理,可以添加搜索建议。
搜索控件(
SearchView
):系统实现的搜索控件,可以放在任意位置(可以与Toolbar
结合使用),默认情况下与EditText
类似,需要自己添加监听处理用户输入的数据,通过配置可以达到与搜索弹窗一致的行为。
使用搜索框架实现搜索功能
可搜索配置
在res/xml目录下创建searchable.xml
(必须用此命名),如下:
<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
android:hint="@string/search_hint"
android:label="@string/app_name" />
android:label
是此配置文件必须配置的属性,通常配置为App的名字,android:hint
配置用户未输入内容时的提示文案,官方建议格式为“搜索${content or product}”
。
更多可搜索配置包含的语法和用法可以看官方文档。
搜索页面
配置一个单独的Activity
用于显示搜索内容,用户可能会在搜索完一个内容后立刻搜索下一个内容,所以建议把搜索页面设置为SingleTop
,避免重复创建搜索页面。
在AndroidManifest
中配置搜索页面,如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
...
>
<activity
android:name=".search.SearchActivity"
android:exported="false"
android:launchMode="singleTop">
<meta-data
android:name="android.app.searchable"
android:resource="@xml/searchable" />
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
</intent-filter>
</activity>
</application>
</manifest>
在Activity
中处理搜索数据,代码如下:
class SearchActivity : AppCompatActivity() {
private lateinit var binding: LayoutSearchActivityBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.layout_search_activity)
// 当搜索页面第一次打开时,获取搜索内容
getQueryKey(intent)
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
// 更新Intent数据
setIntent(intent)
// 当搜索页面多次打开,并仍在栈顶时,获取搜索内容
getQueryKey(intent)
}
private fun getQueryKey(intent: Intent?) {
intent?.run {
if (Intent.ACTION_SEARCH == action) {
// 用户输入的内容
val queryKey = getStringExtra(SearchManager.QUERY) ?: ""
if (queryKey.isNotEmpty()) {
doSearch(queryKey)
}
}
}
}
private fun doSearch(queryKey: String) {
// 根据用户输入内容执行搜索操作
}
}
使用SearchView
SearchView
可以放在页面的任意位置,本文与Toolbar
结合使用,如何在Toolbar
中创建菜单项在上一篇文章中介绍过,此处省略。要使SearchView
与搜索弹窗保持一致的行为需要在代码中进行配置,如下:
class SearchActivity : AppCompatActivity() {
private lateinit var binding: LayoutSearchActivityBinding
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.example_seach_menu, menu)
menu?.run {
val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager
val searchView = findItem(R.id.action_search).actionView as SearchView
//设置搜索配置
searchView.setSearchableInfo(searchManager.getSearchableInfo(componentName))
}
return true
}
...
}
使用搜索弹窗
在Activity
中使用搜索弹窗,如果Activity
已经配置为搜索页面则无需额外配置,否则需要在AndroidManifest
中添加配置,如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
...
>
<activity
android:name=".search.SearchExampleActivity">
<!--为当前页面指定搜索页面-->
<!--如果所有页面都使用搜索弹窗,则将此meta-data移到applicaion标签下-->
<meta-data
android:name="android.app.default_searchable"
android:value=".search.SearchActivity" />
</activity>
</application>
</manifest>
在Activity
中通过onSearchRequested
方法来调用搜索弹窗,如下:
class SearchExampleActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: LayoutSearchExampleActivityBinding = DataBindingUtil.setContentView(this, R.layout.layout_search_example_activity)
binding.btnSearchDialog.setOnClickListener { onSearchRequested() }
}
}
搜索弹窗对Activity生命周期的影响
搜索弹窗的显示隐藏,不会像其他弹窗一样触发Activity
的onPause
、onResume
方法。如果在搜索弹窗显示隐藏的同时需要对其他功能进行处理,可以通过onSearchRequested
和OnDismissListener
来实现,代码如下:
class SearchExampleActivity : AppCompatActivity() {
override fun onSearchRequested(): Boolean {
// 搜索弹窗显示,可以在此处理其他功能
return super.onSearchRequested()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: LayoutSearchExampleActivityBinding = DataBindingUtil.setContentView(this, R.layout.layout_search_example_activity)
binding.btnSearchDialog.setOnClickListener { onSearchRequested() }
val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager
searchManager.setOnDismissListener {
// 搜索弹窗隐藏,可以在此处理其他功能
}
}
}
附加额外的参数
使用搜索弹窗时,如果需要附加额外的参数用于优化搜索查询的过程,例如用户的性别、年龄等,可以通过如下代码实现:
// 配置额外参数
class SearchExampleActivity : AppCompatActivity() {
override fun onSearchRequested(): Boolean {
val appData = Bundle()
appData.putString("gender", "male")
appData.putInt("age", 24)
startSearch(null, false, appData, false)
// 返回true表示已经发起了查询
return true
}
...
}
// 在搜素页面中获取额外参数
class SearchActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
intent?.run {
if (Intent.ACTION_SEARCH == action) {
// 用户输入的内容
val queryKey = getStringExtra(SearchManager.QUERY) ?: ""
// 额外参数
val appData = getBundleExtra(SearchManager.APP_DATA)
appData?.run {
val gender = getString("gender") ?: ""
val age = getInt("age")
}
}
}
}
}
语音搜索
语音搜索让用户无需输入内容就可进行搜索,要开启语音搜索,需要在searchable.xml
增加配置,如下:
<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
android:hint="@string/search_hint"
android:label="@string/app_name"
android:voiceSearchMode="showVoiceSearchButton|launchRecognizer" />
语音搜索必须配置showVoiceSearchButton
用于显示语音搜索按钮,配置launchRecognizer
指定语音搜索按钮启动一个语音识别程序用于识别语音转录为文本并发送至搜索页面。
更多语音搜索配置包含的语法和用法可以看官方文档。
注意,语音识别后的文本会直接发送至搜索页面,无法更改,需要进行完备的测试确保语音搜索功能适合你的App。
搜索记录
用户执行过搜索后,可以将搜索的内容保存下来,下次要搜索相同的内容时,输入部分文字后就会显示匹配的搜索记录。
要实现此功能,需要完成下列步骤:
创建SearchRecentSuggestionsProvider
自定义RecentSearchProvider继承SearchRecentSuggestionsProvider
,代码如下:
class RecentSearchProvider : SearchRecentSuggestionsProvider() {
companion object {
// 授权方的名称(建议设置为文件提供者的完整名称)
const val AUTHORITY = "com.chenyihong.exampledemo.search.RecentSearchProvider"
// 数据库模式
// 必须配置 DATABASE_MODE_QUERIES
// 可选配置 DATABASE_MODE_2LINES,为搜索记录提供第二行文本,可用于作为详情补充
const val MODE: Int = DATABASE_MODE_QUERIES or DATABASE_MODE_2LINES
}
init {
// 设置搜索授权方的名称与数据库模式
setupSuggestions(AUTHORITY, MODE)
}
}
在AndroidManifest
中配置Provider
,如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
...
>
<!--android:authorities的值与RecentSearchProvider中的AUTHORITY一致-->
<provider
android:name=".search.RecentSearchProvider"
android:authorities="com.chenyihong.exampledemo.search.RecentSearchProvider"
android:exported="false" />
</application>
</manifest>
修改可搜索配置
在searchable.xml
增加配置,如下:
<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
android:hint="@string/search_hint"
android:label="@string/app_name"
android:voiceSearchMode="showVoiceSearchButton|launchRecognizer"
android:searchSuggestAuthority="com.chenyihong.exampledemo.search.RecentSearchProvider"
android:searchSuggestSelection=" ?"/>
android:searchSuggestAuthority
的值与RecentSearchProvider中的AUTHORITY保持一致。android:searchSuggestSelection
的值必须为" ?"
,该值为数据库选择参数的占位符,自动由用户输入的内容替换。
在搜索页面中保存查询
获取到用户输入的数据时保存,代码如下:
class SearchActivity : BaseGestureDetectorActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
intent?.run {
if (Intent.ACTION_SEARCH == action) {
val queryKey = getStringExtra(SearchManager.QUERY) ?: ""
if (queryKey.isNotEmpty()) {
// 第一个参数为用户输入的内容
// 第二个参数为第二行文本,可为null,仅当RecentSearchProvider.MODE为DATABASE_MODE_QUERIES or DATABASE_MODE_2LINES时有效。
SearchRecentSuggestions(this@SearchActivity, RecentSearchProvider.AUTHORITY, RecentSearchProvider.MODE)
.saveRecentQuery(queryKey, "history $queryKey")
}
}
}
}
}
清除搜索历史
为了保护用户的隐私,官方的建议是App必须提供清除搜索记录的功能。请求搜索记录可以通过如下代码实现:
class SearchActivity : BaseGestureDetectorActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
SearchRecentSuggestions(this, RecentSearchProvider.AUTHORITY, RecentSearchProvider.MODE)
.clearHistory()
}
}
示例
整合之后做了个示例Demo,代码如下:
// 可搜索配置
<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
android:hint="@string/search_hint"
android:label="@string/app_name"
android:searchSuggestAuthority="com.chenyihong.exampledemo.search.RecentSearchProvider"
android:searchSuggestSelection=" ?"
android:voiceSearchMode="showVoiceSearchButton|launchRecognizer" />
// 清单文件
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
...
>
<activity
android:name=".search.SearchExampleActivity"
android:screenOrientation="portrait">
<!--为当前页面指定搜索页面-->
<meta-data
android:name="android.app.default_searchable"
android:value=".search.SearchActivity" />
</activity>
<activity
android:name=".search.SearchActivity"
android:exported="false"
android:launchMode="singleTop"
android:parentActivityName="com.chenyihong.exampledemo.search.SearchExampleActivity"
android:screenOrientation="portrait">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.chenyihong.exampledemo.search.SearchExampleActivity" />
<meta-data
android:name="android.app.searchable"
android:resource="@xml/searchable" />
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
</intent-filter>
</activity>
<provider
android:name=".search.RecentSearchProvider"
android:authorities="com.chenyihong.exampledemo.search.RecentSearchProvider"
android:exported="false" />
</application>
</manifest>
// 示例Activity
class SearchExampleActivity : BaseGestureDetectorActivity() {
override fun onSearchRequested(): Boolean {
val appData = Bundle()
appData.putString("gender", "male")
appData.putInt("age", 24)
startSearch(null, false, appData, false)
return true
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: LayoutSearchExampleActivityBinding = DataBindingUtil.setContentView(this, R.layout.layout_search_example_activity)
binding.btnSearchView.setOnClickListener { startActivity(Intent(this, SearchActivity::class.java)) }
binding.btnSearchDialog.setOnClickListener { onSearchRequested() }
val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager
searchManager.setOnDismissListener {
runOnUiThread { Toast.makeText(this, "Search Dialog dismiss", Toast.LENGTH_SHORT).show() }
}
}
}
class SearchActivity : BaseGestureDetectorActivity() {
private lateinit var binding: LayoutSearchActivityBinding
private val textDataAdapter = TextDataAdapter()
private val originData = ArrayList<String>()
private var lastQueryValue = ""
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.example_seach_menu, menu)
menu?.run {
val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager
val searchView = findItem(R.id.action_search).actionView as SearchView
searchView.setOnCloseListener {
textDataAdapter.setNewData(originData)
false
}
searchView.setSearchableInfo(searchManager.getSearchableInfo(componentName))
if (lastQueryValue.isNotEmpty()) {
searchView.setQuery(lastQueryValue, false)
}
}
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.action_clear_search_histor) {
SearchRecentSuggestions(this, RecentSearchProvider.AUTHORITY, RecentSearchProvider.MODE)
.clearHistory()
}
return super.onOptionsItemSelected(item)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.layout_search_activity)
setSupportActionBar(binding.toolbar)
supportActionBar?.run {
title = "SearchExample"
setHomeAsUpIndicator(R.drawable.icon_back)
setDisplayHomeAsUpEnabled(true)
}
binding.rvContent.adapter = textDataAdapter
originData.add("test data qwertyuiop")
originData.add("test data asdfghjkl")
originData.add("test data zxcvbnm")
originData.add("test data 123456789")
originData.add("test data /.,?-+")
textDataAdapter.setNewData(originData)
// 获取搜索内容
getQueryKey(intent, false)
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
// 更新Intent数据
setIntent(intent)
// 获取搜索内容
getQueryKey(intent, true)
}
private fun getQueryKey(intent: Intent?, newIntent: Boolean) {
intent?.run {
if (Intent.ACTION_SEARCH == action) {
val queryKey = getStringExtra(SearchManager.QUERY) ?: ""
if (queryKey.isNotEmpty()) {
SearchRecentSuggestions(this@SearchActivity, RecentSearchProvider.AUTHORITY, RecentSearchProvider.MODE)
.saveRecentQuery(queryKey, "history $queryKey")
if (!newIntent) {
lastQueryValue = queryKey
}
val appData = getBundleExtra(SearchManager.APP_DATA)
doSearch(queryKey, appData)
}
}
}
}
private fun doSearch(queryKey: String, appData: Bundle?) {
appData?.run {
val gender = getString("gender") ?: ""
val age = getInt("age")
}
val filterData = originData.filter { it.contains(queryKey) } as ArrayList<String>
textDataAdapter.setNewData(filterData)
}
}
效果如图:
链接:https://juejin.cn/post/7154560903537491975
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
死磕操作系统!谷歌重磅发布开源KataOS,网友:「谷歌坟场」喜+1
谷歌又发布新系统了!
等等,我为什么要说「又」?
出走半生,谷歌的操作系统之心始终不死。对于全新推出的KataOS,谷歌计划让它成为一个「可证明的安全平台」,并针对运行机器学习应用的嵌入式设备进行充分的优化。
有趣的是,文章发布之后,虽然陆续有了不少报道,但并没有激起什么水花。
没想到,就在这两天,竟然同时登上了知乎和Reddit的热榜。
不过,网友们的观点都出奇的一致——早晚得黄……
KataOS:用Rust写的「安全操作系统」
在博客中,谷歌解释了开发这个系统的理由。
当我们被越来越多收集和处理环境信息的智能设备所包围时,我们比任何时候都更需要一个简单的解决方案,来为嵌入式硬件构建可验证的安全系统。
如果我们的设备不能证明自己能保证数据的安全,那么它们收集的个人身份识别数据——如人的图像和声音的记录——就可能被恶意软件获取。
不幸的是,系统安全通常被视为添加到现有系统,或通过额外的ASIC硬件解决的软件功能——这远远不够。
针对这个问题,谷歌希望建立一个可证明的安全平台,为运行ML应用程序的嵌入式设备进行优化。
现在,谷歌已经在GitHub上开放了KataOS的几个组件,并且已经与Antmicro合作开发了Renode模拟器和相关框架。
这个新操作系统以seL4作为微内核。谷歌给出的理由是:「因为它把安全放在第一位;它在数学上被证明是安全的,具有保证保密性、完整性和可用性。」
为什么KataOS的安全性这么高呢?
谷歌解释说,因为从逻辑上讲,应用程序不可能破坏内核的硬件安全保护,并且系统组件是可验证安全的。
同时,KataOS也几乎完全由Rust实现,这更是加了一层buff,因为Rust消除了整类错误,比如逐一错误和缓冲区溢出。
目前的GitHub版本,已经涵盖了大部分KataOS的核心部分,包括用于Rust的框架(如sel4-sys crate,用于让seL4系统调用API),一个用Rust编写的备用根服务器(用于全系统的动态内存管理),以及对seL4的内核修改,用于回收根服务器使用的内存。
在内部,KataOS也能够动态地加载和运行CAmkES框架之外的第三方应用程序。
目前,Github上的代码不包括运行这些应用程序所需的组件,这些功能可能会在不久后发布。
同时,谷歌还为KataOS建立一个名为Sparrow的参考实现,它让KataOS与安全的硬件平台结合起来。
除了逻辑安全的操作系统内核外,Sparrow还包括一个在RISC-V架构上用OpenTitan构建的逻辑安全的信任根。对于最初的版本,谷歌的目标是建立一个用QEMU模拟运行的更标准的64位ARM平台。
谷歌希望在以后将Sparrow的全部内容开源,包括所有的硬件和软件设计。
而现在,谷歌发出号召,希望大家能共建「智能环境ML系统值得信赖的未来。」
KataOS的横空出世,又会掀起怎样的波澜?
国外网友:坐等被弃
对此,Reddit网友表示:Abandon是早晚的事儿!
另有扎心回复:「不懂就问,是已经宣布关闭日期了吗?」讽刺值瞬间拉满。
可以说,抛弃现有项目,转而支持那些还没成熟的半成品新项目,是谷歌20多年来的「传统艺能」了。
他们会支付数十亿美元,招揽全球的顶尖人才,花费数年打磨一个项目,制造出昂贵的东西,然后再丢掉。
于是,在外界看来,谷歌的方向完全可以用俩字来形容——「混乱」。
对于游戏领域,他们是三心二意,在大量的项目中手忙脚乱。前脚大举进军,后脚就狠心抛弃。
在硬件方面,前几年收购Fitibt之后,直到现在都没有把它很好地集成到Google Fit里。
对于谷歌一言不合就砍项目的操作,有网友调侃道:
「我们决定关掉『Google Existential」。我们仍然会坚信这个概念,但我们觉得它从未达到期望的高度。」
「那个服务是做什么的?」
「我们还没决定呢。」
至于这次推出的KataOS,知乎答主「星辰」表示:
知乎答主「亚东」也表示,谷歌做出来操作系统还能保它不挂,主要就是太有钱了。无数的古早系统,都死在了沙滩上。
取代安卓没下文,任职10年高管走人
说到谷歌的操作系统,除了大名鼎鼎的「Android」之外,还有一个相当神秘的「Fuchsia OS」。
而Fuchasia OS的命运,或许可以给KataOS做个参考。
要知道,曾经一度,Fuchasia OS可是被宣传为能取代Android的操作系统。
2016年8月,GitHub上的一组神秘源码,指向了谷歌正在开发的一个名为「Fuchsia OS」的全新操作系统。
代码显示,Fuchsia OS能够跨平台运行,包括「汽车的娱乐媒体系统和嵌入式设备,如手表、手机、平板以及电脑等等」。
2018年1月,谷歌允许开发者以Google Pixelbook为目标设备,下载Fuchsia OS进行开发与测试。
2019年6月,Fuchsia OS的开发者网站Fuchsia.dev上线。
2020年12月,首度在Google Open Source 博亮相,吁开发者来做贡献。
2021年5月,谷歌员工证实,Fuchsia OS首次实现了消费市场的部署。在对预览版设备进行第一波更新后,Fuchsia OS于2021年8月被推送至所有Nest Hub设备。
来源:雲爸
最初大家还在猜测,谷歌开发Fuchsia OS的目的是希望以单一平台统一移动操作系统生态系。
然而,谷歌至今都未曾说明Fuchsia OS的产品定位。
除了应用在了新款的Nest Hub上之外,并未像先前说明的那样,应用在手机、平板、电脑,甚至众多物联网设备上。
时间来到2022年3月,Fuchsia OS团队的负责人Chris McKillop,宣布自己已经离开任职10年的谷歌,加入到了微软Xbox团队。
不过比较起来,Fuchsia和KataOS还是有区别的。
KataOS/Sparrow似乎在一开始就明确了自己的计划——低功耗嵌入式设备。
从Github项目里可以看到,Sparrow最初的目标总内存为4MiB。
谷歌坟场:那些年被「杀死」的项目们
那么,为啥网友们清一色的表示谷歌早晚要「Abandon」呢?
看看那些被腰斩的项目就知道了。
据统计,这个数量至今已经达到了275个。2023年还没到,就已经预定了4个。
在这片触目惊心的「谷歌坟场」,你可以按年份搜索它「死」去的项目——2022年,23个;2021年,31个;2020年,25个……
项目地址:https://killedbygoogle.com/
这不,就在上个月,谷歌便官宣了云游戏服务平台Stadia正式下线的消息。
时间回到3年前,谷歌在推出Stadia时声称,只要一台普通电脑,装个Chrome,就能畅玩游戏大作。
然而,这几年以来,用户反馈并不好,甚至可以用糟糕来形容。
用户不买账,游戏阵容迟迟起不来,这业务又挺烧钱的,那就砍了吧。
不过,今年早些时候,谷歌还专门针对Stadia要黄的传言发推特澄清过:「Stadia没有关闭。请放心,我们一直在努力为平台和Stadia Pro带来更多优秀的游戏」。
结果过了两个月就官宣了Stadia下线的消息......
目前来看,反正谷歌财大气粗闲钱多,所以试一试KataOS和Rust也不是什么大事。
大不了,进展不顺利了再砍掉,就像以前无数被拍死在沙滩上的谷歌项目一样。
参考资料:
https://opensource.googleblog.com/2022/10/announcing-kataos-and-sparrow.html
https://www.reddit.com/r/programming/comments/y7noit/google_announces_a_new_os_written_in_rust/
https://www.zhihu.com/question/560937437
来源:好困 Aeneas | 新智元
收起阅读 »前端的焦虑,你想过30岁以后的前端路怎么走吗?
曾几何时,我总会很庆幸自己进了前端这个行业。因为在这个职业范畴里面,我如鱼得水,成长很快,成就感满满。然而,随着年龄和工龄的增长,渐渐发现自己的瓶颈越来越明显了,我感觉自己似乎碰到了前端的天花板。
原因何在
1.从客观原因来看,前端相对于后端的入门门槛确实低了不少。公司对前端的需求量虽然很旺盛,但是对前端的技术能力要求却不是很高,特别是一些小公司或者不是技术驱动的公司。这给人一种错觉,好像只需要懂一些js,会一般的html+css就能完成前端的工作。也由于这种原因,前端总是处于技术鄙视链的最底层。
2.从主观原因来说,前端平时基本都是和页面和看得到的UI打交道居多,对于后端的服务,数据存储,运维,部署等等懂得的不多,也导致了领导我们的往往都是后端。在大多数的情况下,你基本很难看到前端去统筹大局,统领前后端。
3.从个人原因来总结,前端经验上去了,工作年限上去了,但是职级却没有上去。归根结底,主要是因为自己的后端知识薄弱,前端深度不够。还有前端管理的职位僧多粥少导致的。
居于上述的原因,前端的天花板来得比别的技术栈更早。这也是导致我们焦虑的主要原因。既然有原因,那就可以找相应的解决方法。
解决方法
1.对症下药,哪里缺乏补哪里。前端的进阶,总离不开对后端的认知。我们不能把自己限死在前端这个范畴里面。业务驱动技术,而不是技术引导业务。不懂数据库,补数据库。不懂服务端,补服务端。幸好现在有nodeJs这个利器。 我们完全可以借用nodejs,去切入后端的世界,了解和学习后端的知识。做到不受语言的限制,学习应用,也就能突破自己的瓶颈。 除了node,php也是一个不错的选择。
2.主动创造条件。很多时候,选择比努力更重要。如果你发现你在一个地方再怎么努力也改变不了现状,这个时候你就应该出去别的地方看看,或者想想怎样改变现状。如果你无法升管理,那你可以尝试去别的地方当管理;如果你总是厌倦天天的无止境的切图和coding,但是又有很多想法,转岗去尝试当产品也是一个选择。
3.大前端和全栈是以后前端的一个趋势,懂后端的前端,懂各端的前端更加具有竞争力,以后可以往这个方向靠拢。
现在脑补一下前端知识体系的脑图。
注:脑图来自 ouvens/frontend-system-map
接下来再总结一下前端以后的路怎么走。
选择一:前端——高级前端——全栈——前端架构师(前端专家)
选择这条路的童鞋,最好就是技术迷,热爱前端,对技术有说不出的热情。喜欢专研,不管现在,还是将来,都乐于接受新事物新知识。
这条路的优点:一直都能呆在自己喜欢的领域,踏踏实实的敲代码,薪水也能不断提高。
这条路的缺点:30多岁还要各种敲代码,难免要被其他人管着,疲于各种公司的需求。
选择二:前端——高级前端——前端主管——前端经理
这条路,可能是大部分前端,都渴望走的路,都会理所当然的以为自己以后会走上的路。这个时候问题来了?哪里来这么多的前端主管和前端经理给你啊?
这条路的优点:一步一脚印,人生不断往上爬。成为高富帅,赢取白富美,登上事业的高峰。
这条路的缺点:就拿广州来说,不要说前端经理,就是前端主管这个职位,估计也没有多少公司是存在的。很多人上到前端经理也算到顶了。这里是想说明一点,路是有的,但是选择很少。万一有一天你要跳槽了,你真的不一定能找到下一间公司,又能当会前端主管的。 我所在的公司,当得上主管或者组长这个职位的人,真的两只手就可以数完。
ps:本人其实也想走这条路,但是我很唠叨的再强调一遍,30几岁之后,你未必能找到喜欢的公司的这个职位。僧多粥少啊。最后的结果会沦为,继续当码农。
选择三:前端——高级前端——转后台——高级后台——后台经理
这也是不少有实力的前端走的一条路。毕竟,在大多的公司,在大多的时候,都是后台统领着前台。说一句不好听的话,前端是一个习惯被领导的职位。 后台引导统筹项目的开发,估计大家都看得多了。前端统领后台,统筹项目开发你听过没有(除了张云龙)? 很少。至少我是没接触过的。
这条路的优点:华丽转岗,前后通杀,也能走出一辈子码农的死循环,当上经理,做管理层。
这条路的缺点:前端转后台,这明显不是一条好走的路,需要熬很多苦,学很多后台的东西,再慢慢成长起来。简单概括就是成本高,前期很辛苦。熬过了,上路了,就有机会走上更高的台阶;熬不过了,浪费了青春,继续当个二流的后台开发,继续码农。
选择四:前端——高级前端——转产品——产品经理——高级产品经理
这条路本人觉得也是一条不错的出路。在这个最好又最坏的年代,人人都是产品经理。在前端界打滚了这么多年,自然有不少产品的基础和思想。所以前端转产品,也是一条相对不会很吃力的路。
这条路的优点:有一定的基础,产品经理需求量大,以后的选择很多。
这条路的缺点:半路出家,前期也会很吃力地转型,转产品需要自身很有想法。懒于思考的人儿不适合。
选择五:前端——高级前端——其他行业,创业等等
这条路就是现在的我,总是憧憬着以后有一份不错的生意,然后有白富美,有车有楼,财务自由的一条路。
这条路的优点:未知性很大,不用再整天敲代码,可能还真的很赚钱。
这条路的缺点:正因为未知性太大,所以前途未卜。选择走这条路的童鞋,要早早地想好要干什么,干的事情需要具备什么技能,趁早学。
总结:学无止境,祝大家都能突破自己的瓶颈。可能还有其他的路,欢迎补充。 ps:以上所说带有强烈的个人主观意愿,可能有失客观事实,望体谅。
作者:Alone381
来源:juejin.cn/post/6844903615681806344
公司没钱了,工资发不出来,作为员工怎么办?
公司没钱了,工资发不出来,作为员工怎么办?
现在大环境不好,很多公司都会遇到一些困难。如果公司真的有现金流还好,如果没有,还等着客户回款。那么就有点难办了。很多公司会采取延期发工资,先发80%、50%工资这样的操作。
员工遇到这种情况,无非以下几种选择。
认同公司的决策,愿意跟公司共同进退。
不认同公司的决策,我要离职。
不认同公司的决策,但感觉自己反对也没用。所以嘴上先答应,事后会准备去找新的工作机会。
不认同公司的决策,我也不主动离职。准备跟公司battle,”你们这么做是不合法滴“
你可以代入这个场景看看自己是哪一类。首先由于每个人遇到的真实情况不一样,所以所有的选择只有适合自己的,并没有对错之分。
我自己的应对思路是,抛开存量,看增量。存量就是我在公司多少多少年了,公司开除我要给我N+1的补偿。公司之前对我特别特别好,老板对我有知遇之恩等等。你就当做自己已经不在公司了,现在公司给你发了offer,现在的你是否愿意接受公司开给你的条件?如果愿意,那么你可以选择方案一。如果不愿意,那么你就可以选择方案三。现在这环境,骑驴找马,否则离职后还要自己交社保。还不如先苟着。
为什么不选择方案四?因为不值得,如果公司属于违法操作,你先苟着,后面离职后还是可以找劳动局仲裁的。这样既不耽误你换工作,也不耽误你要赔偿。如果公司是正规操作,那么闹腾也没用,白浪费自己的时间。
离职赔偿还是比较清晰明确的,如果是散伙那可能会牵扯到更多利益。我自己的经验是,不能什么都想着要。当最优解挺难获得的时候,拿个次优解也可以。当然,不管你选择的哪个,我都有一个建议。那就是当一天和尚,敲一天钟。在职期间,还是要把事情干好的,用心并不全是为了公司,更多是为了自己。人生最大的投资是投资自己的工作和事业,浪费时间就是浪费生命。哪怕公司没有事情安排给你做,也要学会自己找事情做。
如果公司后面没钱了,欠的工资还拿得到吗?
我们作为员工是很难知道公司财务状况的,所以出了这样的事就直接去仲裁,最好是跟同事凑齐十个人去,据说会优先处理。公司如果还要做生意,一般会在仲裁前选择和解,大概是分几个月归还欠款。如果公司不管,那么仲裁后,会冻结公司公账。但有没有钱就看情况了。
如果公司账上没钱且股东已经实缴了股本金,那么公司是可以直接破产清算的。公司破产得话,基本上欠员工的钱就没有了。如果没有实缴,那么股东还需要按照股份比例偿还债务。
作者:石云升
来源:juejin.cn/post/7156242740034928671
Sendable 和 @Sendable 闭包代码实例详解
前言
Sendable 和 @Sendable 是 Swift 5.5 中的并发修改的一部分,解决了结构化的并发结构体和执行者消息之间传递的类型检查的挑战性问题。
使用 Sendable
应该在什么时候使用 Sendable?
Sendable协议和闭包表明那些传递的值的公共API是否线程安全的向编译器传递了值。当没有公共修改器、有内部锁定系统或修改器实现了与值类型一样的复制写入时,公共API可以安全地跨并发域使用。
标准库中的许多类型已经支持了Sendable协议,消除了对许多类型添加一致性的要求。由于标准库的支持,编译器可以为你的自定义类型创建隐式一致性。
例如,整型支持该协议:
extension Int: Sendable {}
一旦我们创建了一个具有 Int 类型的单一属性的值类型结构体,我们就隐式地得到了对 Sendable 协议的支持。
// 隐式地遵守了 Sendable 协议
struct Article {
var views: Int
}
与此同时,同样的 Article 内容的类,将不会有隐式遵守该协议:
// 不会隐式的遵守 Sendable 协议
class Article {
var views: Int
}
类不符合要求,因为它是一个引用类型,因此可以从其他并发域变异。换句话说,该类文章(Article)的传递不是线程安全的,所以编译器不能隐式地将其标记为遵守Sendable协议。
使用泛型和枚举时的隐式一致性
很好理解的是,如果泛型不符合Sendable协议,编译器就不会为泛型添加隐式的一致性。
// 因为 Value 没有遵守 Sendable 协议,所以 Container 也不会自动的隐式遵守该协议
struct Container<Value> {
var child: Value
}
然而,如果我们将协议要求添加到我们的泛型中,我们将得到隐式支持:
// Container 隐式地符合 Sendable,因为它的所有公共属性也是如此。
struct Container<Value: Sendable> {
var child: Value
}
对于有关联值的枚举也是如此:
如果枚举值们不符合 Sendable 协议,隐式的Sendable协议一致性就不会起作用。
你可以看到,我们自动从编译器中得到一个错误:
Associated value ‘loggedIn(name:)’ of ‘Sendable’-conforming enum ‘State’ has non-sendable type ‘(name: NSAttributedString)’
我们可以通过使用一个值类型String来解决这个错误,因为它已经符合Sendable。
enum State: Sendable {
case loggedOut
case loggedIn(name: String)
}
从线程安全的实例中抛出错误
同样的规则适用于想要符合Sendable的错误类型。
struct ArticleSavingError: Error {
var author: NonFinalAuthor
}
extension ArticleSavingError: Sendable { }
由于作者不是不变的(non-final),而且不是线程安全的(后面会详细介绍),我们会遇到以下错误:
Stored property ‘author’ of ‘Sendable’-conforming struct ‘ArticleSavingError’ has non-sendable type ‘NonFinalAuthor’
你可以通过确保ArticleSavingError的所有成员都符合Sendable协议来解决这个错误。
如何使用Sendable协议
隐式一致性消除了很多我们需要自己为Sendable协议添加一致性的情况。然而,在有些情况下,我们知道我们的类型是线程安全的,但是编译器并没有为我们添加隐式一致性。
常见的例子是被标记为不可变和内部具有锁定机制的类:
/// User 是不可改变的,因此是线程安全的,所以可以遵守 Sendable 协议
final class User: Sendable {
let name: String
init(name: String) { self.name = name }
}
你需要用@unchecked属性来标记可变类,以表明我们的类由于内部锁定机制所以是线程安全的:
extension DispatchQueue {
static let userMutatingLock = DispatchQueue(label: "person.lock.queue")
}
final class MutableUser: @unchecked Sendable {
private var name: String = ""
func updateName(_ name: String) {
DispatchQueue.userMutatingLock.sync {
self.name = name
}
}
}
遵守 Sendable的限制
Sendable协议的一致性必须发生在同一个源文件中,以确保编译器检查所有可见成员的线程安全。
例如,你可以在例如 Swift package这样的模块中定义以下类型:
public struct Article {
internal var title: String
}
Article 是公开的,而标题title是内部的,在模块外不可见。因此,编译器不能在源文件之外应用Sendable一致性,因为它对标题属性不可见,即使标题使用的是遵守Sendable协议的String类型。
同样的问题发生在我们想要使一个可变的非最终类遵守Sendable协议时:
可变的非最终类无法遵守 Sendable 协议
由于该类是非最终的,我们无法符合Sendable协议的要求,因为我们不确定其他类是否会继承User的非Sendable成员。因此,我们会遇到以下错误:
Non-final class ‘User’ cannot conform to Sendable; use @unchecked Sendable
正如你所看到的,编译器建议使用@unchecked Sendable。我们可以把这个属性添加到我们的User类中,并摆脱这个错误:
class User: @unchecked Sendable {
let name: String
init(name: String) { self.name = name }
}
然而,这确实要求我们无论何时从User继承,都要确保它是线程安全的。由于我们给自己和同事增加了额外的责任,我不鼓励使用这个属性,建议使用组合、最终类或值类型来实现我们的目的。
如何使用 @Sendabele
函数可以跨并发域传递,因此也需要可发送的一致性。然而,函数不能符合协议,所以Swift引入了@Sendable属性。你可以传递的函数的例子是全局函数声明、闭包和访问器,如getters和setters。
SE-302的部分动机是执行尽可能少的同步
我们希望这样一个系统中的绝大多数代码都是无同步的。
使用@Sendable属性,我们将告诉编译器,他不需要额外的同步,因为闭包中所有捕获的值都是线程安全的。一个典型的例子是在Actor isolation中使用闭包。
actor ArticlesList {
func filteredArticles(_ isIncluded: @Sendable (Article) -> Bool) async -> [Article] {
// ...
}
}
如果你用非 Sendabel 类型的闭包,我们会遇到一个错误:
let listOfArticles = ArticlesList()
var searchKeyword: NSAttributedString? = NSAttributedString(string: "keyword")
let filteredArticles = await listOfArticles.filteredArticles { article in
// Error: Reference to captured var 'searchKeyword' in concurrently-executing code
guard let searchKeyword = searchKeyword else { return false }
return article.title == searchKeyword.string
}
当然,我们可以通过使用一个普通的String来快速解决这种情况,但它展示了编译器如何帮助我们执行线程安全。
Swift 6: 代码启用并发性检查
Xcode 14 允许您通过 SWIFT_STRICT_CONCURRENCY 构建设置启用严格的并发性检查。
启用严格的并发性检查,以修复 Sendable 的符合性
这个构建设置控制编译器对Sendable和actor-isolation检查的执行水平:
Minimal : 编译器将只诊断明确标有Sendable一致性的实例,并等同于Swift 5.5和5.6的行为。不会有任何警告或错误。
Targeted: 强制执行Sendable约束,并对你所有采用async/await等并发的代码进行actor-isolation检查。编译器还将检查明确采用Sendable的实例。这种模式试图在与现有代码的兼容性和捕捉潜在的数据竞赛之间取得平衡。
Complete: 匹配预期的 Swift 6语义,以检查和消除数据竞赛。这种模式检查其他两种模式所做的一切,并对你项目中的所有代码进行这些检查。
严格的并发检查构建设置有助于 Swift 向数据竞赛安全迈进。与此构建设置相关的每一个触发的警告都可能表明你的代码中存在潜在的数据竞赛。因此,必须考虑启用严格并发检查来验证你的代码。
Enabling strict concurrency in Xcode 14
你会得到的警告数量取决于你在项目中使用并发的频率。对于Stock Analyzer,我有大约17个警告需要解决:
并发相关的警告,表明潜在的数据竞赛.
这些警告可能让人望而生畏,但利用本文的知识,你应该能够摆脱大部分警告,防止数据竞赛的发生。然而,有些警告是你无法控制的,因为是外部模块触发了它们。在我的例子中,我有一个与SWHighlight有关的警告,它不符合Sendable,而苹果在他们的SharedWithYou框架中定义了它。
在上述SharedWithYou框架的例子中,最好是等待库的所有者添加Sendable支持。在这种情况下,这就意味着要等待苹果公司为SWHighlight实例指明Sendable的一致性。对于这些库,你可以通过使用@preconcurrency属性来暂时禁用Sendable警告:
@preconcurrency import SharedWithYou
重要的是要明白,我们并没有解决这些警告,而只是禁用了它们。来自这些库的代码仍然有可能发生数据竞赛。如果你正在使用这些框架的实例,你需要考虑实例是否真的是线程安全的。一旦你使用的框架被更新为Sendable的一致性,你可以删除@preconcurrency属性,并修复可能触发的警告。
雪球 Android App 秒开实践
一、背景
启动速度可以说是一个 APP 的门面,对用户体验至关重要。随着业务不断增加,需要初始化的任务也越来越多,如果放任不管,启动时长会逐步增加,为此雪球客户端针对应用启动时长做了大量优化工作。本文从应用启动基本原理出发,总结了雪球客户端启动优化的思路和遇到的问题。主要包括启动原理介绍、优化方案和线上验证等三方面内容。
二、启动原理
根据 Google 官方文档,应用启动分为以下三种类型:
冷启动
热启动
温启动
冷启动
冷启动是指 APP 进程被杀死(系统回收、用户手动关闭等),启动 APP 需要系统重新创建应用进程,从用户点击应用桌面图标到第一个页面加载完成的全部过程。冷启动是启动类型中耗时最长的一种,也是启动优化最关键的优化点,下面我们来看一下冷启动的启动过程。
从上图可以看出 APP 冷启动可以分为以下三个过程:
用户点击桌面 APP 图标,调用 Launcher.startActivity ,由 Binder 通知 system_server 进程,system_server 内部使用 ActivityManagerService 通知 zygote 创建应用子进程
应用进程创建完成后,会加载 ActivityThread 并调用 ActivityThread.main 方法,用来实例化 ApplicationThread 、Lopper 和 Handler
ActivityThread 内部调用 attach 方法进行 Binder 通信回到 system_server 进程,执行 ActivityManagerService.attachApplication 完成 Application 的创建,同时启动第一个 Activity
我们可以换一种通俗易懂的描述:
想象一下把 Launcher 比做手机桌面,桌面里面很多 APP 可以理解成 Launcher 的孩子,zygote 就是一个进程,system_server 好比服务大管家,ActivityThread 好比每个 APP 进程自己的管家。
启动 APP 首先要通知服务大管家 (system_server),服务大管家 (system_server)收到通知后,会跟它的第一对接人 zygote 进程联系,请求 zygote 创建一个属于孩子的家,也就是 APP 自己的进程,进程创建完成后,接下来是属于孩子自己的工作,它开始使用自己的管家 ActivityThread 布置自己的家,可以简单把 Application 比做是大门,把 Activity 比作是卧室,AMS 是装修团队,ActivityThread 会不断和 AMS 交互,直到 Application 和 Activity 创建完毕,至此一个 APP 就启动完成了。
热启动
热启动是指应用程序从后台被唤起,此时应用进程仍然存在,应用启动无需创建子进程,但是可能会重新执行 Activity 的生命周期,在热启动中,系统的所有工作就是将您的 Activity 带到前台,只要应用的所有 Activity 仍驻留在内存中,应用就不必重复执行对象初始化、布局和绘制,例如用户按下 back 或者 home 键回到后台。
温启动
温启动包含了在冷启动期间发生的部分操作,同时它的开销要比热启动高,例如用户在退出应用后又重新启动应用,此时应用进程仍然存在,但应用必须通过调用 onCreate() 从头开始重新创建 Activity
冷启动是三种启动状态中最耗时的一种,启动优化也是在冷启动的基础上进行优化,热启动和温启动相对耗时较少,暂不考虑优化。
三、问题归因
工欲善其事必先利其器,要想优化启动时长,首先必须知道应用启动过程中发生了什么,以及耗时方法是哪些,下图列举了一些 APP 常用的性能检测工具:
adb shell
获取应用启动总时长 adb 命令:adb shell am start -W [packageName]/[packageName.xActivity]
详细使用可参考文档:developer.android.google.cn/studio/comm…
参数说明:
Activity:应用启动的第一个Activity
TotalTime:应用启动总时长,包括应用进程创建、Application 创建和第一个 Activity 创建并绘制完成到显示的所有过程,冷启动的情况下我们只需要关注 TotalTime 即可
Displayed
displayed 使用比较简单,我们只需要在 Logcat 中过滤关键字 displayed 即可看到应用启动的总时长,如下图所示,displayed 打印的时长跟 adb shell 几乎相同,也就是一次冷启动的总时长。
adb shell 和 displayed 都可以帮助我们快速获取应用启动时长,但是无法获取具体耗时方法的堆栈信息,应用启动的具体信息我们可以使用 Systrace 和 Traceview 来获取。
Systrace
Systrace 是 Android 平台自带的命令行工具,可记录短时间内的设备活动,并保存在压缩的文本文件中,该工具会生成一份报告,其中汇总了 Android 内核中的数据,例如 CPU 调度程序、磁盘活动和应用线程。
Systrace 工具默认在 Android SDK 里面,路径一般为 Android/sdk/platform-tools/systrace
使用 systrace 生成应用冷启动具体信息
如果没有配置环境变量,先切到 systrace 目录下 cd ~/Library/Android/sdk/platform-tools/systrace
执行 systrace.py -t 10 -o /Users/liuyakui/trace.html -a com.xueqiu.fund
或者直接用绝对路径执行 systrace
详细使用可参考文档:developer.android.google.cn/topic/perfo…
python ~/Library/Android/sdk/platform-tools/systrace/systrace.py -t 10 -o /Users/liuyakui/trace.html -a com.xueqiu.fund
systrace 报告如下图所示,这里仅摘取了启动优化所需要的主要信息:
区域1代表 CPU 使用率,柱子越高,越密集代表 CPU 使用率越高
区域2代表 CPU 编号,该设备是8核处理器,编号0-7,点击 CPU 编号区域,可以查看当前正在运行的任务
区域3代表所有线程和方法具体耗时情况,可以帮助我们定位具体耗时方法
从上图可以看出在0-3秒内,CPU 平均利用率较低,特别是1-3秒这段时间,CPU 几乎处于闲置状态,提高 CPU 利用率,充分发挥 CPU 的性能,是我们主要的优化方向。
上述三部分区域所提供的信息,基本上可以帮助我们定位启动耗时问题,它提供了 CPU 使用情况以及每个线程工作情况,但它不能告诉我们具体的问题代码在哪里,我们要确定具体的耗时代码,可以使用 Traceview 工具。
Traceview
Traceview 能够以图形化的形式展示线程的工作状态,以及方法的调用堆栈和调用链,我们以 application onCreate 为例,统计 onCreate() 内部详细的方法调用,并生成 trace 报表。
详细使用可参考文档:developer.android.google.cn/studio/prof…
@Override
public void onCreate() {
super.onCreate();
Debug.startMethodTracing("app_trace");
//初始化代码...
//...
Debug.stopMethodTracing();
}
应用启动完成后,会在 /sdcard/Android/data/com.xueqiu.fund/files 路径下生成一个 app_trace.trace 文件,直接用 AndroidStudio 打开即可,如下图所示:
trace 文件详细展示了每个线程的工作情况,以及每个线程内部具体的方法调用情况,下面简单介绍一下trace 报表中最重要的三块区域:
区域1代表 CPU 使用情况,可以拖拽选择时间段
区域2代表当前线程工作信息,截图所示为当前主线程在0-5s内所有的方法调用情况
区域3代表当前线程内部的方法调用堆栈,以及方法耗时等信息,使用 Top Down 和 Bottom Up 可以对方法正反排序
trace 报表清晰的展示了每个线程对应的所有方法的调用链和耗时情况,很好的帮助我们定位启动过程中具体问题所在,为优化方案提供了重要的参考依据。
四、优化方案
经过上述分析,APP 启动问题主要集中在以下两个阶段:
Application 创建
闪屏页绘制
因此下面主要是针对这两方面进行优化
Application 创建优化
从上述 Traceview 报表可以看出,影响 Application 创建的代码主要集中在 initThirdLibs 内部,我们来看一下 initThirdLibs 内部初始化代码执行流程。
initThirdLibs 内部包含了雪球客户端所有的初始化项,这些初始化任务不分主次和优先级都在主线程顺序执行,中间任意一个任务阻塞,都会影响 Application 的创建,而且随着业务不断迭代,初始化任务越来越多,Application 的创建时长也会继续累加。
因此梳理 initThirdLibs 内部任务的优先级,通过合理的方式统一调度,并对各个任务进行延迟初始化是优化 Application 创建的重要内容,延迟初始化主要实现的目标分为以下三点:
提高 CPU 利用率,充分发挥 CPU 性能
初始化任务 Task 处理,降低维护成本和提高任务调度的灵活性
多线程处理,梳理各个 Task 的优先级,形成一个有向无环图
Task 任务流程图如下:
关于启动器实现核心逻辑为,自定义线程池,根据设备 CPU 情况动态计算线程数量,保证所有 Task 任务并发执行,并且相互独立,所有 Task 执行完毕后会最后执行 Final Task 用来做一些收尾的工作,或者有强依赖的任务,也可以放到 Final Task 中进行,这里推荐以下两种实现方式:
CountDownLatch
自定义线程池
启动器伪代码如下:
//这里只是一段伪代码,帮助大家理解启动器的基本实现原理
TaskManager manager = new TaskManager();
ExecutorService service = createThreadPool();
final Task1 task1 = new Task1(1);
final Task2 task2 = new Task2(2);
final Task3 task3 = new Task3(3);
final Task4 task4 = new Task4(4);
for (int i = 0; i < n; i++) {
Runnable runnable = new Runnable() {
@Override
public void run() {
manager.get(i).start();
}
};
service.execute(runnable);
}
Task 调度完成后,将不依赖主线程的初始化任务,移动到并发 Task 中进行延迟初始化,进行统一管理并且避免阻塞主线程,提高 CPU 利用率。
闪屏页绘制优化
目前闪屏页主要承载的是业务广告,通过优化广告加载的逻辑可以间接调整页面的布局结构。
布局结构
闪屏页会预加载广告数据存到本地,每次应用启动从本地读取广告数据,这里我们可以优化无广告页面展示的逻辑,目前闪屏页无广告的时候仍然会加载布局文件,并设置默认的页面停留时长,理论上如果页面无广告,闪屏页创建完成后可以直接进入首页,不用加载页面的布局文件从而减少页面绘制时间,调整后页面广告加载逻辑核心代码如下:
private void prepareSplashAd() {
//读取广告数据
String jsonString = PublicSetting.getInstance().getSplashAd();
if (TextUtils.isEmpty(jsonString)) {
//无广告,关闭页面,进入首页
exitDelay();
return;
}
//加载布局文件
View parentView = inflateView();
setContentView(parentView);
//显示广告
AD todayAd = ads.get(0);
showSplashAd(todayAd.imgUrl, todayAd.linkUrl);
}
优化结果
经过多个版本的线上数据采样,启动时长明显下降,以华为 Mate 30E Pro 为例,效果对比如下:
优化前
优化后
从上面对比中可以看到,在5年以内的旗舰机型上,启动时长从原来的 1.9s - 2.5s 降低到 0.75s - 1.2s ,整体降低60%左右,可以达到秒开的效果!CPU 活动转为密集型,充分发挥 CPU 的性能,提高了 CPU 的利用率。
五、总结
本文先介绍了应用启动的基本原理,以及如何通过各种检测工具定位影响启动速度的原因,最后重点阐述 Application 创建和闪屏页绘制两个阶段的优化方案。同时它也代表一组最佳实践,在后续的性能优化中,都是不错的选择。
其实启动优化的方案还有很多,但我们除了要关注启动优化本身,更需要制定长远的规划,设计适合自己的方案,为后续业务迭代做好铺垫,避免再次出现启动时长逐步增加的问题。
作者:雪球工程师团队
来源:juejin.cn/post/7081606242212413447
入职东北国企做程序员一个月,感受如何?
不知不觉入职新公司快一个月了,突然心血来潮想跟大家唠唠,在新公司上班的感受。有好有坏,喜忧参半吧。
工作环境
我新入职的公司是哈尔滨的一家国企下的二级子公司,新成立的研发公司,目前还处于蓬勃发展的阶段,业务水准也算的上是不错了。
人
目前人数100多个,但是却五脏俱全。单说研发部门,从产品,UI,研发,测试,运维,甚至运营人员都很完善,人员只需要根据自己的职责去负责自己的事情就好。
办公环境可以分为两个环境,分别是“职能部门”和“研发部门”:
* 职能部门比较正式,工位、装修以及员工着装都比较正规。
* 研发部门较为随意一些,无论是工位还是桌椅什么的,有些东拼西凑的感觉,但是整体还是可以接受。
复制代码
另外可能是因为国企的原因,所有的工位都是大隔断那种,如果换成现在公司常见的大通桌,估计人数还能多做十好几个,毕竟我刚来的时候还没有正式工位坐呢。
吃
相比于在其他公司上班,可能在这最大的体会就是不用考虑吃什么。公司有食堂,提供午饭,菜不能选,但是每天四菜一汤,加水果或酸奶。相比于每天纠结的选择外卖,我对这个很满意。
晚上如果加班的话,公司会统一订餐,大概一餐的费用也在20至30块之间吧,当然也没法选择吃什么,有啥吃啥被。
早餐为什么最后说,因为公司的早餐在早上八点之前供应,八点半上班。。。有点难受啊。
幸好公司提供简单的零食,面包、火腿肠、泡面等等,虽然偶尔会被大家抢空,但是总比没有强吧。
行
上家公司离我家只有1公里的距离,所以从回到哈尔滨也没有买车,每天不行上班,还挺惬意的。
现在不行了,新公司距离家里有十好几公里,当然我也暂时没有选择买车,地铁出行,快捷方便,还省心,唯一的缺点就是要走个1.5公里吧。
在晚上八点之后打车可以报销的,但是只能是网约车,可能是出租车的票,粘贴太过麻烦了吧。反正我是不打车,因为我嫌报销麻烦。
工具
啥是工具呢,对程序员来说就是电脑了,公司提供电脑,但是性能就一般。可以自己去购买,提供发票到公司,然后按月返钱。但是电脑价格档位要达到10000以上,我是直呼好家伙。我的电脑才5700块买的啊,这是报不了。不过也无所以为了,毕竟我用不习惯苹果!(其实我还没用过)
公司的会议室设施还是不错的,各种投屏等等,比较先进,完全摒弃了传统的投影仪等等,这还让我对公司有种另眼相看的感觉。
还提供显示器什么的,自己申请就好了。
入职感受
我面试的岗位是java开发,常规的java框架使用起来都没有问题。面试过程还是比较简单的,主要是常用的一些组件,简单的实现原理等等,所以顺利通过了。
但是比较遗憾的公司给我砍了一些,但是比原本提高了大概20%吧,定位的职级也不是很高。说实话我还是有点难受的,毕竟整个面试过程,和我对个人的能力认知还是比较清楚地。
但是当我入职后我明白了,这里毕竟是哈尔滨,收入和年龄还是有很大的关系的。部门内有好几个大哥,年龄都是35+了,他们也只比我高了一个级别,想想也就释然了,在其位谋其政吧,他们的工作确实我我接下来要做的繁琐。希望日后能够慢慢的升职加薪吧。
总体来说,东北人还是敞亮,有事直接提,工作也没啥拐弯抹角的,干就完了。我才刚来公司第一天,就给我把工作安排上了,一点不拿我当外人啊。
工作感受
既然谈到工作了,就展开说说。
我第一天到公司,找了个临时工位,领导们各种git账号、禅道账号就给我创建好,一个项目扔给我,本月中期要求做完。。我当时内心的想法真的是:东北人果然是好相处啊,整的跟老同事似的。我能怎么办,干就完了啊。
项目还是很简单的,常规的springboot + mybatis + vue2的小项目,大概也没到月中期,一个礼拜就完事了。
比较让我惊喜的是部署的环节。居然使用的是devops工具KubeSphere。我只说这一句你们可能不理解,这是我在哈尔滨的第三家公司,从来没有一家公司说使用过k8s,甚至相关的devops工具。只能说是哈尔滨软件行业的云化程度还是太低了。唯一在上家公司的jenkins还是因为我想偷懒搭建的。
不过运维相关的内容都把握在运维人员手里,所以想要料及并且掌握先关的知识还是要自己私下去学习的。
项目其实都是小项目,以web端和app为主,基本都是前后端分离的单体架构。唯一我接触到的微服务架构应该就是公司的中台,提供统一的权限配置和登录认证,整体还是很不错的。
虽然公司的项目很多,工作看起来很忙碌,但实际还是比较轻松愉快的,我还能应付自如。每天晚上为了蹭一顿晚饭,通常会加班到七点半。用晚上这个时间更更文,也挺好的。
从体来说,是我比较喜欢的工作节奏。
个人分析
我是一个不太安定的人,长期干一件事会让我比较容易失去兴趣,还是挺享受刚换工作时,这段适应环境的感觉。也有可能更喜欢这种有一定挑战的感觉。
和上一家公司相比,这家公司在公司的时间明显多出很多,也没有那么悠闲了,但是我却觉得这更适合我,毕竟我是一个闲不住的人,安逸的环境让我感到格外的焦虑,忙碌的生活会让自己感到生活很充实。
记得之前的文章说过自己的身体健健的不太好,但是最近不知道是上班的路程变远,导致运动量的增加,之前不适的症状似乎都小时了。真闲出病来了!
作者:我犟不过你
来源:juejin.cn/post/7125627005407592462
程序界鄙视链的终点
前言
不知道是大数据惹的祸,还是我的被迫害妄想症犯了,总是刷到一些哭笑不得的内行笑话系列,想反驳又觉得不该年轻气盛,憋了很久,还是觉得系统性的抒发一下,今天要聊的是关于程序界的鄙视链话题,各位老哥,如果有涉及程序语言部分,欢迎来杠。
主流鄙视链
语言
🍺鄙视链的话题由来已久也一直存在,原本只是体现在适用性和从业选择方向上,像是之前有游戏梦想的基本主攻C、C++、MFC、DirectX、MFC、 QT, C# 以PC市场为主,后来的网页应用市场php、VB.NET、perl、asp.net、jsp、flex、flash应用鼎盛时期也占据半壁江山,塞班系统,塞班开发,以及手机市场昙花一现的各种手机应用开发语言,数据库也从sqlserve,oracle,mysql感觉是一段时间之后才慢慢进入视野,mogodb,时序库InfluxDB等等,后来的JQ、node、glup、boostrap、H5、canvas、angular、react、vue,、icon、antd、elment-ui再到混合开发多端应用,再到Objective-C、python、Goland、rust、deno等等等等。
编译器
🍺编译工具从TurboC、VC6、Dreamweaver、VS2005-VS2022、Eclipse、MyEclipse、idea、Android Studio、WebStorm、vscode、HBuilder X,编辑器换了好几轮。
个体挣扎
🍺很难想象短短的10几年时间里,经历了这么多轮换血和语言转换,很多过了鼎盛期已经被淘汰,很多半死不过的存续着,相信很多从业者也经历过某个语言从生到死的过程,一直都秉持着技多不压身的准则,一常备、一学习、一了解,虽然很多人都在杠,那个语言更底层,那个语言常青藤,那个语言生命周期最长,入门最难,我能理解,从事某一个语言耕耘良久突然宣告没有市场那种失落感,但这就跟历史一样,有其发展规律,历史框架下的人,都是规律的适应者,并非一成不变的,语言的高度也因其活跃度,主流面临解决的问题相关,所以其实跟绝大多数从业者半毛钱关系都没有,我们也只是受益者,并不代表你的高度到了那个层级,语言鄙视的说法就好像登山的人在嘲笑下山的人,不置可否。
上清下沉
🍺在google还没离场,淘宝还没发家的前夜,微博、金山、PC端游还火爆,工具大神,搜狐还红的时候,还没有什么大厂、外包的提法,都是搞软件的,只是主攻方向不同,能成长能学习就行,公司好有些光环,解决问题是最重要的,后来,我听过一个理论,学历和大厂,至少能保证从业者是优质里面的顶尖部分,乍一听觉得没道理,后来想想,当面试那关的能力划等号,我是选硕士更充门脸还是选专科,用脚也能做出选择,长此以往的上清下沉,盘古开天,辅助以各种奇葩的企业文化,企业鄙视链的说法也就不足为奇了。
价值化
🍺 “更好的值得更高的待遇”,工资待遇标签化,跟房子有了商业化属性一样,我比你拿的多,说明我方方面面碾压你,即使你不想被贴标签,也会被动的贴上标签,记得我从中型互联网转到传统企业时就被强制贴了一波标签,相信很多人摆平心态,也有这种无奈的体验,体验更差就是从出了名的外包场出来的,相信体感更差,如果你真的有计较,争论着低人一等,干同样的事儿,被区别对待,就跟秀才考功能,跟人攀比吃穿用度有什么差异。
乱象
🍗 有大神在买课,20多岁的架构师、一问缘由,算上加班,工作10年,之前一直是把这个东西当作调侃,没想到有人正儿八经的说出来了,听说现在软件培训费用就要几万,比上个大学还贵,教人在线面试,美化简历等等乱想,“我能有啥害人的心思呢,我只是想帮你”,我只是看上了你荷包里面跳动的money。
🍗 有人在孜孜不倦的教人python爬虫,“线上从入门到进去”,美化点的叫法叫数据采集、预处理,至于高端点儿的识别预测,算法类的东西,tensorflow一般人先不论你的机器跟不跟得上,学历已经卡出去大半人了,如果是测试自动化,稍微还好点儿,其他的真的就有点儿居心叵测了。
🍗 前几年直播编程号称几天0观看,后几年突然就多了,我始终理解不了,看视频能学到啥东西,正儿八经,有目标的实现某个功能目标,不才是正途吗?不知道是不是我太肤浅了。
🍗可能我不分端太久了,换了环境稍稍有点儿不适应,按理说,即使技术有语言有局限性,也不该分不清楚一些常规的状态码和逻辑主次关系,活脱脱完全限制了自己,把自己封印在了一个区域,这还是工作7-8年的,语言的多样性,会让我们的世界变的更大,当你不接受外部的内容,总耕耘在自己熟悉的领域,培养傲慢的同时,也会丧失敬畏。
🍗我不清楚这是不是普遍现象,前端面试多数只会问技术,不会涉及到功能闭环和业务,面了好几个,可能做的事情比较边角,也不会去试图理解做某一个应用的含义,完整性闭合性都说不出来,难道面的姿势不对,没有把准备的东西发挥出来,一到业务就避而不谈或者就说只做功能不涉及到业务。
🍗其后也莫名其妙面了报价30-40的,应该是30多,研究生,天然条件很好,其他的不论,只以面试论,我诧异的是,岗位属于业务擅长,着重点该在业务上,却神奇的写了一些技术,占了很大篇幅,问到具体的业务,条理分明的胡扯,或者涉密,问到技术又开始顾左右而言他。
🍗再有就是我很难相信,一个面试时综合能力还可以的人,业务能力为0的情况,可能王者天生爱执行吧。
🍗以上并不针对个人,只是想说明,做软件,很多人其实只是把它当作糊口的工具,本身其实并不喜欢这份工作,只是恰好工资相对较高,而且每个人对技术的追求分阶段不同,想法认知不同,很多情况要学会保留意见停止争论,待认知线在同一水准后,再适时决定,程序做久了要适当的学会拐弯,不然人为的屏障会越来越让你放弃沟通交流。
我的经历
接触
🍺细算下来我最早涉及到编程接触的第一门语言是java,那会刚考上大学,得知被调剂到了软件,无所事事跑到网吧了解了一哈啥是编程,跑了个java计算器的例子,第一次有种掌控的感觉,也许这就是编程带来的魅力之一,掌控感,后来上学微机原理,TurboC 输出了第一个程序标配Hello World, 我记得看过一段话,一笔一划码出一个世界,我想我原本应该就是热爱编程的,爱泡图书馆看些软件杂书,记得因为上课在看机器人人工智能算法,被老师注意到,莫名其妙的神经网络BP,从C,C++,C#薅了三遍,后面连带又薅了一波人工智能动态寻路directx渲染的规避,最终没能成功去做游戏,感觉血亏。
过程
🍺其后的工作经历之前也又提到过,无非就是遇山开山遇水开河,值得骄傲的是从来没因工作的地狱级难度退缩过,正儿八经外头的私活也整了又10年左右了,可能驳杂的技术体系也缘于此,心态比较重要,只要是能成长的都可以去学,熟悉的多了,就不会有恐惧感,我的很多技能点都属于外部创新,工作深挖实践过来的,信心需要培养,不知道你有没有这种中二的经历,每次解决一个疑难杂症,我总是不由自主的喊出来 “我TN真是个天才”,乐此不疲,也许这就是别人说的掌控感。
接触
🍺我看到很多人在说在中国不过20年,没看到过35岁之后还搞程序的,我本能的忽略了年龄这个问题,其实之前我确确实实看到过一个老哥60岁了,还在搞C++,烟瘾特别大,几乎很短实践就搞出了包含算法预处理的专业软件,当时可能还在自我膨胀中,没有意识到这项工作从0-1的难度有好大,之后也和一个60岁的老哥相处过一段,可能是年龄大了,有些不受招呼,风评不咋好,一块聊过一段,给我们讲了他的当年,合伙创业,失败就业,总之也是波澜壮阔,还有之前我们的总监,40多了长得跟个20多岁的人一样,为人随和,可能相处下来,感受不到年龄的隔阂,给我一种感觉,大家都差不多,提笔回顾,恍惚之间才意识到,当然现在特别是今年,经济不好,再加上各种企业文化,我对我能持续多久有过担忧,但尽最大的努力,留最小的遗憾,是我一直以来,对事儿的态度,如果沉浸在焦虑中,会错过很多风景,反而是在焦虑中浪费了时光.
▨▨▨没什么具体的该怎么做,只能说,适当的多放下身段,多听听周围不同岗位的人对实现具体某一件事情,别人的认知和评判是怎样的,和自己的认知背离是什么原因造成的,自己的原因多补充相关知识,别人的原因多吸取经验教训,如果同一件事情,自己认为很难,充满抱怨,别人觉得简单,思路清晰的解决了问题,该是你充分学习经验的时候
悟道
🍺戾气重的环境,让我们忘记了回溯,忘记了思考,很多的事情本能的忽略,软件"工具人"的称呼我并不排斥,但之前看贴的时候,看到很多人对这个称谓很不忿,觉得很恶心,但本质上,外包、中型厂、大厂“研发资源”的叫法会更好听吗?不是别人怎么叫,而是我们要认清不足,继续抵足前行,外部的杂音不足挂齿,内心的修炼与自身能力的强大才是我们该争取的,不想当将军的士兵,必然成不了将军,但想当将军的士兵,最终不一定会成为将军,只能说,行进的策略一直让我们时刻准备,时刻充实着,可能这是精神充实的一种“信仰”,但这不妨碍我时刻划定标准在进步着,所以忙着和别人攀比比较有什么意义呢,相较于环境与别人,改变自己才是最容易的吧。
原因刨析
💪关于大厂小厂之前一番讨论:
Me:事实上、有个很严重的分歧点在于,小厂更注重的是全面性,巴不得你从业务、前后端、框架、学习能力、设计能力、甚至商务以及交付能力都具备。往往从技术到支持都是考虑最低成本实现的,需要很强的灵活性和变通能力,而且很多业务都是在软件能力之上有行业经验要求的、所以降工资是一方面,还得适应变态的差异化开发习惯、
另外前段时间面试的时候发现个问题,纯前端有个很严重的弊端,最接近业务,却最不了解业务、问业务都不了解或者说不清楚闭环、
还有就是即便是技术专家、普遍的诉求其实当下不是开拓性市场、屠龙技需要平台才施展的开
前端早早聊:很有道理,大厂面试你的屠龙技,进去后拧 180 米长的复杂螺丝,不好拧,小厂面试你的螺丝功,进去后要求你用屠龙技,一个人全套搞定空间站,全能全干,两边点亮的技能点大有不同,需要的心态也大大不同
💪鄙视链的问题
语言鄙视
很多讨论其实集中在语言的入门难易度,应用层级的问题,其实跟用这门语言的人关系不大,最接近的关系我能一直用这门语言存续多久,也就是我的语言技能会不会随着实践继续升值。 后端、前端的问题,这个本质是技术局限性引发的,很多事情不去做,只是评价的话,这和你嘲讽搞PPT的人,外行指导内行有什么差别。
年龄鄙视
之前看到怪谈,通过不写注释,故意错乱结构来提高自己的存在价值,就事论事,能力是能力的问题,有些行为准则是人的问题,好多论调在说过了35岁,谁还需要去投简历,投简历的都是能力不行,还有别人已经挣够了,讲真的,靠打工致富毕竟是少数,都是机缘巧合,绝大部分人还是该忧虑就忧虑,"农民想象当皇帝用金锄头",放开眼界,总有不一样的精彩。
学历鄙视
早先的一段面试经历,感觉有震撼到我,我没想到还有公司会这么玩,找相关领域的开源作者挨个打电话,他们找到了一位开源作者,当时面我的作者也体验了一把被标签化,他说过一段 “语言只是工具,以实现功能为目的” ,听人力小姐姐介绍情况说,这个开源作者的神奇经历,高中辍学,一直是自由开发者,看了开源内容,质量很高,起点可能比很多人要差,但通过另外一种名片找到了归属,所以能力是真的会闪光,贵在坚持,至于卡学历等等的境遇,那也只说明你和这家公司的八字不合、换家便是。
技术鄙视
大到社会,小到公司,我们都是职能链上被需要的,很多技术经验丰富的去做架构设计,但厌恶循环往复的业务调整,很多对工作推进执行做的很好的,却没法理解架构设计中一些“脱裤子放屁”的举动,团队中成员可以被替换,但职能分工是必须的,难不成要搞一堆技术大佬天天干仗不成。
待遇鄙视
我们要为自己的选择负责,最终选定的工作,要么因为待遇高、要么因为压力小,如果你不慎踩坑,实在无法适应,多了解了解别人坚持下去的动机是啥、看到很多在抱怨“死都不去外包,侮辱人格,低人一等”,多想想能力和待遇插值,再有就是精神压力等等之类的,也比抱怨来的实在,大厂诉说着各种福利待遇,至于最终是其内里的红线、精神压力和健康付出状况,各种技术成长之类的,若真剔除自身的向上进取,于工作层面真有那么多高端的技术需要你去钻营嘛,就稳定性而言,我反而觉得大厂是最不受控的,因为真无关你的价值和能力,所以我觉得这个问题应该论证着看,并没有绝对的定性。
你的追求是什么?
我曾梦想着用代码改变世界,结果我改变了我的代码,我梦想竹杖芒鞋轻胜马,谁怕?一蓑烟雨任平生快意恩仇,潇洒江湖,结果只能护住身前一尺一个家。我梦想达则兼济天下,穷则独善其身,结果我依然穷着,却做不到独善其身,事到如今,我还是会经常想起我的梦想,却也不愤恨自己平凡的半生,无非是,我做着自己喜欢做的事情,这个事情恰巧又是我的工作,我用它支撑着我弱不惊风的家,仅此而已,但也不仅限于此,至少我还在我代码的江湖,追逐着...
结束吧
有点儿跑题了,最近实在是看到了很多怪像,希望留下你的经历,形成讨论,便于形成良性的参考价值,期待你的加入!!
PPS
本来吐槽居多,后来枚举语言更替的时候,忽然觉得,历经这么多变迁,每个挣扎着的程序员,其实也在无奈中成就了平凡的伟大,心态开阔,多点儿包容!!!
作者:沈二到不行
来源:juejin.cn/post/7129868233900818468
Android进阶宝典 -- GC与ART调优
1 GC相关算法
在进行GC的时候,垃圾回收器需要知道什么对象需要被回收,回收后内存如何整理,这其中就涉及到了很多核心的算法,这里详细介绍一下。
1.1 垃圾确认算法
垃圾确认算法,目的在于标记可以被回收的对象,其中主要有2种:引用计数算法和GcRoot可达性分析算法
1.1.1 引用计数算法
引用计数算法是比较原始的一个算法,核心逻辑采用计数器的方式,当一个对象被引用时,引用计数+1,而引用失效之后,引用计数-1,当这个对象引用计数为0时,代表该对象是可以被回收的。
那么这个引用计数算法被废弃的主要原因有2个:
(1)需要使用引用计数器存储计数,需要额外开辟内存;
(2)最大问题就是,无法解决循环引用的问题,这样会导致引用计数始终无法变为0,但两个引用对象已经没有其他对象使用了。
所以可达性分析算法的出现,就能够解决这个问题。
1.1.2 可达性分析算法
可达性分析算法,是以根节点集合为起点,其实就是GcRoots集合,然后遍历每个GcRoot引用的对象,其中与GcRoot直接或者间接连接的对象都是存活的对象,其他对象会被标记为垃圾。
那么什么样的对象会被选中为GcRoot呢?
(1)虚拟机栈局部变量表中的对象
这个其实比较好解释,就是一个方法的执行肯定需要这个对象的,如果随便就被回收了,这个方法也执行不下去了。
(2)方法区中的静态变量
(3)方法区中的常量
这种都是生命周期比较长的对象,也可以作为GcRoot
(4)本地方法栈中JNI本地方法的引用对象。
我们能够看到,GcRoot对象的共同点都是不易于被垃圾回收器回收。
1.2 垃圾清除算法
前面我们通过标记算法标记了可以被回收的对象,接下来通过垃圾清除算法就可以将垃圾回收
1.2.1 标记清除算法
其中打上标记的,就是需要被清除的垃圾对象,那么垃圾回收之后
这种算法存在的的问题:
(1)效率差; 需要遍历全部对象查找被标记的对象
(2)在GC的时候需要STW,影响用户体验
(3)核心问题会产生内存碎片,这种算法不能重新整理内存,例如需要申请4内存空间,会发现没有连续的4块内存,只能再次发起GC
1.2.2 复制算法
这部分跟新生代的survivor区域有些类似,复制算法是将内存区域1分为2,每次只使用1块区域,当发起GC的时候,先把活的对象全部复制到另一块区域,然后把当前区域的对象全部删除。
在分配内存时,只是使用左半边区域,发起GC后:
我们发现,复制算法会整理内存,这里就不会再有内存碎片了。
这种方式存在的弊端:因为涉及到内存整理,因此需要维护对象的引用关系,时间开销大。
1.3.3 标记整理算法
其实看名字,就应该知道这个算法是集多家之所长,在清除的同时还能去整理内存,避免内存碎片。
首先跟标记清除算法一样,先将死的对象全部清楚,然后通过算法内部逻辑移动内存碎片,使其成为一块连续的内存
其实3种算法比较来看,复制算法效率最快,但是内存开销大;相对来说,标记整理更加平滑一些,但是也不是最优解,而且凡是移动内存的操作,全部都会STW,影响用户体验。
1.3.4 分代收集算法
这个方式在上一篇文章开题就已经介绍过了,将堆区分为新生代和老年代,因为大部分对象一开始都会存储在Eden区,因此新生代会是垃圾回收最活跃的,因此在新生代就使用了复制算法,将新生代按照8(Eden):2(survivor)的比例分成,速度最快,减少因为STW带来的体验问题;
那么在老年代显然是GC不活跃的区域,而且在这个区域中不能有内存碎片,防止大对象无法分配内存,因此采用的是标记整理算法,始终是连续的内存区域。
2 垃圾回收器
2.1 垃圾回收的并行与串行
从上图中,我们可以看出,只有一个GC线程在执行垃圾回收操作,这个时候垃圾回收就是串行执行的。
在上图中,我们可以看到有多个GC线程在同时工作,这个时候垃圾回收就是并行的。
其实在多线程中有两个概念:并行和并发。
其中,并行就是上述GC线程,在同一时间段执行,但是线程之间并无竞争关系而是独立运行的,这就是并行执行;而并发同样也是多个线程在同一时间点执行,只不过他们之间存在竞争关系,例如抢占锁,就涉及到了并发安全的问题。
2.2 垃圾回收器分类
关于垃圾回收器的分类,我们从新生代和老年代两个大方向来看:
我们可以看到,在新生代的垃圾回收器,都是采用的复制算法,目的就是为了提效;而在老年代而是采用标记整理算法居多,前面的像Serial、ParNew这些垃圾回收器采用的复制算法我们都明白是什么流程,接下来介绍下CMS垃圾回收器的并发标记清除算法思想。
2.2.1 CMS垃圾回收器
CMS垃圾回收器,是JDK1.5之后发布的第一款真正意义上的并发垃圾回收器。它采用的思想是并发标记 - 清除 - 整理,真正去优化因为STW带来的性能问题。
这里先看下CMS的具体工作原理
(1)标记GCROOT对象;这个过程时间短,会STW;
(2)标记整个GCROOT引用链;这个过程耗时久,采用并发标记的方式,与用户线程混用,不会STW,因为耗时比较久,在此期间可能会产生新的对象;
(3)重新标记;因为第二步可能产生新的对象,因此需要重新标记数据变动的地方,这个过程时间短,会STW;
(4)并发清理;将标记死亡的对象全部清除,这个过程不会STW;
看到上面的主要过程后,可能会问,整理内存并没有做,那么是什么时候完成的内存整理呢?其实CMS内存整理并不是伴随着每次GC完成的,而是开启定时,在空闲的时间完成内存整理,因为内存整理会导致STW,这样就不会影响到用户体验。
3 ART虚拟机调优
前面我们介绍的都是JVM,而Android开发使用的又不是JVM,那么为什么要学习JVM呢,其实不然,因为不管是ART还是Dalvik,都是依赖JVM的规范做的衍生产物,所以两者是相通的。
3.1 Dalvik和ART与Hotspot的区别
首先Android中使用的ART虚拟机,在Android 5.0以前是Dalvik虚拟机,这两种虚拟机与Hotspot基本是一样的,差别在于两者执行的指令集是不一样的,Android中指令集是基于寄存器的,而Hotspot是基于堆栈的;还有就是Android虚拟机不能执行class文件,而是执行dex文件。
接下来我们通过对比DVM和JVM运行时数据区的差异
3.1.1 栈区别
我们知道,在JVM中执行方法时,每个方法对应一个栈帧,每个栈帧中的数据结构如下:
而ART/Dalvik中同样存在栈帧,但是跟Hotspot的差别比较大,因为Android中指令集是基于寄存器的,所以将局部变量表和操作数栈移除了,取而代之的是寄存器的形式。
因为在字节码指令中指明了操作数的地址,因此CPU可以直接获取到操作数,例如累加操作,通过CPU的ALU计算单元直接计算,然后赋值给另一块内存地址,相较于JVM不断入栈出栈,这种响应速度更快,尤其对于Android来说,速度大于一切。
所以DVM的栈内存相较于JVM,少了操作数栈的概念,而是采用了寄存器的多地址模式,速度更快。
3.1.2 堆内存
ART的堆内存跟JVM的堆内存几乎是完全不一样的,主要是分为4块:
(1)Image Space:这块区域用于存储预加载的类,在类加载之前自动加载
这部分首先要从Dalvik虚拟机开始说起,在Android 2.2之后,Dalvik引入了JIT(即时编译技术),它会对于执行过的代码做dex优化,不需要每次都编译dex文件,提高了执行的速度,但是这个是在运行时做的处理,dex转为机器码需要时间。
因此在Android 5.0之后,Dalvik被废弃,取而代之的是ART虚拟机,从而引进了全新的编译方式AOT,就是在安装app的过程中,将dex文件全部编译为本地机器码,运行时就直接拿机器码执行,提高了执行速度,但是也存在很多问题,安装app的时候特别慢,造成资源浪费。
因此在Android N(Android 7.0)之后,引入了混编技术(JIT + 解释 + AOT)。在安装应用的时候不再全量转换,那么安装速度变快了;而是在运行时将经常执行的方法进行JIT,并将这些信息保存在Profile文件中,那么在手机空闲或者充电的时候,后台有一个BackgroundDexOptService会从Profile文件中拿到这些方法,看哪些没有编译成机器码进行AOT,然后存储在base.art文件中
那么base.art文件就是存储在Image Space中的,这个区域不会发生GC。
(2)Zygote Space:用于存储Zygote进程启动之后,预加载的类和创建的对象;\
(3)Allocation Space:用于存储用户数据,我们自己写的代码创建的对象,类似于JVM中堆的新生代
(4)LargeObject Space:用于存储超过12K(3页)的大对象,类似于JVM堆中的老年代
3.1.3 对象分配
在ART中存在3种GC策略,内部采用的垃圾回收器是CMS:
(1)浮游GC:这次GC只会回收上次GC到本次GC中间申请的内存空间;
(2)局部GC:除了Image Space和Zygote Space之外的内存区域做一次内存回收;
(3)全量GC:除了Image Space之外,全部的内存做一次内存回收。
所以在ART分配对象的时候,会从第一个策略开始依次判断是否有足够空间分配内存,如果不够就继续往下走;如果全量GC都无法分配内存,那么就判断是否能够扩容堆内存。
3.2 线上内存问题定位
回到
# Android进阶宝典 -- JVM运行时数据区开头说的场景
(1)App莫名其妙地产生卡顿;
(2)线下测试好好的,到了线上就出现OOM;
(3)自己写的代码质量不高;
其实我们在线下开发的过程中,如果不注意内存问题其实很难会发现,因为我们每次修改都会run一次应用,相当于应用做了一次重置,类似于OOM或者内存溢出很难察觉,但是一到线上,用户使用时间久了就会出问题,下面就用一个线上案例配合JVM内存分配查找问题原因。
当时的场景,我们需要自定义一个View,这个View在旋转的时候需要做颜色的渐变,我们先看下出问题的代码。
class MyFadeView : View {
constructor(context: Context) : super(context) {
initView()
}
constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet) {
initView()
}
private fun initView() {
initTimer()
}
private val colors = mutableListOf("#CF1B1B", "#009988", "#000000")
private var currentColor = colors[0]
@SuppressLint("DrawAllocation")
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
Log.e("TAG", "onDraw")
val borderPaint = Paint()
borderPaint.color = Color.parseColor(currentColor)
borderPaint.isAntiAlias = true
borderPaint.strokeWidth =
context.resources.getDimension(androidx.constraintlayout.widget.R.dimen.abc_action_bar_content_inset_material)
val path = Path()
path.moveTo(0f, 0f)
path.lineTo(0f, 100f)
path.lineTo(100f, 100f)
path.lineTo(100f, 0f)
path.lineTo(0f, 0f)
canvas?.let {
it.drawPath(path, borderPaint)
}
}
private var FadeRunnable: Runnable = Runnable {
currentColor = colors[(0..2).random()]
postInvalidate()
}
private fun initTimer() {
val timer = object : CountDownTimer(1000, 2000) {
override fun onTick(millisUntilFinished: Long) {
Handler().post(FadeRunnable)
initTimer()
}
override fun onFinish() {
}
}
timer.start()
}
}
这里我们先做一个简单的自定义View,然后我们可以看下内存Profiler
内存曲线还是比较平滑的,看下对象分配
其中Paint还有Path创建的对象比较多,为什么呢?伙伴们应该都知道,每次调用postInvalidate方法,都会走onDraw方法,频繁地调用onDraw方法,导致Paint和Path被创建了多次。
在之前JVM的学习中,我们知道当一个方法结束之后,栈内的对象也会被回收,因此这样就会造成频繁地创建和销毁对象,如果当前内存紧张便会频繁地GC,导致内存抖动,因此创建对象不能在频繁调用的方法中执行,需要在initView中做初始化。
还有就是,伙伴们有用过直接使用Color.parseColor去加载一种颜色,这种方法也不能在频繁调用的方法中执行,看下源码,在这个方法中调用了substring方法,每次都会创建一个String对象。
那么有个问题,内存抖动是造成App卡顿的真凶吗?其实不然,即便是产生了内存抖动,在方法执行结束之后,对象也都被回收掉了不会存在于内存中,JVM还是很强大的,在内存充足的时候还是没有太大的影响的。
如果是产生了卡顿,那么一定伴随着内存泄漏,因为内存泄漏导致内存不断减少,从而导致了GC的提前到来,又加上频繁地创建和销毁对象,导致频繁地GC,从而产生了卡顿。
在# Android性能优化 -- 内存优化这篇文章中,有关于内存优化工具的具体使用,有兴趣的伙伴可以看一下。
链接:https://juejin.cn/post/7154929465749929997
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Flutter paint shader渐变使用的问题
背景
flutter版本要实现一个渐变的圆弧指示器,如图
颜色需要有个渐变,而且根据百分比的不同,中间的菱形指向还不一样
1.自定义CustomPainter
class PlatePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
// 画图逻辑
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
// 是否需要重绘的判断 ,可以先返回false
return false;
}
}
然后加入一点点画图的细节:
import 'dart:math';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
class PlatePainter3 extends CustomPainter {
final Paint _paintProgress = Paint()
..strokeWidth = 15
..style = PaintingStyle.stroke;
final Paint _paintBg = Paint()
..strokeWidth = 15
..color = const Color(0xFFC8CAFF).withAlpha(22)
..style = PaintingStyle.stroke;
final Paint _paintLine = Paint()
..strokeWidth = 2
..color = const Color(0Xff7A80FF)
..style = PaintingStyle.fill;
final Path _path = Path();
final Paint _paintCenter = Paint()
..strokeWidth = 2
..color = const Color(0xFF767DFF).withAlpha(14)
..style = PaintingStyle.fill;
@override
void paint(Canvas canvas, Size size) {
final width = size.width;
final height = size.height;
final center = Offset(width / 2, height * 3 / 4);
final rect = Rect.fromCircle(
center: center,
radius: 60,
);
canvas.drawArc(rect, pi * 0.8, pi * 2 * (0.1 + 0.1 + 0.5), false, _paintBg);
_paintProgress.shader = ui.Gradient.sweep(
center,
[
const Color(0XffCACCFF),
const Color(0Xff7A80FF),
],
);
canvas.drawArc(rect, pi * 0.8, (pi * 2 * 0.7) , false, _paintProgress);
TextPainter textPainter = TextPainter(
text: const TextSpan(text: '0', style: TextStyle(color: Colors.black, fontSize: 10)),
textDirection: TextDirection.ltr,
);
textPainter.layout(maxWidth: width);
textPainter.paint(canvas, Offset(width / 2 - 60 + 15, height - 5));
textPainter.text = const TextSpan(text: '100', style: TextStyle(color: Colors.black, fontSize: 10));
textPainter.layout(maxWidth: width);
textPainter.paint(canvas, Offset(width / 2 + 60 - 15 - 20, height - 5));
Offset c = Offset(width / 2, height * 3 / 4);
var angle = pi * 0.8 + pi * 2 * (0.1 + 0.1 + 0.5) ;
canvas.drawLine(c + _calXYByRadius(angle, 50), c + _calXYByRadius(angle, 70), _paintLine);
final o1 = c+_calXYByRadius(angle, 15);
final o2 = c+_calXYByRadius(angle + pi, 15);
final o3 = c+_calXYByRadius(angle + 0.5 * pi, 5);
final o4 = c+_calXYByRadius(angle + pi + 0.5 * pi, 5);
_path.reset();
_path.moveTo(o1.dx, o1.dy);
_path.lineTo(o3.dx, o3.dy);
_path.lineTo(o2.dx, o2.dy);
_path.lineTo(o4.dx, o4.dy);
_path.close();
_paintCenter.color = const Color(0xFF767DFF);
canvas.drawPath(_path, _paintCenter);
_paintCenter.color = const Color(0xFF767DFF).withAlpha(14);
canvas.drawCircle(c, 20, _paintCenter);
_paintCenter.color = Colors.white;
canvas.drawCircle(c, 2, _paintCenter);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
Offset _calXYByRadius(double angle, double radius) {
final y = sin(angle) * radius;
final x = cos(angle) * radius;
return Offset(x, y);
}
}
中间颜色的渐变用到了Paint的方法shader,设置的属性为 dart:ui包下的Gradient,不要导错包了,应该import的时候加入 as ui,才可以如代码中设置的样式.
import 'dart:ui' as ui;
满心欢喜的运行一下,Duang
渐变颜色没有按照想象中的开始和结束.
2.关于 paint的shader属性
/// The shader to use when stroking or filling a shape.
///
/// When this is null, the [color] is used instead.
///
/// See also:
///
/// * [Gradient], a shader that paints a color gradient.
/// * [ImageShader], a shader that tiles an [Image].
/// * [colorFilter], which overrides [shader].
/// * [color], which is used if [shader] and [colorFilter] are null.
Shader? get shader {
return _objects?[_kShaderIndex] as Shader?;
}
set shader(Shader? value) {
_ensureObjectsInitialized()[_kShaderIndex] = value;
}
直接查看Gradient类的sweep方法,参数如下
Gradient.sweep(
Offset center,
List<Color> colors, [
List<double>? colorStops,
TileMode tileMode = TileMode.clamp,
double startAngle = 0.0,
double endAngle = math.pi * 2,
Float64List? matrix4,
])
翻译如下
创建一个以
center
为中心、从startAngle
开始到endAngle
结束的扫描渐变。startAngle
和endAngle
应该以弧度提供,零弧度是center
右侧的水平线,正角度围绕center
顺时针方向。如果提供了colorStops
,colorStops[i]
是一个从 0.0 到 1.0 的数字,它指定了color[i]
在渐变中的开始位置。如果colorStops
没有提供,那么只有两个停止点,在 0.0 和 1.0,是隐含的(因此color
必须只有两个条目)。startAngle
之前和endAngle
之后的行为由tileMode
参数描述。有关详细信息,请参阅 [TileMode] 枚举。
哦哦,应该修改startAngle和endAngle方法,然后按照开始和结束的颜色结束.修改
_paintProgress.shader = ui.Gradient.sweep(
center,
[
const Color(0XffCACCFF),
const Color(0Xff7A80FF),
],
[0, 1],
TileMode.clamp,
0.8 * pi,
2.2 * pi,
);
然后运行
好像开始的颜色正常了,但是结束颜色还是一样的问题.
3.两种解决方法
3.1 设置shader属性(推荐)
_paintProgress.shader = ui.Gradient.sweep(
center,
[
const Color(0Xff7A80FF),
const Color(0XffCACCFF),
const Color(0Xff7A80FF),
],
[0.0, 0.5, 0.9],
TileMode.clamp,
);
运行如图:
3.2 旋转控件,开始绘制从0开始
painter修改代码
import 'dart:math';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
class PlatePainter4 extends CustomPainter {
final Paint _paintProgress = Paint()
..strokeWidth = 15
..style = PaintingStyle.stroke;
final Paint _paintBg = Paint()
..strokeWidth = 15
..color = const Color(0xFFC8CAFF).withAlpha(22)
..style = PaintingStyle.stroke;
final Paint _paintLine = Paint()
..strokeWidth = 2
..color = const Color(0Xff7A80FF)
..style = PaintingStyle.fill;
final Path _path = Path();
final Paint _paintCenter = Paint()
..strokeWidth = 2
..color = const Color(0xFF767DFF).withAlpha(14)
..style = PaintingStyle.fill;
@override
void paint(Canvas canvas, Size size) {
final width = size.width;
final height = size.height;
final center = Offset(width / 2, height /2);
final rect = Rect.fromCircle(
center: center,
radius: 60,
);
canvas.drawArc(rect, 0, (pi * 1.4), false, _paintBg);
_paintProgress.shader = ui.Gradient.sweep(
center,
[
const Color(0XffCACCFF),
const Color(0Xff7A80FF),
// const Color(0Xff7A80FF),
// Colors.white,
// Colors.black,
],
);
canvas.drawArc(rect, 0, (pi * 1.4) , false, _paintProgress);
// TextPainter textPainter = TextPainter(
// text: const TextSpan(text: '0', style: TextStyle(color: Colors.black, fontSize: 10)),
// textDirection: TextDirection.ltr,
// );
// textPainter.layout(maxWidth: width);
// textPainter.paint(canvas, Offset(width / 2 - 60 + 15, height - 5));
// textPainter.text = const TextSpan(text: '100', style: TextStyle(color: Colors.black, fontSize: 10));
// textPainter.layout(maxWidth: width);
// textPainter.paint(canvas, Offset(width / 2 + 60 - 15 - 20, height - 5));
Offset c = Offset(width / 2, height / 2);
var angle = pi * 1.4 ;
canvas.drawLine(c + _calXYByRadius(angle, 50), c + _calXYByRadius(angle, 70), _paintLine);
final o1 = c+_calXYByRadius(angle, 15);
final o2 = c+_calXYByRadius(angle + pi, 15);
final o3 = c+_calXYByRadius(angle + 0.5 * pi, 5);
final o4 = c+_calXYByRadius(angle + pi + 0.5 * pi, 5);
_path.reset();
_path.moveTo(o1.dx, o1.dy);
_path.lineTo(o3.dx, o3.dy);
_path.lineTo(o2.dx, o2.dy);
_path.lineTo(o4.dx, o4.dy);
_path.close();
_paintCenter.color = const Color(0xFF767DFF);
canvas.drawPath(_path, _paintCenter);
_paintCenter.color = const Color(0xFF767DFF).withAlpha(14);
canvas.drawCircle(c, 20, _paintCenter);
_paintCenter.color = Colors.white;
canvas.drawCircle(c, 2, _paintCenter);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
Offset _calXYByRadius(double angle, double radius) {
final y = sin(angle) * radius;
final x = cos(angle) * radius;
return Offset(x, y);
}
}
页面代码加入旋转代码:
Transform.rotate(
angle: 0.8 * pi,
child: CustomPaint(
painter: PlatePainter4(),
size: const Size(180, 180),
),
),
运行如下图第二个:
缺点:画文字的坐标还需要重新计算和旋转
4.加上动画,动起来
效果图:
最终代码:
Page:
import 'dart:math';
import 'package:demo4/widgets/plate_painter.dart';
import 'package:demo4/widgets/plate_painter3.dart';
import 'package:flutter/material.dart';
import '../widgets/plate_painter2.dart';
import '../widgets/plate_painter4.dart';
class Page6 extends StatefulWidget {
const Page6({Key? key}) : super(key: key);
@override
State<Page6> createState() => _Page6State();
}
class _Page6State extends State<Page6> with TickerProviderStateMixin{
late AnimationController _animationController;
static final Animatable<double> _iconTurnTween =
Tween<double>(begin: 0.0, end: 1.0).chain(CurveTween(curve: Curves.fastOutSlowIn));
@override
void initState() {
_animationController = AnimationController(vsync: this, duration: const Duration(seconds: 6));
_animationController.drive(_iconTurnTween);
_animationController.forward();
super.initState();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('自定义圆盘'),
),
body: Column(
children: [
AnimatedBuilder(
animation: _animationController.view,
builder: (_, __) {
final progress = _animationController.value;
return CustomPaint(
painter: PlatePainter(progress),
size: const Size(180, 180),
);
},
),
AnimatedBuilder(
animation: _animationController.view,
builder: (_, __) {
final progress = _animationController.value;
return Transform.rotate(
angle: 0.8 * pi,
child: CustomPaint(
painter: PlatePainter2(progress),
size: const Size(180, 180),
),
);
},
),
],
),
);
}
}
方法一:
import 'dart:math';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
class PlatePainter extends CustomPainter {
PlatePainter(
this.progress,
);
final num progress;
final Paint _paintProgress = Paint()
..strokeWidth = 15
..style = PaintingStyle.stroke;
final Paint _paintBg = Paint()
..strokeWidth = 15
..color = const Color(0xFFC8CAFF).withAlpha(22)
..style = PaintingStyle.stroke;
final Paint _paintLine = Paint()
..strokeWidth = 2
..color = const Color(0Xff7A80FF)
..style = PaintingStyle.fill;
final Path _path = Path();
final Paint _paintCenter = Paint()
..strokeWidth = 2
..color = const Color(0xFF767DFF).withAlpha(14)
..style = PaintingStyle.fill;
@override
void paint(Canvas canvas, Size size) {
final width = size.width;
final height = size.height;
final center = Offset(width / 2, height * 3 / 4);
final rect = Rect.fromCircle(
center: center,
radius: 60,
);
canvas.drawArc(rect, pi * 0.8, pi * 2 * (0.1 + 0.1 + 0.5), false, _paintBg);
_paintProgress.shader = ui.Gradient.sweep(
center,
[
const Color(0Xff7A80FF),
const Color(0XffCACCFF),
const Color(0Xff7A80FF),
],
[0.0, 0.5, 0.9],
TileMode.clamp,
);
canvas.drawArc(rect, pi * 0.8, (pi * 2 * 0.7) * progress, false, _paintProgress);
TextPainter textPainter = TextPainter(
text: const TextSpan(text: '0', style: TextStyle(color: Colors.black, fontSize: 10)),
textDirection: TextDirection.ltr,
);
textPainter.layout(maxWidth: width);
textPainter.paint(canvas, Offset(width / 2 - 60 + 15, height - 5));
textPainter.text = const TextSpan(text: '100', style: TextStyle(color: Colors.black, fontSize: 10));
textPainter.layout(maxWidth: width);
textPainter.paint(canvas, Offset(width / 2 + 60 - 15 - 20, height - 5));
Offset c = Offset(width / 2, height * 3 / 4);
var angle = pi * 0.8 + pi * 2 * (0.1 + 0.1 + 0.5) * progress;
canvas.drawLine(c + _calXYByRadius(angle, 50), c + _calXYByRadius(angle, 70), _paintLine);
final o1 = c+_calXYByRadius(angle, 15);
final o2 = c+_calXYByRadius(angle + pi, 15);
final o3 = c+_calXYByRadius(angle + 0.5 * pi, 5);
final o4 = c+_calXYByRadius(angle + pi + 0.5 * pi, 5);
_path.reset();
_path.moveTo(o1.dx, o1.dy);
_path.lineTo(o3.dx, o3.dy);
_path.lineTo(o2.dx, o2.dy);
_path.lineTo(o4.dx, o4.dy);
_path.close();
_paintCenter.color = const Color(0xFF767DFF);
canvas.drawPath(_path, _paintCenter);
_paintCenter.color = const Color(0xFF767DFF).withAlpha(14);
canvas.drawCircle(c, 20, _paintCenter);
_paintCenter.color = Colors.white;
canvas.drawCircle(c, 2, _paintCenter);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return (oldDelegate as PlatePainter).progress != progress;
}
Offset _calXYByRadius(double angle, double radius) {
final y = sin(angle) * radius;
final x = cos(angle) * radius;
return Offset(x, y);
}
}
方法二:
import 'dart:math';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
class PlatePainter2 extends CustomPainter {
PlatePainter2(
this.progress,
);
final num progress;
final Paint _paintProgress = Paint()
..strokeWidth = 15
..style = PaintingStyle.stroke;
final Paint _paintBg = Paint()
..strokeWidth = 15
..color = const Color(0xFFC8CAFF).withAlpha(22)
..style = PaintingStyle.stroke;
final Paint _paintLine = Paint()
..strokeWidth = 2
..color = const Color(0Xff7A80FF)
..style = PaintingStyle.fill;
final Path _path = Path();
final Paint _paintCenter = Paint()
..strokeWidth = 2
..color = const Color(0xFF767DFF).withAlpha(14)
..style = PaintingStyle.fill;
@override
void paint(Canvas canvas, Size size) {
final width = size.width;
final height = size.height;
final center = Offset(width / 2, height /2);
final rect = Rect.fromCircle(
center: center,
radius: 60,
);
canvas.drawArc(rect, 0, (pi * 1.4), false, _paintBg);
_paintProgress.shader = ui.Gradient.sweep(
center,
[
const Color(0XffCACCFF),
const Color(0Xff7A80FF),
// const Color(0Xff7A80FF),
// Colors.white,
// Colors.black,
],
);
canvas.drawArc(rect, 0, (pi * 1.4) * progress, false, _paintProgress);
// TextPainter textPainter = TextPainter(
// text: const TextSpan(text: '0', style: TextStyle(color: Colors.black, fontSize: 10)),
// textDirection: TextDirection.ltr,
// );
// textPainter.layout(maxWidth: width);
// textPainter.paint(canvas, Offset(width / 2 - 60 + 15, height - 5));
// textPainter.text = const TextSpan(text: '100', style: TextStyle(color: Colors.black, fontSize: 10));
// textPainter.layout(maxWidth: width);
// textPainter.paint(canvas, Offset(width / 2 + 60 - 15 - 20, height - 5));
Offset c = Offset(width / 2, height /2);
var angle = pi * 1.4 * progress;
canvas.drawLine(c + _calXYByRadius(angle, 50), c + _calXYByRadius(angle, 70), _paintLine);
final o1 = c+_calXYByRadius(angle, 15);
final o2 = c+_calXYByRadius(angle + pi, 15);
final o3 = c+_calXYByRadius(angle + 0.5 * pi, 5);
final o4 = c+_calXYByRadius(angle + pi + 0.5 * pi, 5);
_path.reset();
_path.moveTo(o1.dx, o1.dy);
_path.lineTo(o3.dx, o3.dy);
_path.lineTo(o2.dx, o2.dy);
_path.lineTo(o4.dx, o4.dy);
_path.close();
_paintCenter.color = const Color(0xFF767DFF);
canvas.drawPath(_path, _paintCenter);
_paintCenter.color = const Color(0xFF767DFF).withAlpha(14);
canvas.drawCircle(c, 20, _paintCenter);
_paintCenter.color = Colors.white;
canvas.drawCircle(c, 2, _paintCenter);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return (oldDelegate as PlatePainter2).progress != progress;
}
Offset _calXYByRadius(double angle, double radius) {
final y = sin(angle) * radius;
final x = cos(angle) * radius;
return Offset(x, y);
}
}
链接:https://juejin.cn/post/7155752698229293086
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
类Discord应用『环信超级社区1.0』项目介绍【附源码】
2021年马斯克让Clubhouse火爆出圈,2022年Discord以1.5亿月活150亿美元估值的数据让全球的开发者们看到了泛娱乐领域新的机会,环信作为泛娱乐行业的基础设施服务商,一直致力于给开发者提供更稳定的SDK,更丰富更易用的API,更垂直的场景解决方案。近日环信重磅推出了“环信超级社区DEMO”,这是一款类Discord产品的开源项目,在此基础上二开,可以快速搭建国内版Discord产品,帮您节省60%的开发难度!
项目介绍
环信超级社区是一款基于环信IM+声网RTC打造的类Discord实时社区应用,用户可创建/管理自己的兴趣社区,设置/管理频道(群组),支持陌生人/好友单聊、社区成员无上限,可创建的频道数无上限,用户加入的频道数无上限,真正实现万人实时群聊,语音聊天等。
功能架构
核心优势
1、IM提供高并发的通讯管道,支持亿级用户并发
▲万人群组互动
▲群组数量无上限
▲自定义加群权限设置
▲支持群资料和属性
▲提供群组/聊天室完善的群聊管理功能
▲提供管理员列表、成员列表、禁言列表、黑名单等服务
▲聊天室功能与直播功能进行对接实现直播聊天室
▲可以根据客户需要进行灵活配置,包括关系、数量、能力
2、百万人大群组承载
环信群组分片技术:将1个群中百万成员分片在100个万人群里
3、消息爆炸问题
解决方案:通过notice减少消息的Qps,进群后再拉取下发消息
4、环信SD-GMN,构建低延迟网络,实现全球加速
▲五大数据中心覆盖全球200+个国家和地区;
▲集团自建上万台服务器,部署全球300多个补充加速节点,实现低延迟;
▲FPA加速与AWS加速智能切换,确保通信质量和高可用能力;
▲典型时延:北美,30-40毫秒;欧洲20-30毫秒;东南亚,日韩30-40毫秒;中东70毫秒;北非45毫秒;澳洲50毫秒;最远的南美和南非,90毫秒;
▲持续改进,不断优化…
5、内容过滤能力
环信内容审核系统,低成本,高效率,个性化,高准确
适合场景
兴趣社交、游戏社交、区块链、媒体、粉丝社区、品牌社区等等。
项目源码
https://github.com/easemob/easemob_supercommunity
APK下载
链接: https://pan.baidu.com/s/1HUL_CUYTvUr3mT29WRcoaQ 提取码: zq1x
超级社区2.0
继超级社区1.0以后,环信推出了超级社区2.0(Circle),这是一款基于环信 IM 打造的类 Discord 实时社区应用场景方案,支持社区(Server)、频道(Channel) 和子区(Thread) 三层结构。一个 App 下可以有多个社区,同时支持陌生人/好友单聊。用户可创建和管理自己的社区,在社区中设置和管理频道将一个话题下的子话题进行分区,在频道中根据感兴趣的某条消息发起子区讨论,实现万人实时群聊,满足超大规模用户的顺畅沟通需求。旨在一站式帮助客户快速开发和构建稳定超大规模用户即时通讯的"类Discord超级社区",作为构建实时交互社区的第一选择,环信超级社区自发布以来很好地满足了类 Discord 实时社区业务场景的客户需求,并支持开发者结合业务需要灵活自定义产品形态,目前已经广泛服务于国内头部出海企业以及海外东南亚和印度企业。
环信超级社区2.0介绍:https://www.easemob.com/product/im/circle
环信超级社区2.0体验:https://www.easemob.com/download/demo#discord
viewpager2中viewModelScope 取消的问题
场景
有这么一个场景,一个菜谱订制的app里,用户是根据每周作为一个周期制定自己的菜谱计划,每天从已知菜谱库存中选一两道菜,规划自己下周做什么吃,下下周做什么吃。
viewpager(或viewpager2)中加载若干个fragment,fragment里被传入一个时间戳的参数,用这个时间戳当做初始时间,计算出本周的起止时间(周日~周六),然后获取这一周的菜谱计划。类似于这样(demo式UI)
上图中标注a点击后切到上一周(viewpager2中前一个fragment),b点击切到下一周,下方列表为viewpager2(若干fragments)当然列表显示什么不重要
viewpagerAdapter 类似这样,用list维护一个Fragment
class MealPagerAdapter(fm: FragmentManager?, life : Lifecycle) : FragmentStateAdapter(fm!! , life) {
val fragmentList = mutableListOf<MealFragment>()
val startDateTimeList = mutableListOf<Long>()
fun addItem(startDate : Long) {
startDateTimeList.add(startDate)
fragmentList.add(MealFragment.newInstance(startDate))
notifyItemInserted(fragmentList.size - 1)
}
override fun getItemCount(): Int = fragmentList.size
override fun createFragment(position: Int): MealFragment = fragmentList[position]
}
默认初始调用一次addItem方法,参数传入当前周(周日开始)的周日0点的时间戳,就可以获得这一周的起止日期了,这是一个可以无限添加fragment的viewpager2, 所以
offscreenPageLimit = 5
设置多少不那么重要了,暂定5吧
MealFragment里就是 viewmodel中定义一个initData()方法,伪代码示意:
class MealViewModel : BaseViewModel() {
fun init() {
viewModelScope.launch {
// 请求数据
...
}
}
}
class MealFragment : Fragment() {
val viewModel : MealViewModel by viewModels()
onverried fun onViewCreated() {
viewmodel.init()
}
}
现在是不是已经看出问题来了。
现象是多次点击【下一周】按钮 添加几个fragment,只要超出了设置的离屏缓存数量,往回滑,之前显示过的fragment会重新加载,因为viewpager移除了fragment,不过重新经过onViewcreated 周期的时候,viewmodel里的 viewmodelScope 不再执行,导致页面空白
viewModelScope.launch {
// 不再执行了
...
}
显然,这个协程scope被cancel(close)了,不过没有被从ViewModel的map中移除,
所以也就谈不上重建。
扫一眼ViewModel 源码,内部有个
Map<String, Object> mBagOfTags = new HashMap<>();
用来存储scope
出现这种问题大概权当使用不当吧,虽然我这么用viewpager已经很久了
一些方案
方案1
viewmodel里的viewModelScope 被取消但不被移除,那就暂且不用这个了,替换为GlobalScope 总能用吧
stackoverflow上有一个同样和我一知半解的老外提了同样的问题
未测试
不过这种方案应该没人会采用,globalScope 估计只用于demo测试中
方案2
不自己缓存fragmentlist了,只缓存数据,每次去从viewpager缓存中获取
class MealViewPagerAdapter(fm: FragmentManager?, life : Lifecycle) : FragmentStateAdapter(fm!! , life) {
val startDateTimeList = mutableListOf<Long>()
fun addItem(startDate : Long) {
startDateTimeList.add(startDate)
notifyItemInserted(itemCount - 1)
}
override fun getItemCount(): Int = startDateTimeList.size
override fun createFragment(position: Int): MealItemFragment = MealItemFragment.newInstance(startDateTimeList[position])
}
测试ok
方案3
不使用viewmodelScope, 用fragment的Scope代替
fun launchLifecycle(lifecycleOwner : LifecycleOwner , block: suspend () -> Unit) {
lifecycleOwner.lifecycleScope.launch {
block()
}
}
测试ok
方案4
谷歌官方的示例项目iosched中是这样写的
viewpager2中同样使用list 缓存fragment,但不是缓存实例,只缓存生成方法,createFragment(position: Int) 方法调用的时候调用闭包来获取fragment(invoke)。
/**
* Adapter that builds a page for each info screen.
*/
inner class InfoAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
override fun createFragment(position: Int) = INFO_PAGES[position].invoke()
override fun getItemCount() = INFO_PAGES.size
}
companion object {
private val INFO_TITLES = arrayOf(
R.string.event_title,
R.string.travel_title,
R.string.faq_title
)
private val INFO_PAGES = arrayOf(
{ EventFragment() },
{ TravelFragment() },
{ FaqFragment() }
// TODO: Track the InfoPage performance b/130335745
)
}
尽管他只有三个fragment,不过测了下自己的场景,同样能解决问题
测试ok
代码位于项目中的InfoFragment类里
方案5
上边的stackOverflow方法中还提到用共享viewmodel的方式,大致应该是这样
private val viewModel: SearchViewModel by viewModels(
ownerProducer = { requireParentFragment() }
)
让viewmodel去伴随父fragment的周期,不过感觉设计上不太合适,没有去测
链接:https://juejin.cn/post/7064185160299708446
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Jetpack架构演变(一):初步使用flow,附加经典案例
对于初学者来说使用lieveData的好处是足够简单和相对安全
引入flow主要因为以下几点:
- 具有更友好的API,学习成本较低
- 跟Kotlin协程、LiveData结合更紧密,Flow能够转换成LiveData,在ViewModel中直接使用
- 结合协程的作用域,当协程被取消时,Flow也会被取消,避免内存泄漏
- flow库隶属于kotlin, livedata属于Android, 拜托Android平台的限制对于未来跨平台发展有利
【flow是个冷数据流】
所谓冷流,即下游无消费行为时,上游不会产生数据,只有下游开始消费,上游才开始产生数据。
而所谓热流,即无论下游是否有消费行为,上游都会自己产生数据。
下边通过一个经典场景详细描述下flow(单纯的flow,而stateFlow会在后续章节中讲解)的使用
案例:一个菜谱应用app中,我想在一个页面展示一个列表(recyclerview) ,此列表的每个item是个子列表,子列表依次为
计划菜谱列表;
收藏菜谱列表;
根据食材筛选的菜谱列表;
根据食材获取用户偏好的菜谱列表;
如图
四个子列表需要四个接口来获取,组装好后来刷新最后的列表
其中每个列表都有可能是空,是emptylist的话这行就不显示了,因为四个接口数据量大小不同,所以不会同一时间返回,同时又要保障这四个子列表按要求的顺序来展示。
思路:
设计数据结构,最外层的data:
data class ContainerData(val title : String , val list: List<Recipe>)
其中Recipe实体是每个菜谱
data class Recipe(val id: String,
val name: String,
val cover: String,
val type: Int,
val ingredients: List<String>? = mutableListOf(),
val minutes: Int,
val pantryItemCount : Int )
模拟四个请求为:
val plannlist = Request.getPlannlist()
val favouritelist= Request.getFavouritelist()
... 以此类推
如果按照要求四个请求返回次序不同,同时要求在列表中按顺序显示,如果实现?
方案一:可以等待四个请求都返回后然后组装数据,刷新列表
可以利用协程的await方法:
val dataList = MutableLiveData<List<Constainer>>()
viewModelScope.launch {
// planner
val plannerDefer = async { Request.getPlannlist() }
// favourite
val favouriteDefer = async { Request.getFavouritelist() }
val plannerData = plannerDefer.await()
val favouriteData = favouriteDefer.await()
....省略后两个接口
val list = listof(
Container("planner" , plannerData),
Container("favourite" , favouriteData),
...
)
dataList.postValue(list)
}
await() 方法是挂起协程,四个接口异步请求(非顺序),等最后一个数据请求返回后才会执行下边的步骤
然后组装数据利用liveData发送,在view中渲染
viewModel.dataList.observe(viewLifecycleOwner) {
mAdapter.submitList(it)
}
此种方式简单,并且有效解决了按顺序排列四个列表的需求,缺点是体验差,假如有一个接口极慢,其他几个就会等待它,用户看着loading一直发呆么。
方案二:接口间不再互相等待,哪个接口先回来就渲染哪个,问题就是如何保障顺序?
有的同学会有方案:先定制一个空数据list
val list = listOf(
Container("planner", emptylist()),
Container("favourite", emptylist()),
...
)
然后先用adapter去渲染list,哪个接口回来就去之前的列表查找替换,然后adapter刷新对应的数据,当然可以,不过会产生一部分逻辑胶水代码,查找遍历的操作。
此时我们可以借助flow来实现了
1 构造一个planner数据流
val plannerFlow = flow {
val plannList = Request.getPlanlist()
emit(ContainerData("Planner", plannList))
}.onStart {
emit(ContainerData("", emptylist()))
}
注意是个val 变量, 不要写成 fun plannerFlow() 方法,不然每次调用开辟新栈的时候新建个flow,并且会一直保存在内存中,直到协程取消
其中onStart 会在发送正式数据之前发送,作为预加载。
然后我们就可以构造正式请求了
viewModelScope.launch {
combine(plannerFlow , favouriteFlow , xxxFlow ,xxxFlow) { planner , favourites , xxx , xxx ->
mutableListOf(planner , favourites , xxx,xxx)
}.collect {
datalist.postValue(it)
}
}
combine 的官方注释为
Returns a Flow whose values are generated with transform function by combining the most recently emitted values by each flow.
combine操作符可以连接两个不同的Flow , 一旦产生数据就会触发组合后的flow的流动,同时它是有序的。
后续章节继续讲述flow其他特性,并彻底弃用liveData。
链接:https://juejin.cn/post/7054053306158563359
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
三十岁前端的破冰尝试
大多数人没有意识到精力的有限而盲目学习,从没有停下来认真咀嚼已有的东西。
本人简介
JavaScrip码农,今年三十,宿主是非互联网类型的外企,提供的内存虽然不大,但也基本够存活。
工作之余,我的主题就是咸鱼。但或许是我的咸度不够,最近开始腐烂了,尤其是夜深人静,主要的信息输入被关闭之后,我就感觉内在的信息流在脑海里乱窜,各种健康指数开始飙升。就像是一台老旧的电脑,非要带最新的显卡游戏,发出嘤嘤嘤的EMO声,最后在卡死在昏睡页面。
大多时候醒来会一切安好,像是被删去了前一晚的日志。但有时也会存有一些没删除干净的缓存,它们就像是病毒,随着第二天的重启复苏。我会感到无比的寒冷,冷到我哪怕是饥饿也不敢出门,只有戴上口罩会给我一丝丝的勇气。
这种寒冷会刺激着我无病呻吟,我会感到惊恐和害怕,害怕某天被宿主的回收机制发现这里的不正常,然后被文明的光辉抹除,就如新冠背后那鲜红的死亡人数一样。
或许是幼年求学寄人篱下时烙下的病根,但那时候心田干涸了还可以哭泣。如今呢,心田之上早已是白雪皑皑。
这些年也有人帮助过我,我也努力挣扎过,但大多时候毫无章法,不仅伤了别人的心,也盲目地消耗着心中的热血,愧疚与自责的泪水最终只是让冰层越积越深。
今天也不知哪根筋抽抽了,想着破冰。
嗯,就是字面上的意思,满脑子都是“破冰”二字……
破冰项目
发表这个稿子算是破冰的第一步~
项目的组织架构初步定为凌凌漆,敏捷周期为一周,其中周日进行复盘和制定新计划,其余作为执行日。由于项目长期且紧迫,年假就不予考虑了,病假可以另算,津贴方面目前只考虑早餐,其他看项目发展情况再做调整。
硬件层面
目前作息相当紊乱,供电稳定性差,从近几年的硬件体验报告可以看出,总体运行还算正常,但小毛病层出不穷,电压不稳是当前主要矛盾。OKR如下:
O:保持一个良好的作息
KR1: 保证每天八小时的睡眠。
KR2:保证每天凌晨前关灯睡下。
KR3:保证每天早上九点前起床。
软件层面
英语是硬伤,其次是底层算法需要重写,不然跑着跑着还是会宕机。
翻译是个不错的路子,但数据源是个头痛的问题……肯定得找和技术相关的东西来翻译,并且可以有反馈。嗯…… 想到可以找掘金里已经有的翻译文章,截取其中一小段来进行快速试错。
至于底层算法的问题,此前在leetcode练过一段时间,但仅停留在已知的变得熟练,未知的依旧不会。
因此我觉得有必要先梳理出关于算法的个人认知的知识体系……
总结下来下一阶段任务:
选择一篇翻译文章,找到其原文,选其中完整的一段进行翻译。
根据当前认知画个关于算法的思维导图。
下周日会出这周的运行报告以及新一期的计划表。
最后随想
若是觉得我这样的尝试也想试一试,欢迎在评论附上自己的链接,一起尝试,相互借鉴,共同进步~
作者:行僧
来源:juejin.cn/post/7152143987225133086
小城市的程序员该如何生存
前言
Hello,这里是百里, 一个无所事事的老年程序员.
随便写写,感慨一下.现今社会越来越畸形,以前打仗农村包围城市,现在经济也农村包围城市.一方面享受的交通,经济娱乐的便利,一方面又感慨,大城市何处是家. 今天讲讲我一个半路出身程序员的想法,以及将来我该如何或者我想如何.
半路出身转程序
普通二本,机械专业,直接进了校企和做的国家投资单位,做一名优秀的流水线工人.没错干了1年多真就流水线,我负责QA品质检查,检查玻璃质量如何,有没有损坏异色,干了1年多.工资5500一个月,每天9小时 ,单休.我当时还觉得我挺高兴的.直到发现招工时候,高中毕业的人也和我干一样的活,还是我领导,比我进来还晚.ε=(´ο`)))唉ε=(´ο`)))唉 .
18年裸辞,在家自己学了一下程序,最开始学的是java 学了3个多月,面了一家医疗企业,但是没让我做开发,让我做运维实施.因为有些编程基础,平时可以自己改改.工资其实也不错,在房价1.3w的地方能开到1.2w一个月. 缺点么.. 我离职的时候还有176天的假期没修完. 基本上无休.我干了两年.
20年.刷抖音时候看了python 怎么怎么好 ,一咬牙一跺脚,花了3w多培训了python ,当初讲的多好多好, 但是,但是,这工作只能在大城市,我们这小地方 ,最好找工作的依然是php 和java ,python 一个都没有.至今还记得那个培训机构叫做 某男孩. 76个人进去的14个人毕业, 还说毕业率100% ,呵呵呵 骗子企业.
再后来凭借着会一些sql ,在某传统企业,做erp 二开, 基于delphi, 一直干到现在.
大城市就业机会多VS 小城市生活惬意
现今很多人不结婚,晚婚,多半是因为大城市生活节奏快,或者说结婚了没有物质基础,结婚了以后孩子怎么办,自己本身很痛苦了,让孩子更痛苦?
我是23岁结的婚,老婆是大学同学,大学谈了4年,当初也想过去大城市去打拼,因为同样的工作甚至更简单的工作工资就比我熬夜加班高的多. 但是我退缩了.传统农村人思想罢了.想回到家老婆孩子热炕头,小地方两个人赚一个月工资也够活的.
我有很多朋友在北京大厂,一年20w ,30w 的 工作 ,做的跟我相同的工作. 其实真的很羡慕,一年顶我2年的工作.也不是没想过去北上广深,但是我受不了孤独,哈哈矫情罢了..抛弃不了孩子老婆.
我们自己有一片菜地,还有个小院子,会自己种菜,还养了鸡.家门口有小河 , 偶尔还跟岳父抓抓鱼,真就码农.
讲讲技术栈
到现在入门程序已经快3年了.看到掘金中各种大佬说的东西讲道理,,完全看不懂,也许是年纪大了,(马上27),不知道学什么好,我的想法就是这辈子我不打算去大城市,就小城小桥流水活着 ,但是老技术不能吃一辈子, delphi 的工作讲道理我感觉做不久, 好多同学甚至不知道这个语言干嘛的. 本身技术栈.
python ,花了3w培训的,简单的没什么问题,不过好久没用了.
delphi,不能说精通,但是基本干活没啥问题.curd 没问题.天天用.
VUE2,3 ,偶尔做做bi,没事自己学的,买的课,但是也就是学了而已,学完了就忘了, 因为用不到. 而且也不深,因为看所谓的面试题,基本上不会,我一度认为我学的是假的东西 ,还去找人家退款.
SQL/kattle 算不上精通, 属于干活没问题情况, 因为delphi 是基于sql 存储过程的语言,动不动sql 写上万行... 那种 . 至于kattle 则是偶尔取数,做bi使用 ,还是停留在 能用会用, 问我就挂那种情况 .
帆软/数据分析 : 公司花钱买了帆软的8000 的课, 考试我是都考过了,然后 Bi 还是拿vue 做. 小程序 拿 uniapp 做. 也不知道为啥花钱买这个, 我兴师动众的学了3个多月基本上都会做,但是还是那句话 ,用不到,现在也就是学过了而已.
SAP 今年公司新近的业务, 讲道理 据说这个工资很高,而且很吃香, 现在ABAP 自己学了几个月了,已经能入手一些业务,不知道将来的发展如何. 继续用着吧.
未来及方向
年纪越来越大了,响应国家政策,现在努力二胎,又是一笔开销.
越活越迷茫,我该做什么,我该学什么 ,当前领导总是让我看了很多什么什么做人,怎么怎么演讲的书,美名其曰成长,但是我觉得还是东西学到手了才是真的.
打算扎根制造业,对于erp ,mes ,aps 等业务流程还是很熟悉的, 感觉制造业都用的东西还是可以的. 打算学sap,数据分析,BI方向吧. 也不知道方向对不对.
以上随便写写,27了还迷茫不知道是不是因为半路转行的缘故.
后续
三百六十行,行行转IT,感觉现在IT 这碗水早晚要洒,只是年头问题.当然如果非常牛逼的人除外. 但是人如果区分家庭和事业哪个更重要,也不好分辨,各有各的道理.
认识一个以前在群里的大佬.34岁没结婚,没孩子,死了,技术贼牛逼.也认识啥都不会但是光靠说也能拿几十万的人.钱难赚,钱又好赚. ε=(´ο`*)))唉 . 行了 写完继续摸鱼, 写写技术笔记吧.
不知道有没有在夜深人静的时候想过,我将来怎么办,这种可笑的话题.
作者:百里落云
来源:juejin.cn/post/7140887445632974884
收起阅读 »我与 Groovy 不共戴天
来到新公司后,小灵通开始接手了核心技术-快编插件,看到传说中的核心技术,小灵通傻眼了,啊这,groovy 写的插件,groovy 认真的嘛,2202 年了,插件咋还用 groovy 写呢,我新手写插件也换 kotlin 了,张嘴就是 这辈子都不可能写 groovy,甭想了。 但是嘛,工作不寒碜,学学呗。
一开始和组里几个大佬聊下来,磨刀霍霍准备对历史代码动刀,全迁移到 kotlin 上爽一发,但发现。。。咦,代码好像看不懂诶,我不知道 kt 对应的写法是啥样的。文章结束,小灵通因此被辞退。
开个玩笑,我现在还是在岗状态。工作还是要继续的。既然能力有限我全部迁不过去,那我可以做到新需求用 kotlin 来写嘛,咦,这就有意思了。
Groovy 和 java 以及 kotlin 如何混编
怎么实现混编
我不会嘛,看看官方怎么写的。gradle 源码有这么段代码来阐释了是怎么优先 groovy 编译 而非 java 编译.
// tag::compile-task-classpath[]
tasks.named('compileGroovy') {
// Groovy only needs the declared dependencies
// (and not longer the output of compileJava)
classpath = sourceSets.main.compileClasspath
}
tasks.named('compileJava') {
// Java also depends on the result of Groovy compilation
// (which automatically makes it depend of compileGroovy)
classpath += files(sourceSets.main.groovy.classesDirectory)
}
// end::compile-task-classpath[]
噢,可以这么写啊,那我是不是抄下就可以了,把名字改改。我就可以写 kotlin 了,欧耶!
compileKotlin {
classpath = sourceSets.main.compileClasspath
}
compileGroovy {
classpath += files(sourceSets.main.kotlin.classesDirectory)
}
跑一发,没有意外的话,你会看到这个报错。
诶,为啥我照着抄就跑不起来呢?我怀疑是 kotlin classesDiretory 有问题,断点看一波 compileGroovy 这个 task 的 sourceSets.main.kotlin.classesDirectory 是个啥。大概长这样, 是个 DefaultDirectoryVar 类。
诶,这是个啥,一开始我也看不太懂,觉得这里的 value 是 undefined 怪怪的,也不确定,那我看看其他正常的 classesDirectory 是啥
其实到这里可以确定应该是 kotlin 的 classDirectory 在此时是不可用的状态,印证下自己猜想,尝试添加 catch 的断点,确实是这样
具体为啥此时还不可用,我没有更详细的深入了,有大佬知道的,可以不吝赐教下。
SO 搜了一波解答,看到一篇靠谱的回复 compile-groovy-and-kotlin.
compileGroovy.dependsOn compileKotlin
compileGroovy.classpath += files(compileKotlin.destinationDir)
复制代码
试了一下确实是可以的,但为啥这样可以了呢?以及最上面官方的代码是啥意思呢?还有一些奇奇怪怪的名词是啥,下面吹一下
关于 souceset
我们入门写 android 时,都看到 / 写过类似这样的代码
sourceSets {
main.java.srcDirs = ['src/java']
}
我对他的理解是指定 main sourceset 下的 java 的源码目录。 SourceSets 是一个 Sourset 的容器用来创建一个个的 SourceSet, 比如 main, test. 而 main 下的 java, groovy, kotlin 目录是一个编译目录(SourceDirectorySet),编译实质是找到一个个的编译目录,然后将他们变成 .class 文件放在 build/classes/sourceDirectorySet
下面, 也就是 destinationDirectory。
像 main 对应的是 SourceSet 接口,其实现是 DefaultSourceSet。而 main 下面的 groovy, java, kotlin 是 SourceDirectorySet 接口,其实现是 DefaultSourceDirectorySet。
官方 gradle 对于 sourceset 的定义是:
the source files and where they’re located 定位源码的位置
the compilation classpath, including any required dependencies (via Gradle configurations) 编译时的 class path
where the compiled class files are placed 编译出的 class 放在哪
输入文件 + 编译时 classpath 经过 AbstractCompile Task 得到 输出的 class 目录
第二个 编译时的 classpath,在项目里也见过,sourceSetImplementation 声明 sourceSet 的依赖。第三个我很少见到,印象不深,SourceDirectorySet#destinationDirectory 用来指定 compile task 的输出目录。而 SourceDirectorySet#classesDirectory 和这个值是一致的。再重申一遍这里的 SourceDirectorySet 想成是 DSL 里写的 java, groovy,kt 就好了。
官方文档对于 classesDirectory 的描述是
The directory property that is bound to the task that produces the output via
SourceDirectorySet.compiledBy(org.gradle.api.tasks.TaskProvider, java.util.function.Function)
. Use this as part of a classpath or input to another task to ensure that the output is created before it is used. Note: To define the path of the output folder useSourceDirectorySet.getDestinationDirectory()
大意是 classesDirectory 与这个 compile task 的输出是相关联的,具体是通过 SourceDirectorySet.compiledBy() 方法,这个字段由 destinationDirectory 字段决定。查看 DefaultSourceDirectorySet#compiledBy 方法
public <T extends Task> void compiledBy(TaskProvider<T> taskProvider, Function<T, DirectoryProperty> mapping) {
this.compileTaskProvider = taskProvider;
taskProvider.configure(task -> {
if (taskProvider == this.compileTaskProvider) {
mapping.apply(task).set(destinationDirectory);
}
});
classesDirectory.set(taskProvider.flatMap(mapping::apply));
}
雀食语义上 classesDirectory == destinationDirectory。
现在我们可以去理解下 官方的 demo 了,官方的 demo 简单说就是优先执行 Compile Groovy task, 再去执行 Compile Java task.
tasks.named('compileGroovy') {
classpath = sourceSets.main.compileClasspath // 1
}
tasks.named('compileJava') {
classpath += files(sourceSets.main.groovy.classesDirectory) // 2
}
可能看不懂的地方是 1,2 注释处做了啥, 1 处我问了我们组大佬,这是重置了 compileGroovy task 的 classpath 使其不依赖 compile java classpath,在 GroovyPlugin 源码中有那么一句代码
classpath.from((Callable<Object>) () -> sourceSet.getCompileClasspath().plus(target.files(sourceSet.getJava().getClassesDirectory())));
可以看到 GroovyPlugin 其实是依赖于 java 的 classpath 的。这里我们需要改变 groovy 和 java 的编译时序需要把这层依赖断开。
2呢,使 compileJava 依赖上 compileGroovy 的 output property,间接使 compileJava dependson compileGroovy 任务。
具体为啥 Kotlin 的不行,俺还没搞清楚,知道的大佬可以指教下。
而 SO 上的这个答复其实也是类似的,而且更直接
compileGroovy.dependsOn compileKotlin
compileGroovy.classpath += files(compileKotlin.destinationDir)
使 compileGroovy 依赖于 compileKotlin 任务,再让 compileGroovy 的 classPath 添加上 compileKotlin 的 output. 既然任务的 classPath 添加 另一个任务的 output 会自动依赖上另一个 task。那其实这么写也是可以的
compileGroovy.classpath += files(compileKotlin.destinationDir)
实验了下雀食是可以跑的. 那既然 Groovy 和 Java 都包含 main 的 classpath,是不是 compileKotlin 的 classpath 置为 main,那 compileGroovy 会自动依赖上 compileKotlin。试试呗
compileKotlin.classpath = sourceSets.main.compileClasspath
可以看到 kotlin 的执行顺序雀食跑到了最前面。
在项目实操中,我发现 Kotlin 跑在了 compile 的最前面,那其实 kotlin 的类里面是不能依赖 java 或者 groovy 的任何依赖的。这也符合预期,不然就会出现依赖成环,报 Circular dependsOn hierarchy found in the Kotlin source sets 错误。我个人观点这是一种对历史代码改造的折衷,在新需求上使用 kotlin 进行开发,一些功能相同的工具类能翻译成 kt 就翻译,不能就重写一套。
小结
在这节讲了两种实现混编的方案。写法不同,本质都是使一个任务依赖另一个任务的 output
// 1
compileGroovy.classpath += files(compileKotlin.destinationDir)
// 2
compileKotlin.classpath = sourceSets.main.compileClasspath
我对于 SourceSet 和 SourceDirectorySet 的理解
项目中实践混编方案的现状
Groovy 有趣的语法糖
在写 Groovy 的过程中,我遇到一个头大的问题,代码看不懂,里面有一些奇奇怪怪没见过的语法糖,乍一看就懵了,你要不一起瞅瞅。
includes*.tasks
我司的仓库是大仓的结构,仓库和子仓之间是通过 Composite build 构建联系的。那么怎么使主仓的 task 触发 includeBuild 的仓库执行对应仓库呢?是通过这行代码实现的
tasks.register('publishDeps') {
dependsOn gradle.includedBuilds*.task(':publishIvyPublicationToIvyRepository')
}
这里的 includeBuilds*.task 后面的 *.task 是啥?includeBuilds 看源码发现是个 List。我不懂 groovy,但好歹我能看懂 kotlin, 我看看官方文档右边对应的 kt 写法是啥?
tasks.register("publishDeps") {
dependsOn(gradle.includedBuilds.map { it.task(":publishMavenPublicationToMavenRepository") })
}
咦嘿,原来是个 List 的 map 操作,骚里骚气的。翻了翻原来是个 groovy 的语法糖,写个代码试试看看他编译到 class 是啥样子
def list = ["1", "22", "333"]
def lengths = list*.size()
lengths.forEach{
println it
}
编译成 class
Object list = ScriptBytecodeAdapter.createList(new Object[]{"1", "22", "333"});
Object lengths = ScriptBytecodeAdapter.invokeMethod0SpreadSafe(Groovy.class, list, (String)"size");
var1[0].call(lengths, new Groovy._closure1(this, this));
在 ScriptBytecodeAdapter.invokeMethod0SpreadSafe 实现内部其实还是新建了一个 List 再逐个对 List 中元素进行 map.
String.execute
这是执行一个 shell 指令,比如 "ls -al".execute(), 刚看到这个的时候认为这个东西类似 kotlin 的扩展函数,点进去看实现发现不一样
public static Process execute(final String self) throws IOException {
return Runtime.getRuntime().exec(self);
}
可以看到 receiver 是他的第一个参数,莫非这是通用的语法糖,我试试写了个
public static String deco(final String self) throws IOException {
return self + "deco"
}
// println "".deco()
运行下,哦吼,跑不了,报了 MissingMethodException。看样子是不通用的。翻了翻 groovy 文档,找到了这个文档
Static methods are used with the first parameter being the destination class, i.e.
public static String reverse(String self)
provides areverse()
method forString
.
看样子这个语法糖是 groovy 内部定制的,我不清楚有没有支持开发定制的方式,知道的大佬可以评论区留言下。
Range 怎么写
groovy 也有类似 kotlin 的 Range 的概念,包含的 Range 是 ..
, 不包含右边界(until)的是 ..<
Try with resources
我遇到过一个 OKHttp 连接泄露的问题,代码原型大概是这样
if (xxx) {
response.close()
} else {
// behavior
}
定位到是 Response 没有在 else 的分支上进行 close,当然可以简单在 else 分支上进行 close, 并在外层补上 try, catch 兜底,但在 Effective Java
一书提及针对资源关闭 try-with-resource 优于 try cactch。但我尝试像 java 一样写 try-with-resource,发现嗝屁了,直接报红,我去 SO 上搜了一波 groovy 的 try-with-resource. Groovy 是通过 withCloseable 扩展来实现,看这个方法的声明与 Process#execute 语法糖类似—public static def withCloseable(Closeable self, Closure action) . 最终改造后的代码是这样的
Response.withCloseable { reponse ->
if (xxx) {
} else {
}
}
<<
这个是 groovy 中的左移运算符也是可以重载的,而 kotlin 是不支持的。他运用比较多的场景。起初我印象中 Task 的 是覆写了这个运算符作为 doLast 简易写法,现在 gradle7.X 的版本上是没有了。其它常见的是文件写入操作, 列表添加元素。
def file = new File("xxx")
file << "text"
def list = []
list << "aaa"
Groovy 的一家之言
如果 kotlin 是 better java, 那么 groovy 应该是 more than java,它的定位更加偏向脚本一些,更加动态化(从它反编译的字节码可见一斑),上手曲线较高,但一个人精通这个语言,并且独立维护一个项目,其实 groovy 的开发效率并不会比 kotlin 和 java 差,感受比较深切的是 maven publish 的例子,看看插件中 groovy 和 kotlin 的写法上的不同。
// Groovy
def mavenSettings = {
groupId 'org.gradle.sample'
artifactId 'library'
version '1.1'
}
def repSettings = {
repositories {
maven {
url = mavenUrl
}
}
}
afterEvaluate {
publishing {
publications {
maven(MavenPublication) {
ConfigureUtil.configure(mavenSettings, it)
from components.java
}
}
ConfigureUtil.configure(repoSettings, it)
}
def publication = publishing.publications.'maven' as MavenPublication
publication.pom.withXml {
// inject msg
}
}
// Kotlin
// Codes are borrowed from (sonatype-publish-plugin)[https://github.com/johnsonlee/sonatype-publish-plugin/]
fun Project.publishing(
config: PublishingExtension.() -> Unit
) = extensions.configure(PublishingExtension::class.java, config)
val Project.publishing: PublishingExtension
get() = extensions.getByType(PublishingExtension::class.java)
val mavenClosure = closureOf<MavenPublication> {
groupId = "org.gradle.sample"
artifactId = "library"
version = "1.1"
}
val repClosure = closureOf<PublishingExtension> {
repositories {
maven {
url = mavenUrl
}
}
}
afterEvaluate {
publishing {
publications {
create<MavenPublication>("maven") {
ConfigureUtil.configure(mavenClosure, this)
from(components["java"])
}
}
ConfigureUtil.configure(repoClosure, this)
}
val publication = publishing.publications["maven"] as MavenPublication
publication.pom.withXml {
// inject msg
}
}
我觉得吧,如果像我们大佬擅长 groovy 的话,而且是一个人开发的商业项目,插件里的确写 groovy 会更快,更简洁,那为什么不呢?这对他来说是种善,语言没有优劣,动态性和静态语言优劣我不想较高下,这因人而异。
我选择 kotlin 是俺不擅长写 groovy 啊,我写了几个月 groovy 每次改动插件发布后再应用第一次都会有语法错误,调试的头皮发麻,所以最后搞了个折衷方案,新代码用 kotlin, 旧代码用 groovy 继续写。而且参考了 KOGE@2BAB 文档,发现咦,gradle 正面回应过 groovy 与 kotlin 之争. "Prefer using a statically-typed language to implement a plugin"@Gradle。嗯, 我还是继续写 Kotlin 吧。
作者:小灵通
来源:juejin.cn/post/7084949825866694686
收起阅读 »面试突击90:过滤器和拦截器有什么区别?
实现过滤器和拦截器
首先,我们先来看一下二者在 Spring Boot 项目中的具体实现,这对后续理解二者的区别有很大的帮助。
a) 实现过滤器
过滤器可以使用 Servlet 3.0 提供的 @WebFilter 注解,配置过滤的 URL 规则,然后再实现 Filter 接口,重写接口中的 doFilter 方法,具体实现代码如下:
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
@Component
@WebFilter(urlPatterns = "/*")
public class TestFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("过滤器:执行 init 方法。");
}
@Override
public void doFilter(ServletRequest servletRequest,
ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
System.out.println("过滤器:开始执行 doFilter 方法。");
// 请求放行
filterChain.doFilter(servletRequest, servletResponse);
System.out.println("过滤器:结束执行 doFilter 方法。");
}
@Override
public void destroy() {
System.out.println("过滤器:执行 destroy 方法。");
}
}
其中:
void init(FilterConfig filterConfig):容器启动(初始化 Filter)时会被调用,整个程序运行期只会被调用一次。用于实现 Filter 对象的初始化。
void doFilter(ServletRequest request, ServletResponse response,FilterChain chain):具体的过滤功能实现代码,通过此方法对请求进行过滤处理,其中 FilterChain 参数是用来调用下一个过滤器或执行下一个流程。
void destroy():用于 Filter 销毁前完成相关资源的回收工作。
b) 实现拦截器
拦截器的实现分为两步,第一步,创建一个普通的拦截器,实现 HandlerInterceptor 接口,并重写接口中的相关方法;第二步,将上一步创建的拦截器加入到 Spring Boot 的配置文件中。
接下来,先创建一个普通拦截器,实现 HandlerInterceptor 接口并重写 preHandle/postHandle/afterCompletion 方法,具体实现代码如下:
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class TestInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("拦截器:执行 preHandle 方法。");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("拦截器:执行 postHandle 方法。");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("拦截器:执行 afterCompletion 方法。");
}
}
其中:
- boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handle):在请求方法执行前被调用,也就是调用目标方法之前被调用。比如我们在操作数据之前先要验证用户的登录信息,就可以在此方法中实现,如果验证成功则返回 true,继续执行数据操作业务;否则就返回 false,后续操作数据的业务就不会被执行了。
- void postHandle(HttpServletRequest request, HttpServletResponse response, Object handle, ModelAndView modelAndView):调用请求方法之后执行,但它会在 DispatcherServlet 进行渲染视图之前被执行。
- void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handle, Exception ex):会在整个请求结束之后再执行,也就是在 DispatcherServlet 渲染了对应的视图之后再执行。
最后,我们再将上面的拦截器注入到项目配置文件中,并设置相应拦截规则,具体实现代码如下:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class AppConfig implements WebMvcConfigurer {
// 注入拦截器
@Autowired
private TestInterceptor testInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(testInterceptor) // 添加拦截器
.addPathPatterns("/*"); // 拦截所有地址
}
}
了解了二者的使用之后,接下来我们来看二者的区别。
过滤器 VS 拦截器
过滤器和拦截器的区别主要体现在以下 5 点:
- 出身不同;
- 触发时机不同;
- 实现不同;
- 支持的项目类型不同;
- 使用的场景不同。
接下来,我们一一来看。
1.出身不同
过滤器来自于 Servlet,而拦截器来自于 Spring 框架,从上面代码中我们也可以看出,过滤器在实现时导入的是 Servlet 相关的包,如下图所示:
而拦截器在实现时,导入的是 Spring 相关的包,如下图所示:
2.触发时机不同
请求的执行顺序是:请求进入容器 > 进入过滤器 > 进入 Servlet > 进入拦截器 > 执行控制器(Controller),如下图所示:
所以过滤器和拦截器的执行时机也是不同的,过滤器会先执行,然后才会执行拦截器,最后才会进入真正的要调用的方法。
3.实现不同
过滤器是基于方法回调实现的,我们在上面实现过滤器的时候就会发现,当我们要执行下一个过滤器或下一个流程时,需要调用 FilterChain 对象的 doFilter 方法进行回调执行,如下图所示:
由此可以看出,过滤器的实现是基于方法回调的。
而拦截器是基于动态代理(底层是反射)实现的,它的实现如下图所示:
代理调用的效果如下图所示:
4.支持的项目类型不同
过滤器是 Servlet 规范中定义的,所以过滤器要依赖 Servlet 容器,它只能用在 Web 项目中;而拦截器是 Spring 中的一个组件,因此拦截器既可以用在 Web 项目中,同时还可以用在 Application 或 Swing 程序中。
5.使用的场景不同
因为拦截器更接近业务系统,所以拦截器主要用来实现项目中的业务判断的,比如:登录判断、权限判断、日志记录等业务。
而过滤器通常是用来实现通用功能过滤的,比如:敏感词过滤、字符集编码设置、响应数据压缩等功能。
本文项目源码下载
总结
过滤器和拦截器都是基于 AOP 思想实现的,用来处理某个统一的功能的,但二者又有 5 点不同:出身不同、触发时机不同、实现不同、支持的项目类型不同以及使用的场景不同。过滤器通常是用来进行全局过滤的,而拦截器是用来实现某项业务拦截的。
链接:https://juejin.cn/post/7155069405993369631
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Flutter 组件集录 | 新一代 Button 按钮参上
0. 按钮一族现状
随着 Flutter 3.3
的发布,RaisedButton
组件从 Flutter
框架中移除,曾为界面开疆拓土的 按钮三兄弟
彻底成为历史。
另外 MaterialButton
、RawMaterialButton
也将在未来计划被废弃,所以不建议大家再使用了:
目前,取而代之的是 TextButton
、ElevatedButton
、 OutlinedButton
三个按钮组件,本文将重点介绍这三者的使用方式。
另外,一些简单的按钮封装组件仍可使用:
CupertinoButton : iOS 风格按钮
CupertinoNavigationBarBackButton : iOS 导航栏返回按钮
BackButton : 返回按钮
IconButton : 图标按钮
CloseButton : 关闭按钮
FloatingActionButton : 浮动按钮
还有一些 多按钮
集成的组件,将在后续文章中详细介绍:
CupertinoSegmentedControl
CupertinoSlidingSegmentedControl
ButtonBar
DropdownButton
ToggleButtons
1. 三个按钮组件的默认表现
如下,是 ElevatedButton
的默认表现:有圆角和阴影,在点击时有水波纹。构造时必须传入点击回调函数onPressed
和子组件 child
:
ElevatedButton(
onPressed: () {},
child: Text('ElevatedButton'),
),
如下,是 OutlinedButton
的默认表现:有圆角和外边线,内部无填充,在点击时有水波纹。构造时必须传入点击回调函数onPressed
和子组件 child
:
OutlinedButton(
onPressed: () {},
child: Text('OutlinedButton'),
);
如下,是 TextButton
的默认表现:无边线,无填充,在点击时有水波纹。构造时必须传入点击回调函数onPressed
和子组件 child
:
TextButton(
onPressed: () {},
child: Text('TextButton'),
);
2. 按钮样式的更改
如果稍微翻一下源码就可以看到,这三个按钮本质上是一样的,都是 ButtonStyleButton
的衍生类。只不过他们的默认样式 ButtonStyle
不同而已:
如下所示,在 ButtonStyleButton
类中队列两个抽象方法,需要子类去实现,返回默认按钮样式:
拿下面的 ElevatedButton
组件来说,它需要实现 defaultStyleOf
方法来返回默认主题。在未使用 Material3
时,通过 styleFrom
静态方法根据主题进行相关属性设置:比如各种颜色、阴影、文字样式、边距、形状等。
所以,需要修改按钮样式,只要提供 style
属性设置即可:该属性类型为 ButtonStyle
,三个按钮组件都提供了 styleFrom
静态方法创建 ButtonStyle
对象,使用如下:
ButtonStyle style = ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
foregroundColor: Colors.white,
elevation: 0,
padding: const EdgeInsets.symmetric(horizontal: 40),
shape: const StadiumBorder(),
side: const BorderSide(color: Colors.black,),
);
ElevatedButton(
onPressed: () {},
child: Text('Login'),
style: style
);
通过指定 shape
可以形状,如下所示,通过 CircleBorder
实现圆形组件:
ButtonStyle style = ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
elevation: 2,
shape: const CircleBorder(),
);
ElevatedButton(
onPressed: () {},
style: style,
child: const Icon(Icons.add)
);
TextButton
、ElevatedButton
、 OutlinedButton
这三个按钮,只是默认主题不同。如果提供相同的配置,OutlinedButton
因为可以实现下面的显示效果。
ButtonStyle style = OutlinedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
elevation: 0,
shape: const CircleBorder(),
side:BorderSide.none
);
OutlinedButton(
onPressed: () {},
style: style,
child: const Icon(Icons.add)
);
常见样式属性:
属性名 | 类型 | 用途 |
---|---|---|
foregroundColor | Color? | 前景色 |
backgroundColor | Color? | 背景色 |
disabledForegroundColor | Color? | 禁用时前景色 |
disabledBackgroundColor | Color? | 禁用时背景色 |
shadowColor | Color? | 阴影色 |
elevation | double? | 阴影深度 |
textStyle | TextStyle? | 文字样式 |
padding | EdgeInsetsGeometry? | 边距 |
side | BorderSide? | 边线 |
shape | OutlinedBorder? | 形状 |
另外,还有一些不常用的属性,了解一下即可:
属性名 | 类型 | 用途 |
---|---|---|
alignment | AlignmentGeometry? | 子组件区域中对齐方式 |
enableFeedback | bool? | 是否启用反馈,如长按震动 |
enabledMouseCursor | MouseCursor? | 桌面端鼠标样式 |
disabledMouseCursor | MouseCursor? | 禁用时桌面端鼠标样式 |
animationDuration | Duration? | 动画时长 |
minimumSize | Size? | 最小尺寸 |
maximumSize | Size? | 最大尺寸 |
fixedSize | Size? | 固定尺寸 |
padding | EdgeInsetsGeometry? | 边距 |
3. 按钮的事件
这三个按钮在构造时都需要传入 onPressed
参数作为点击回调。另外,还有三个回调 onLongPress
用于监听长按事件;onHover
用于监听鼠标悬浮事件;onFocusChange
用于监听焦点变化的事件。
ElevatedButton(
onPressed: () {
print('========Login==========');
},
onHover: (bool value) {
print('=====onHover===$value==========');
},
onLongPress: () {
print('========onLongPress==========');
},
onFocusChange: (bool focus) {
print('=====onFocusChange===$focus==========');
},
child: const Text('Login'),
);
当按钮的 onPressed
和 onLongPress
都为 null
时,按钮会处于 禁用状态
。此时按钮不会响应点击,也没有水波纹效果;另外,按钮的背景色,前景色分别取用 disabledBackgroundColor
和 disabledForegroundColor
属性:
ElevatedButton(
onPressed: null,
style: style,
child: const Text('Login'),
);
4. 按钮的尺寸
在按钮默认样式中,规定了最小尺寸是 Size(64, 36)
, 最大尺寸无限。
也就是说,在父级区域约束的允许范围,按钮的尺寸由 子组件
和 边距
确定的。如下所示,子组件中文字非常大,按钮尺寸会适用文字的大小。
ButtonStyle style = ElevatedButton.styleFrom(
// 略...
padding: const EdgeInsets.symmetric(horizontal: 40,vertical: 10),
);
ElevatedButton(
onPressed: null,
style: style,
child: const Text('Login',style: TextStyle(fontSize: 50),),
);
父级约束
是绝对不能违逆的,在紧约束下,按钮的尺寸会被锁死。如下,通过 SizedBox
为按钮施加一个 200*40
的紧约束:
SizedBox(
width: 200,
height: 40,
child: ElevatedButton(
onPressed: (){},
style: style,
child: const Text('Login'),
),
);
如下,将紧约束宽度设为 10
,可以看出按钮也只能遵循。即使它本身最小尺寸是 Size(64, 36)
,也不能违背父级的约束:
所以,想要修改按钮的尺寸,有两种方式:
- 从
子组件尺寸 边距
入手,调整按钮尺寸。
- 从
- 为按钮施加
紧约束
,锁死按钮尺寸。
- 为按钮施加
5. 简看 ButtonStyleButton 组件的源码实现
首先,ButtonStyleButton
是一个抽象类,其继承自 StatefulWidget
, 说明其需要依赖状态类实现内部的变化。
在 createState
方法中返回 _ButtonStyleState
状态对象,说明按钮构建的逻辑在该状态类中:
@override
State<ButtonStyleButton> createState() => _ButtonStyleState();
直接来看 _ButtonStyleState
中的构造方法,一开始会触发组件的 themeStyleOf
和 defaultStyleOf
抽象方法获取 ButtonStyle
对象。这也就是TextButton
、ElevatedButton
、 OutlinedButton
三者作为实现类需要完成的逻辑。
构建的组件也就是按钮的最终表现,其中使用了 ConstrainedBox
组件处理约束;Material
组件处理基本表现内容;InkWell
处理水波纹和相关事件;Padding
用于处理内边距;Align
处理对齐方式。
使用,总的来看:ButtonStyleButton
组件就是一些常用组件的组合体而已,通过 ButtonStyle
类进行样式配置,来简化构建逻辑。通过封装,简化使用。另外,我们可以通过主题来统一样式,无需一个个进行配置,这个在后面进行介绍。那本文就到这里,谢谢观看 ~
链接:https://juejin.cn/post/7149478456609210375
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
ProtoBuf 基本语法总结,看这一篇就够了
前言
最近项目是采用微服务架构开发的,各服务之间通过gPRC调用,基于ProtoBuf序列化协议进行数据通信,因此接触学习了Protobuf,本文会对Protobuf的语法做下总结,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。
gRPC的调用模型如下:
基本规范
- 文件以.proto做为文件后缀,除结构定义外的语句以分号结尾。
- rpc方法定义结尾的分号可有可无。
- Message命名采用驼峰命名方式,字段命名采用小写字母加下划线分隔方式。
基本语法
首先看一个简单的示例:
/*
头部相关声明
*/
syntax = "proto3"; // 语法版本为protobuf3.0
package user; // 定义包名,可以为.proto文件指定包名,防止消息名冲突。
import "common.proto"; // 导入common.proto
option go_package = ".;proto";
//服务
service User {
rpc SayHello (SayHelloRequest) returns (SayHelloResponse) {}
}
//定义请求消息体
message SayHelloRequest {
string name = 1;
int64 role = 2;
}
//定义响应消息体
message SayHelloResponse {
string message = 1;
}
- .proto文件的第一个非注释行用于指定语法版本,默认为“proto2”;
package定义包
可以为.proto
文件指定包名,防止消息名冲突。
import 导入包
可以通过import
导入其它.proto中定义的消息;常用于导入一些公共的信息。
正常情况下只能使用直接导入的proto文件的定义;如果需要使用多级import导入的文件,import 可以使用 public 属性。示例如下:
a.proto
import public "common.proto"; // 注意此处使用的是import public
import "c.proto";
b.proto
import "a.proto";
在b.proto中可以用common.proto中定义的内容,但是不能用c中的定义的内容。
定义Message
定义message使用“message”关键字,消息的字段声明由4部分构成:字段修饰符 字段类型 字段名称 = 标志号。
格式如下:
message 消息名称 {
[字段修饰符] 字段类型 字段名称 = 标志号;
}
字段修饰符
- singular:默认值,该字段可以出现0次或者1次(不能超过1次);
- repeated:该字段可以重复任意多次(包括0次);
我们可以使用repeated关键字来表示动态数组,示例如下:
message User {
repeated int64 id = 1;
}
在请求的时候我们可以传[]int64{1, 2, 3, 4}
。
字段类型
关于字段类型,这里列举几个常用的,其它的如果有需要可以直接网上搜。
类型 | 备注 |
---|---|
string | 字符串 |
double | 64位浮点型 |
float | 32位浮点型 |
int32、int64 | 整型 |
bool | 布尔型 |
uint32、uint64 | 无符号整型 |
sint32、sint64 | 有符号的整形 |
字段编号
每个字段都有一个编号,这些编号是 唯一的。该编号会用来识别二进制数据中的字段。编号在1-15范围内可以用一个字节编码表示,在16-2047范围用两个字节表示,所以将15以内得编号留给频繁出现的字段可以节省空间。
枚举类型
在定义消息类型时,我们有可能会为某个字段预定义值的一个列表,我们可以通过enum来添加一个枚举,为每个可能的值添加一个常量。示例如下:
message UserRequest {
string name = 1;
// 定义性别枚举
enum Gender {
UNKNOWN = 0;
MAN = 1;
WOMAN = 2;
}
// 定义一个枚举字段
Gender gender = 2;
}
注意:所有枚举定义都需要包含一个常量映射到0并且作为定义的首行。
嵌套类型
嵌套类型,也就是字面意思,在 message 消息体中,又嵌套了其它的 message 消息体,一共有两种模式,如下:
syntax = "proto3";
message UserResponse {
message User {
int64 id = 1;
string name = 2;
}
repeated User users = 1;
}
如果在外部消息之外使用内部消息,则需要使用“outermsg.innermsg”的方式,如,需要在UserResponse外使用User, 则应该使用:
UserResponse.User
Map类型
在返回列表的时候,map类型经常用到,可以使用map关键字可以创建一个映射,语法如:
map<key_type, value_type> map_field = N;
- key_type 只能是整数或字符串,enum不能作为key_type;
- value_type 是除了映射(map)意外的任意类型;
示例:
message User {
int64 id = 1;
string name = 2;
}
map[int64, User] users = 1;
定义Service
如果想在RPC中使用已经定义好的消息类型,可以在.proto文件中定一个消息服务接口,使用service关键字进行服务定义,如:
service User {
rpc SayHello (SayHelloRequest) returns (SayHelloResponse) {}
}
链接:https://juejin.cn/post/7155399858004688926
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Compose 动画艺术探索之灵动岛
说起灵动岛,大家肯定都不陌生,因为这段时间这个东西实在是太火了,这是苹果14中算是最大的更新了😂,不拿缺点当缺点,并且还能在缺点上玩出花,这个产品思路确实厉害👍,不得不服!灵动岛看着效果挺炫,其实实现起来并不是特别复杂,今天带大家一起来使用 Compose
实现下属于安卓的“灵动岛”!废话不多说,先来看下本篇文章实现的效果。
看着还可以吧,哈哈哈,接着往下说!
苹果的灵动岛
在网上找了写灵动岛的视频,大家想看的可以点击链接去看下,肯定比Gif图清晰。
嗯,这样看着确实挺好看,如果不是见过真机显示效果我真的就信了😂,不过还是上面说的,思路奇特,大方承认缺点值得肯定!
Compose 简单实现
之前几篇文章大概说了下 Compose
中的动画,思考下这个动画该如何写?我刚看到这个动画的时候也觉得实现起来不容易,但其实转念一想并不难,其实这些动画总结下来就是根据事件不同 Size 的大小也发生了改变,如果在之前原生安卓实现的话会复杂一些,但在 Compose
中就很简单了,还记得之前几篇文章中提到的 animateSizeAsState
么?这是 Compose
中开箱即用的 API,这里其实就可以使用这个来实现,来一起看下代码!
@Composable
fun DynamicScreen() {
var isCharge by remember { mutableStateOf(true) }
val animateSizeAsState by animateSizeAsState(
targetValue = Size(if (isCharge) 170f else 100f, 30f)
)
Column {
Box(modifier = Modifier
.width(animateSizeAsState.width.dp)
.height(animateSizeAsState.height.dp)
.shadow(elevation = 3.dp, shape = RoundedCornerShape(15.dp))
.background(color = Color.Black),
)
Button(
modifier = Modifier.padding(top = 30.dp, bottom = 5.dp),
onClick = { isCharge = false }) {
Text(text = "默认状态")
}
Button(
modifier = Modifier.padding(vertical = 5.dp),
onClick = { isCharge = true }) {
Text(text = "充电状态")
}
}
}
其实核心代码只有一行,就是上面所说的 animateSizeAsState
,其他的代码基本都在画布局,这里使用 Box
来画了下灵动岛的黑色圆角,并且将 box
的背景设置为了黑色,然后画了两个按钮,一个表示充电状态,另一个表示默认状态,点击按钮就可以进行切换,来看下效果!
大概样式有了,但是不是感觉少了点什么?没错!苹果的动画有回弹效果,但咱们这个没有,那该怎么办呢?还好上一篇文章中咱们讲过动画规格,这里就使用 Spring
就可以满足咱们的需求了,如果想详细了解 Compose
动画规格的话可以移步上一篇文章:Compose 动画艺术探索之动画规格。
来稍微改下代码:
val animateSizeAsState by animateSizeAsState(
targetValue = Size(if (isCharge) 170f else 100f, 30f),
animationSpec = spring(
dampingRatio = Spring.DampingRatioLowBouncy,
stiffness = Spring.StiffnessMediumLow
)
)
别的代码都没动,只是修改了下动画规格,再来看下效果!
嗯,是不是有点意思了!
实现多种切换
上面咱们简单实现了充电的一种状态,但是咱们可以看到苹果里面可不止这一种,上面咱们使用的是 Boolean
值来进行切换的,但如果多种状态的话 Boolean
就有点力不从心了,这个时候就得考虑新的方案了!
private sealed class BoxState(val height: Dp, val width: Dp) {
// 默认状态
object NormalState : BoxState(30.dp, 100.dp)
// 充电状态
object ChargeState : BoxState(30.dp, 170.dp)
// 支付状态
object PayState : BoxState(100.dp, 100.dp)
// 音乐状态
object MusicState : BoxState(170.dp, 340.dp)
// 多个状态
object MoreState : BoxState(30.dp, 100.dp)
}
可以看到上面代码中写了一个密封类,参数就是灵动岛的宽和高,然后根据苹果灵动岛的样式大概可以分为了几种状态:默认状态就是一小条;充电状态高度较默认状态不变,宽度增加;支付状态高度增加,宽度较默认状态不变;音乐状态高度和宽度都较默认状态增加;多个应用状态宽度不变,但会多出一个小黑圆点。
下面还需要修改下状态:
var boxState: BoxState by remember { mutableStateOf(BoxState.NormalState) }
将状态值由 Boolean
改为了刚刚编写的 BoxState
,然后修改下 animateSizeAsState
的使用:
val animateSizeAsState by animateSizeAsState(
targetValue = Size(boxState.width.value, boxState.height.value),
animationSpec = spring(
dampingRatio = Spring.DampingRatioLowBouncy,
stiffness = Spring.StiffnessMediumLow
)
)
接下来再修改下按钮的点击事件:
Button(
modifier = Modifier.padding(top = 30.dp, bottom = 5.dp),
onClick = { boxState = BoxState.NormalState }) {
Text(text = "默认状态")
}
Button(
modifier = Modifier.padding(vertical = 5.dp),
onClick = { boxState = BoxState.ChargeState }) {
Text(text = "充电状态")
}
可以看到代码较上面基本没什么改动,只是在点击的时候切换了对应的 BoxState
值。下面再添加几个按钮来对应上面编写的几种状态:
Button(
modifier = Modifier.padding(vertical = 5.dp),
onClick = { boxState = BoxState.PayState }) {
Text(text = "支付状态")
}
Button(
modifier = Modifier.padding(vertical = 5.dp),
onClick = { boxState = BoxState.MusicState }) {
Text(text = "音乐状态")
}
嗯,代码很简单,就不过多描述,直接运行看效果吧!
嗯,效果是不是已经出来了,哈哈哈,是不是很简单,代码实现个简单样式固然不难,但是如果想把系统应用甚至三方应用都适配灵动岛可不是一个简单的事。不过这里咱们值考虑如何实现灵动岛的动画,并不深究系统实现的问题及瓶颈。
多应用状态
上面基本已经实现了灵动岛的大部分动画,但状态中还有一个多应用,就是多个应用在灵动岛上的显示效果还没弄。多应用状态和别的不太一样,别的状态都是灵动岛宽高的变化,但多应用状态会多分出一个小黑圆点,这个需要单独写下。
val animateDpAsState by animateDpAsState(
targetValue = if (boxState is BoxState.MoreState) 105.dp else 70.dp,
animationSpec = spring(
dampingRatio = Spring.DampingRatioLowBouncy,
stiffness = Spring.StiffnessMediumLow
)
)
Box {
Box(
modifier = Modifier
.width(animateSizeAsState.width.dp)
.height(animateSizeAsState.height.dp)
.shadow(elevation = 3.dp, shape = RoundedCornerShape(15.dp))
.background(color = Color.Black),
)
Box(
modifier = Modifier
.padding(start = animateDpAsState)
.size(30.dp)
.shadow(elevation = 3.dp, shape = RoundedCornerShape(15.dp))
.background(color = Color.Black)
)
}
可以看到这块又加了一个动画 animateDpAsState
来处理多应用状态小黑圆点的展示,如果当前状态为多应用状态的话即 padding
值增加,这样小黑圆点就会单独显示出来,反之不是多应用状态的话,小黑圆点就会在灵动岛下面进行隐藏,不进行展示。实现效果就是开头的效果了。此处也就不再进行展示。
其他方案实现
上面的动画实现主要使用的是 animateSizeAsState
,这个实现当然是没有问题的,但如果不止需要 Size
的话就不太够用了,比如还需要透明度的变化,亦或者还需要旋转缩放等操作的时候就不够用了,这个时候应该怎么办呢?别担心,官方为我们提供了 updateTransition
来处理这种情况,Transition
可管理一个或多个动画作为其子项,并在多个状态之间同时运行这些动画。
其实 updateTransition
咱们并不陌生,在 Compose 动画艺术探索之可见性动画 这篇文章中也提到过,AnimatedVisibility
源码中就使用到了。
下面来试着将 animateSizeAsState
修改为 updateTransition
。
val transition = updateTransition(targetState = boxState, label = "transition")
val boxHeight by transition.animateDp(label = "height", transitionSpec = boxSizeSpec()) {
boxState.height
}
val boxWidth by transition.animateDp(label = "width", transitionSpec = boxSizeSpec()) {
boxState.width
}
Box(
modifier = Modifier
.width(boxWidth)
.height(boxHeight)
.shadow(elevation = 3.dp, shape = RoundedCornerShape(15.dp))
.background(color = Color.Black),
)
使用方法并不难,可以看到这里使用了 animateDp
方法来处理灵动岛的宽高动画,然后设置了下动画规格,为了方便这里将动画规格抽取了下,其实和上面使用的一致,都是 spring
;transition
还为我们提供了一些常用的动画方法,来看下有哪些吧!
上图中的动画方法都可以进行使用,大家可以根据需求来选择使用。
下面来运行看下 updateTransition
实现的效果吧:
可以看到效果基本一致,如果不需要别的参数直接使用 animateSizeAsState
就足够了,但如果需要别的一些操作的话就可以考虑使用 updateTransition
来实现了。
多个应用切换优化
多应用状态苹果实现的样式中有类似水滴的动效,这块需要使用二阶贝塞尔曲线,其实并不复杂,来看下代码:
Canvas(modifier = Modifier.padding(start = 70.dp)) {
val path = Path()
val width = (animateFloatAsState + 30) * density
val x = animateFloatAsState * density
val p2x = density * 15f
val p2y = density * 25f
val p1x = density * 15f
val p1y = density * 5f
val p4x = width - 15f * density
val p4y = density * 30f
val p3x = width - 15f * density
val p3y = 0f
val c2x = (abs(p4x - p2x)) / 2
val c2y = density * 20f
val c1x = (abs(p3x - p1x)) / 2
val c1y = density * 10f
path.moveTo(p2x, p2y)
path.lineTo(p1x, p1y)
// 用二阶贝塞尔曲线画右边的曲线,参数的第一个点是上面的一个控制点
path.quadraticBezierTo(c1x, c1y, p3x, p3y)
path.lineTo(p4x, p4y)
// 用二阶贝塞尔曲线画左边边的曲线,参数的第一个点是下面的一个控制点
path.quadraticBezierTo(c2x, c2y, p2x, p2y)
if (animateFloatAsState == 35f) {
path.reset()
} else {
drawPath(
path = path, color = Color.Black,
style = Fill
)
}
path.addOval(Rect(x + 0f, 0f, x + density * 30f, density * 30f))
path.close()
drawPath(
path = path, color = Color.Black,
style = Fill
)
}
嗯,看着其实还挺多,其实并不难,确定好四个个点,然后连接上色就行,然后根据小黑圆点的位置动态绘制连接部分即可,关于贝塞尔曲线在这里就不细说了,大伙应该比我懂。最后来看下效果吧!
这回是不是就有点像了,哈哈哈!
打完收工
本文带大家一起写了下当下很火的苹果灵动岛,只是最简单的模仿实现,效果肯定不如苹果调教一年的效果,仅供大家参考。
链接:https://juejin.cn/post/7154944949132197924
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
kotlin-android-extensions 插件到底是怎么实现的?
前言
kotlin-android-extensions 插件是 Kotlin 官方提供的一个编译器插件,用于替换 findViewById 模板代码,降低开发成本
虽然 kotlin-android-extensions 现在已经过时了,但比起其他替换 findViewById 的方案,比如第三方的 ButterKnife 与官方现在推荐的 ViewBinding
kotlin-android-extensions 还是有着一个明显的优点的:极其简洁的 API,KAE
方案比起其他方案写起来更加简便,这是怎么实现的呢?我们一起来看下
原理浅析
当我们接入KAE
后就可以通过以下方式直接获取 View
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewToShowText.text = "Hello"
}
}
而它的原理也很简单,KAE
插件将上面这段代码转换成了如下代码
public final class MainActivity extends AppCompatActivity {
private HashMap _$_findViewCache;
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setContentView(1300023);
TextView var10000 = (TextView)this._$_findCachedViewById(id.textView);
var10000.setText((CharSequence)"Hello");
}
public View _$_findCachedViewById(int var1) {
if (this._$_findViewCache == null) {
this._$_findViewCache = new HashMap();
}
View var2 = (View)this._$_findViewCache.get(var1);
if (var2 == null) {
var2 = this.findViewById(var1);
this._$_findViewCache.put(var1, var2);
}
return var2;
}
public void _$_clearFindViewByIdCache() {
if (this._$_findViewCache != null) {
this._$_findViewCache.clear();
}
}
}
可以看到,实际上 KAE
插件会帮我们生成一个 _$_findCachedViewById()
函数,在这个函数中首先会尝试从一个 HashMap 中获取传入的资源 id 参数所对应的控件实例缓存,如果还没有缓存的话,就调用findViewById()
函数来查找控件实例,并写入 HashMap 缓存当中。这样当下次再获取相同控件实例的话,就可以直接从 HashMap 缓存中获取了。
当然KAE
也帮我们生成了_$_clearFindViewByIdCache()
函数,不过在 Activity 中没有调用,在 Fragment 的 onDestroyView 方法中会被调用到
总体结构
在了解了KAE
插件的简单原理后,我们一步一步来看一下它是怎么实现的,首先来看一下总体结构
KAE
插件可以分为 Gradle 插件,编译器插件,IDE 插件三部分,如下图所示
我们今天只分析 Gradle 插件与编译器插件的源码,它们的具体结构如下:
AndroidExtensionsSubpluginIndicator
是KAE
插件的入口AndroidSubplugin
用于配置传递给编译器插件的参数AndroidCommandLineProcessor
用于接收编译器插件的参数AndroidComponentRegistrar
用于注册如图的各种Extension
源码分析
插件入口
当我们查看 kotlin-gradle-plugin 的源码,可以看到 kotlin-android-extensions.properties 文件,这就是插件的入口
implementation-class=org.jetbrains.kotlin.gradle.internal.AndroidExtensionsSubpluginIndicator
接下来我们看一下入口类做了什么工作
class AndroidExtensionsSubpluginIndicator @Inject internal constructor(private val registry: ToolingModelBuilderRegistry) :
Plugin<Project> {
override fun apply(project: Project) {
project.extensions.create("androidExtensions", AndroidExtensionsExtension::class.java)
addAndroidExtensionsRuntime(project)
project.plugins.apply(AndroidSubplugin::class.java)
}
private fun addAndroidExtensionsRuntime(project: Project) {
project.configurations.all { configuration ->
val name = configuration.name
if (name != "implementation") return@all
configuration.dependencies.add(
project.dependencies.create(
"org.jetbrains.kotlin:kotlin-android-extensions-runtime:$kotlinPluginVersion"
)
)
}
}
}
open class AndroidExtensionsExtension {
open var isExperimental: Boolean = false
open var features: Set<String> = AndroidExtensionsFeature.values().mapTo(mutableSetOf()) { it.featureName }
open var defaultCacheImplementation: CacheImplementation = CacheImplementation.HASH_MAP
}
AndroidExtensionsSubpluginIndicator
中主要做了这么几件事
- 创建
androidExtensions
配置,可以看出其中可以配置是否开启实验特性,启用的feature
(因为插件中包含views
与parcelize
两个功能),viewId
缓存的具体实现(是hashMap
还是sparseArray
) - 自动添加
kotlin-android-extensions-runtime
依赖,这样就不必在接入了插件之后,再手动添加依赖了,这种写法可以学习一下 - 配置
AndroidSubplugin
插件,开始配置给编译器插件的传参
配置编译器插件传参
class AndroidSubplugin : KotlinCompilerPluginSupportPlugin {
// 1. 是否开启编译器插件
override fun isApplicable(kotlinCompilation: KotlinCompilation<*>): Boolean {
if (kotlinCompilation !is KotlinJvmAndroidCompilation)
return false
// ...
return true
}
// 2. 传递给编译器插件的参数
override fun applyToCompilation(
kotlinCompilation: KotlinCompilation<*>
): Provider<List<SubpluginOption>> {
//...
val pluginOptions = arrayListOf<SubpluginOption>()
pluginOptions += SubpluginOption("features",
AndroidExtensionsFeature.parseFeatures(androidExtensionsExtension.features).joinToString(",") { it.featureName })
fun addVariant(sourceSet: AndroidSourceSet) {
val optionValue = lazy {
sourceSet.name + ';' + sourceSet.res.srcDirs.joinToString(";") { it.absolutePath }
}
pluginOptions += CompositeSubpluginOption(
"variant", optionValue, listOf(
SubpluginOption("sourceSetName", sourceSet.name),
//use the INTERNAL option kind since the resources are tracked as sources (see below)
FilesSubpluginOption("resDirs", project.files(Callable { sourceSet.res.srcDirs }))
)
)
kotlinCompilation.compileKotlinTaskProvider.configure {
it.androidLayoutResourceFiles.from(
sourceSet.res.sourceDirectoryTrees.layoutDirectories
)
}
}
addVariant(mainSourceSet)
androidExtension.productFlavors.configureEach { flavor ->
androidExtension.sourceSets.findByName(flavor.name)?.let {
addVariant(it)
}
}
return project.provider { wrapPluginOptions(pluginOptions, "configuration") }
}
// 3. 定义编译器插件的唯一 id,需要与后面编译器插件中定义的 pluginId 保持一致
override fun getCompilerPluginId() = "org.jetbrains.kotlin.android"
// 4. 定义编译器插件的 `Maven` 坐标信息,便于编译器下载它
override fun getPluginArtifact(): SubpluginArtifact =
JetBrainsSubpluginArtifact(artifactId = "kotlin-android-extensions")
}
主要也是重写以上4个函数,各自的功能在文中都有注释,其中主要需要注意applyToCompilation
方法,我们传递了features
,variant
等参数给编译器插件
variant
的主要作用是为不同 buildType
,productFlavor
目录的 layout 文件生成不同的包名
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.debug.activity_debug.*
import kotlinx.android.synthetic.demo.activity_demo.*
比如如上代码,activity_debug
文件放在debug
目录下,而activiyt_demo
文件则放在demo
这个flavor
目录下,这种情况下它们的包名是不同的
编译器插件接收参数
class AndroidCommandLineProcessor : CommandLineProcessor {
override val pluginId: String = ANDROID_COMPILER_PLUGIN_ID
override val pluginOptions: Collection<AbstractCliOption>
= listOf(VARIANT_OPTION, PACKAGE_OPTION, EXPERIMENTAL_OPTION, DEFAULT_CACHE_IMPL_OPTION, CONFIGURATION, FEATURES_OPTION)
override fun processOption(option: AbstractCliOption, value: String, configuration: CompilerConfiguration) {
when (option) {
VARIANT_OPTION -> configuration.appendList(AndroidConfigurationKeys.VARIANT, value)
PACKAGE_OPTION -> configuration.put(AndroidConfigurationKeys.PACKAGE, value)
EXPERIMENTAL_OPTION -> configuration.put(AndroidConfigurationKeys.EXPERIMENTAL, value)
DEFAULT_CACHE_IMPL_OPTION -> configuration.put(AndroidConfigurationKeys.DEFAULT_CACHE_IMPL, value)
else -> throw CliOptionProcessingException("Unknown option: ${option.optionName}")
}
}
}
这段代码很简单,主要是解析variant
,包名,是否开启试验特性,缓存实现方式这几个参数
注册各种Extension
接下来到了编译器插件的核心部分,通过注册各种Extension
的方式修改编译器的产物
class AndroidComponentRegistrar : ComponentRegistrar {
companion object {
fun registerViewExtensions(configuration: CompilerConfiguration, isExperimental: Boolean, project: MockProject) {
ExpressionCodegenExtension.registerExtension(project,
CliAndroidExtensionsExpressionCodegenExtension(isExperimental, globalCacheImpl))
IrGenerationExtension.registerExtension(project,
CliAndroidIrExtension(isExperimental, globalCacheImpl))
StorageComponentContainerContributor.registerExtension(project,
AndroidExtensionPropertiesComponentContainerContributor())
ClassBuilderInterceptorExtension.registerExtension(project,
CliAndroidOnDestroyClassBuilderInterceptorExtension(globalCacheImpl))
PackageFragmentProviderExtension.registerExtension(project,
CliAndroidPackageFragmentProviderExtension(isExperimental))
}
}
override fun registerProjectComponents(project: MockProject, configuration: CompilerConfiguration) {
if (AndroidExtensionsFeature.VIEWS in features) {
registerViewExtensions(configuration, isExperimental, project)
}
}
}
可以看出,主要就是在开启了AndroidExtensionsFeature.VIEWS
特性时,注册了5个Extension
,接下来我们来看下这5个Extension
都做了什么
IrGenerationExtension
IrGenerationExtension
是KAE
插件的核心部分,在生成 IR 时回调,我们可以在这个时候修改与添加 IR,KAE
插件生成的_findCachedViewById
方法都是在这个时候生成的,具体实现如下:
private class AndroidIrTransformer(val extension: AndroidIrExtension, val pluginContext: IrPluginContext) :
IrElementTransformerVoidWithContext() {
override fun visitClassNew(declaration: IrClass): IrStatement {
if ((containerOptions.cache ?: extension.getGlobalCacheImpl(declaration)).hasCache) {
val cacheField = declaration.getCacheField()
declaration.declarations += cacheField // 添加_$_findViewCache属性
declaration.declarations += declaration.getClearCacheFun() // 添加_$_clearFindViewByIdCache方法
declaration.declarations += declaration.getCachedFindViewByIdFun() // 添加_$_findCachedViewById方法
}
return super.visitClassNew(declaration)
}
override fun visitCall(expression: IrCall): IrExpression {
val result = if (expression.type.classifierOrNull?.isFragment == true) {
// this.get[Support]FragmentManager().findFragmentById(R$id.<name>)
createMethod(fragmentManager.child("findFragmentById"), createClass(fragment).defaultType.makeNullable()) {
addValueParameter("id", pluginContext.irBuiltIns.intType)
}.callWithRanges(expression).apply {
// ...
}
} else if (containerHasCache) {
// this._$_findCachedViewById(R$id.<name>)
receiverClass.owner.getCachedFindViewByIdFun().callWithRanges(expression).apply {
dispatchReceiver = receiver
putValueArgument(0, resourceId)
}
} else {
// this.findViewById(R$id.<name>)
irBuilder(currentScope!!.scope.scopeOwnerSymbol, expression).irFindViewById(receiver, resourceId, containerType)
}
return with(expression) { IrTypeOperatorCallImpl(startOffset, endOffset, type, IrTypeOperator.CAST, type, result) }
}
}
如上所示,主要做了两件事:
- 在
visitClassNew
方法中给对应的类(比如 Activity 或者 Fragment )添加了_$_findViewCache
属性,以及_$_clearFindViewByIdCache
与_$_findCachedViewById
方法 - 在
visitCall
方法中,将viewId
替换为相应的表达式,比如this._$_findCachedViewById(R$id.<name>)
或者this.findViewById(R$id.<name>)
可以看出,其实KAE
插件的大部分功能都是通过IrGenerationExtension
实现的
ExpressionCodegenExtension
ExpressionCodegenExtension
的作用其实与IrGenerationExtension
基本一致,都是用来生成_$_clearFindViewByIdCache
等代码的
主要区别在于,IrGenerationExtension
在使用IR
后端时回调,生成的是IR
。
而ExpressionCodegenExtension
在使用 JVM 非IR
后端时回调,生成的是字节码
在 Kotlin 1.5 之后,JVM 后端已经默认开启 IR
,可以认为这两个 Extension
就是新老版本的两种实现
StorageComponentContainerContributor
StorageComponentContainerContributor
的主要作用是检查调用是否正确
class AndroidExtensionPropertiesCallChecker : CallChecker {
override fun check(resolvedCall: ResolvedCall<*>, reportOn: PsiElement, context: CallCheckerContext) {
// ...
with(context.trace) {
checkUnresolvedWidgetType(reportOn, androidSyntheticProperty)
checkDeprecated(reportOn, containingPackage)
checkPartiallyDefinedResource(resolvedCall, androidSyntheticProperty, context)
}
}
}
如上,主要做了是否有无法解析的返回类型等检查
ClassBuilderInterceptorExtension
ClassBuilderInterceptorExtension
的主要作用是在onDestroyView
方法中调用_$_clearFindViewByIdCache
方法,清除KAE
缓存
private class AndroidOnDestroyCollectorClassBuilder(
private val delegate: ClassBuilder,
private val hasCache: Boolean
) : DelegatingClassBuilder() {
override fun newMethod(
origin: JvmDeclarationOrigin,
access: Int,
name: String,
desc: String,
signature: String?,
exceptions: Array<out String>?
): MethodVisitor {
val mv = super.newMethod(origin, access, name, desc, signature, exceptions)
if (!hasCache || name != ON_DESTROY_METHOD_NAME || desc != "()V") return mv
hasOnDestroy = true
return object : MethodVisitor(Opcodes.API_VERSION, mv) {
override fun visitInsn(opcode: Int) {
if (opcode == Opcodes.RETURN) {
visitVarInsn(Opcodes.ALOAD, 0)
visitMethodInsn(Opcodes.INVOKEVIRTUAL, currentClassName, CLEAR_CACHE_METHOD_NAME, "()V", false)
}
super.visitInsn(opcode)
}
}
}
}
可以看出,只有在 Fragment 的onDestroyView
方法中添加了 clear 方法,这是因为 Fragment 的生命周期与其根 View 生命周期可能并不一致,而 Activity 的 onDestroy 中是没有也没必要添加的
PackageFragmentProviderExtension
PackageFragmentProviderExtension
的主要作用是注册各种包名,以及该包名下的各种提示
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.debug.activity_debug.*
import kotlinx.android.synthetic.demo.activity_demo.*
比如我们在 IDE 中引入上面的代码,就可以引入 xml 文件中定义的各个 id 了,这就是通过这个Extension
实现的
总结
本文主要从原理浅析,总体架构,源码分析等角度分析了 kotlin-android-extensions 插件到底是怎么实现的
相比其它方案,KAE
使用起来可以说是非常简洁优雅了,可以看出 Kotlin 编译器插件真的可以打造出极简的 API,因此虽然KAE
已经过时了,但还是有必要学习一下的
如果本文对你有所帮助,欢迎点赞收藏~
链接:https://juejin.cn/post/7155491115645435917
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
我从大厂被裁,再通过外包回去,只要我不尴尬,尴尬的就是别人!
为了进大厂,有些人选择“曲线救国”,以正式员工身份被裁,又以外包员工身份回去,只是这样的迂回方式是否可行?是否只要你不尴尬,尴尬的就是别人?
这是一位网友的疑问:
网友却说,人生哪有那么多观众?做你想做的就行。
有人说,都是打工挣快钱,谁管你哪里来哪里去,大家都在操心自己的事情。
有人说,赚钱不寒碜,没什么尴尬的,努力的人都是值得尊敬的。
有人说,都是在私营企业,没什么区别。
所以,只要你自己心理过关就行。
还有人说,看样子楼主对公司是真爱了。
有人说,哪里跌倒在哪里爬起来,说不定外包转正编。
有人说,还有人主动从正式转外包,因为外包轻松。
不过也有人说,转外包一般工资比较低。
一位腾讯员工说,大厂有什么好?别陷在围城里,自己巴不得能找到出去的路。
还有人说,士可杀不可辱,千万不能回去!
虽说职业不分高低贵贱,但有一点我们不得不承认,正式员工和外包员工的确在福利待遇和身份地位上有诸多差别,这也是许多人不愿意做外包员工的原因。
可我们工作是为了什么?除了理想、信念、价值等原因,其实最重要的就是生存,是挣钱。挣钱就是目的,生存就是王道,只要能达到这个目标,其他的都可以放到一边。外包员工怎么了?没有人是宇宙中心,没人会时时刻刻盯着你,做好你的工作,过好你的生活,顾好你的家庭,你就是一个成功的人。
何况外包员工并非一无是处,一样签劳动合同,一样有五险一金,一样是打工人,别给自己加那么多戏。
作者:行者
来源:devabc
浅谈2022Android端技术趋势,什么值得学?
引言
回头去看 2021,过的似乎那么快,不敢相信我已经从事 Android 开发两年了,不免生出一些感叹。
那么 2022 ,Android 端会有什么技术趋势吗?或者什么 [新] 技术值得去学? 又或者对我来说,现在什么 [值得] 去学?
本文将通过我个人的技术学习经历去分析我们应该怎么选用某个技术,希望对大家能有所帮助。
回头看
让我们把时间切回过去,最近几年我都给自己加了哪些技术点?
2019-2020
Kotlin
,协程MVP
,Hilt
,MVVM
,JetPack
相关热修复
Flutter
浅试自动化、持续集成相关
2021-2022
JetPack Compose
Epoxy
+Mvrx
,MVI
看完这个表,是不是惊叹于,我靠,你小子 2021 等于啥都没学啊?
说来尴尬,当我自己从博客记录从翻阅时,也有这份感叹,对于 [新技术] ,真的好像没怎么看了,所以在年终总结上,我发出了这样一句感叹,今年用的组件都是以前学过的,只不过忘了,又翻出笔记看了一眼。
但仔细又想了下,2021新技术真的好像没有多少,对于 Android 端而言,Compose
算一击重拳,而 MVI
又是最近因为 Compose
被正式启用为 Google 推荐 [新] 架构标准,其他的好像真的没有。
Google今年推过的技术
如果我们只将技术定义为 [技术组件] ,那就可能太狭义,所以那就细细列举一下今年 Google 推过的那些技术文章:
如何找到呢,那么 Android开发者 公众号就是最优先的。所以我们就通过其发布过的文章,大致统计一下,Android 官方给我们的建议及 [开发指南] ,我排了一个表,大致如下:
JetPack
Navigation
、Hilt
、WorkManager
、ActivityResult
Compose
、Wear OS-Compose
、Wear Os
-卡片库WindowsManager
、Room
、Paging3.0
、Glance - Alpha
折叠屏,大屏适配
推荐了很多次,
Android12
上也推了几次Kotlin
Flow
、协程Android12
行为变更、隐私安全更新、新的 小组件widget
安全方面
数据加密与生物特征、App
合规
Android 启动相关
App Startup
、延迟初始化CameraX
Material Desgin
按照推荐频率,我将它们分别列在了上面,总共上面这几大类。不难发现,JetPack
仍然是 Android 官方 首推 ,然后就是 折叠屏以及不同屏幕 的适配,接着就是 Kotlin
与 Android12
,当然今年因为 合规
方面的各种问题,Android团队 对于安全方面也提到了,最后就是和性能以及 UI 相关的一些推荐。
趋势预测
折叠屏与大屏适配
严格上这个其实不算是一项技术,而是一项适配工作。但是一直以来,对于大屏的适配,Android 上基本做的很少。自从三星推出第一个折叠屏之后,这个适配就开始被重视起来了。
厂商方面,目前 oppo,华为,小米 也都纷纷推出自己的折叠屏手机,以满足先行市场。
官方支持度 方面,如果看过今年的 IO 大会,就会发现,折叠屏适配已经被专门放到了一个栏目,而且专门讲解,官方公众号也已经推了多次。
所以我们姑且可以认为,折叠屏适配应该是2022的一个趋势,但目前对于折叠屏的适配的主流App其实还没有多少,更多的也都是厂商做了相关适配,app开发方面专门针对改动做的其实并不多。
所以可见在2022随着折叠屏手机机型的愈来愈多,某些关键业务的全面适配工作也将随之展开,而不是现在仅仅只是在折叠的时候,同时存在两个APP,或者某个页面展示在另一个屏幕。
技术支持方面,Android团队 为此专门准备了一个新的 JetPack 组件,JetPack WindowManager,其主要功能就是监听屏幕的折叠状态,以及当前相应的屏幕信息,目前主要以可折叠设备为目标,不过未来将支持更多屏幕类型及窗口功能,现在处于 rc 版本,当然今年也肯定会推出稳定版。
JetPack Compose
Compose
自从发布第一个稳定版本后,在今年的 IO 大会上也有专门的分区去讲。
其是用于构建 原生Android 的一个 工具包
,以 声明式 写法,搭配 Kotlin
,可大大简化并加快原生的 UI 开发工作。
目前 Compose
已经对如下几个方面做了支持:
Android UI 支持
Wear 可穿戴设备支持
Android Widget 小组件支持
非官方方面,jetbrains 也对桌面版,以及网页做了相关支持,具体见:Compose Multiplatform
桌面版 目前已经发布了正式版本1.0.1
得益于 Compose
的声明式开发,使得其做折叠屏适配也是较为简单。在与原生 View
的交互上,Compose
支持的也非常不错。
所以我们可以认为,2022,如果从事原生开发,那么
Compose
势必是一个比较适合你学习的新技术,它的上手难度并不大,只要你熟悉Kotlin
,也就能很快上手,只不过目前其在ide上的 预览 功能比较慢,还有待后续优化。
Kotlin
协程
协程其实在前几年已经被广泛使用,我第一次使用协程是在2020年,也见证了其逐渐替代 AsyncTask
及相关线程池工具的过程。
Flow
Flow
今年来被 Android团队 推荐了多次,其主要以协程为基础构建,某种意义上而言,我个人觉得其似乎有点替代 RxJava
的意思。得益于 Kotlin
的强大与简洁,Flow
今年出现最多的场景反而是 Android团队 推荐其用于替代 LiveData ,以增强部分情况下的使用。
当然 Flow
不止于此,如果你正在使用 Kotlin
,并且协程用的也比较多,那么 Flow
肯定是绕不开的一个话题。
所以我们可以预估,在2022,协程 与
Flow
依然值得学习,并且也是能很快感受到效益的组件。但是相比协程,
Flow
其实还有很长一段时间要走,毕竟常见开发场景里,LiveData 就可以满足,而Flow
却显得不是那么必需。
ASM
这项技术其实并不新奇,但是因为其本身需要的前备知识挺多,比如 Android打包流程 ,APK打包流程,字节码,自定义 Gradle 插件,Transform API ,导致细分为了好多领域,大佬们依然在热追,而像我这样的菜鸟其实还是一脸吃瓜。
那为什么我认为其是一个技术趋势呢?
主要是 合规 带来的影响,大的环境下,可能以后打包时,我们都会监测相应的权限声明与隐私调用,否则如何确保后续的改动不会导致违规呢?但如何确定某个 sdk 没有调用?而且我们也不可能每次都让相关第三方去检测。
所以,维护一个相应的监测组件,是大环境下的必需。而实现上述插件最好的方式就是 Hook
或者 ASM
,所以如果你目前阶段比较高,ASM
依然是你避不开的技术选题。
什么[值得]你去学?
这个副标题其实有一点夸张,但仔细想想,其实就是这样,我们应该明白,到底什么是更适合自己当下学习的。
以我个人为例,大家可以从中体会一下,自己应该关注哪些技术,当然,我个人的仅只能作为和我一样的同学做参考:
就像最开始说的,其实这些新组件,很多我都已经用过或者记录过,在最开始的两年,我一直在追寻组件越新越好的道路上,所以每当新出一个组件,总会在项目中进行实践,去尝试。
但是我也逐渐发现了一些问题,当经历了[使用工具]的这个阶段,当我要去解决某些特定情况下问题时,突然发现,自己似乎什么都不会,或者就只会基础,比如:
在集成某些
gradle
插件时,如果要满足CI
下的一些便捷,要去写一些Task
去满足动态集成,而自己对Gradle
仅仅处于Android常见使用阶段,这时候就需要去学相关;我自己也会维护一些组件库,当使用的同学逐渐增多,大家提到的问题也越来越多,那如何解决这些问题,如何优雅的兼容,组件的组合方式,如何运用合适的设计模式去优化,这些又是我需要考虑的问题;
当我们开始对音视频组件进行相关优化时,此时又出现了很多方向,最终的方案选型也是需要你再次进入一个未知领域,从0到0.1;
新技术会让我当前编码变得开心,能节省我很多事,但其不能解决一些非编码或者复杂问题,而这些问题,是每个同学前进道路上也都会遇到的,所以我们常常会看到,做 Android 真难,啥都要会。
总体对我而言,今年会主要将一些精力放在如下几个方面:
Gradle
相关设计模式在三方库中的运用
Android 相关 源码 理解
总结
技术在不断变化与迭代,有些技术我们会发现为什么好几年了,今年似乎特别受人关注,其实也是因为在某种环境下,其的作用逐渐显现。而这些技术正是成为一名优秀的 Android工程师 所必须具备的基础技能。
我们在追寻 [新] 技术的,享受快捷的同时,也别忘了 [停] 下来看看身边风景。
作者:Petterp
来源:juejin.cn/post/7053831595576426504
2023 届秋招回顾,寒气逼人。。。
自我介绍
我来自杭州的一所双非一本学校,是一名普通的本科生,专业【软件工程】。
初学编程
事实上,我从高中毕业起就开始思考未来的工作了,一开始网上都是 Python 相关的新闻,因此从高中毕业的暑假就开始学 Python,当时在新华书店,捧着一本入门书天天看;
但是看了并没有什么用,除了大一的时候吹牛皮,啥都没学到。
然后自 2020 年初(大一寒假) 疫情爆发,学校线上授课;课程中有【面向对象语言】的学习,自此开始正式的跟着视频学习 Java 了。
第一次实习
2021年暑假(大二暑假),我的绩点排名在学校保研线边缘徘徊,但又不愿去刷那些水课的绩点,因此决定考研或者工作,期间比较迷茫。
当时在网上得到一位大数据方向前辈的指点,他说了一句话:“早,就是优势。”
因此,我决定先去实习,当时在杭州人工智能小镇找了家公司实习。
虽说是实习,但其实基本每天上班啥也不干,主管也没分配任务,就是一直在看书,期间看完了周志明老师的 JVM,以及几本讲并发编程的书。
第二次实习
大三上时,眼看着 Java 越来越卷,自己开始学习了大数据相关的组件,像 Hadoop、HBase、Flume 等等组件,一直学到了实时计算之前。
大三下时,我明白自己是一个心态非常不稳定的人,考研对我来说,最后几个月会非常的难熬,并且考研失败的风险也让我望而却步,因此下定决心本科就业!
寒假的时候跟着视频完成了【谷粒商城】那个项目,之后立刻着手准备找实习。
也就是在这第二段实习过程中(2022上半年),我真正的学到了一些实际的开发技巧。
实习期间,看完了几本深入讲中间件 ZK、Redis、Spring源码 和 代码重构的书。
本次实习,让我受益良多,由衷感谢我的 mentor(导师)和主管!
秋招情况
我从 6 月底开始复习准备,因为准备得比较晚,所以基本没参加提前批。
正式批总共投递了近 150 家公司,笔试了 30 家,面试了 15 个公司,除了海康威视,其他基本都意向或排序了。
大致情况如下:
offer:兴业数金
意向:猿辅导,Aloudata
排序 / 审批:华为,网易雷火,荣耀,招银网络,古茗奶茶,CVTE,以及一众独角兽公司
面试挂:海康威视
CVTE 提前批面试(已拒)
大应科技(OC)
e签宝 提前批(已拒)
荣耀 Honor(录用决策中)
猿辅导(OC)
趣链科技(流程中)
海康威视(已挂)
SMART(已拒)
寒王厂(泡池子)
网易雷火(排序中)
招银网络(流程中)
古茗奶茶(流程中)
复习方式
关于焦虑
我们先要肯定一点,在复习的时候,【焦虑】是一件必然的事情,我们要正视焦虑。
就拿我自己举例子吧,【双非本科】的学历会把我放到一个最最糟糕的位置。
自开始复习时,我内心就非常非常的焦虑,胸膛经常会像要爆炸一样的沉闷(真的)...
而我的缓解方式主要分为两种吧:
运动
背一会八股或者刷一会题之后就去走走
每天晚上去操场跑步
心理慰藉
面试前,我会像《三傻大闹宝莱坞》里的阿米尔汗一样,拍着自己的胸口对自己说 “Aal izz well”
给自己想好一个下下策,如果秋招真的找不到工作该怎么办?那至少还有春招,对比明年考研失利的同学,我至少积累了经验!
复习流程
我的整体复习流程分为三步:
处理基础知识
看八股
查漏补缺
阶段一:处理基础知识
对于基础知识部分,我自知《计网》和《操作系统》这两门课学的很差,所以一开始就复习这部分知识。
当时先把两门课的教材翻了一遍,然后做了一些摘抄,但说实话基本没用。
这部分知识,我在面试过程中,大概有 50% 的几率会被问到操作系统,但从来没被问到过计网(幸运)。
之后复习《设计模式》,先跟着一个 csdn 上的博客边看别写,之后找了一个很老的(2003年)博客总结,反复背诵,基本能手写大部分的模式实现了。
这部分知识,我在面试过程中,要求写过 单例 、三大工厂 和 发布订阅 的实现,问过项目中和 Spring 以及其它中间件中用到的设计模式。
阶段二:看八股
全面进军 Java 八股文。
我先看了自己在实习前准备的那些文档,之后网上找了 JavaGuide、JavaKeeper 这两份文档作为补充。
因为自己之前有过两段的实习经验,因此背过很多次八股。
但考虑到本次秋招可能会把战线拉得比较长,因此就自己总结了一份脑图。
阶段三:查漏补缺
经过几轮面试,逐渐察觉到了自己的一些不足,之后针对性的去完善了一下。
这里随便列举几个点,供其它同学参考:
为什么说进程切换开销比线程大?
NIO到底有没有阻塞,NIO到底能不能提高 IO 效率?
Redis分布式锁的限制,RedLock的实现?
ZK 明明有了有序的指令队列,为什么还要用 zxid来辅助排序?
basic paxos 和 multi paxos 的使用?
为什么拜占庭将军无解?
还有一些业务场景的选择问题。。。
总结
我一直提醒自己:你是一个双非本科生,这个秋招你如果再不拼命,你就要完蛋了。
我想,我是幸运的:
我很幸运 在实习的时候,有一个好的 mentor,带我开发了字节码相关的组件,让我的简历不容易挂;
我很幸运 在复习的时候,有几位好的朋友,分享经验,加油鼓励,让我没有被焦虑击倒;
我很幸运 在面试的时候,有无私的舍友们,能在我需要笔试面试时,把宿舍让给我,让我没有后顾之忧;
当然,也会有遗憾。每个人心中都有着大厂梦,而今年进大厂确实很难:
我从大一开始就非常渴望进入阿里巴巴,实习的时候五面阿里不得,秋招全部简历挂;
百度+度小满,投了 4 个岗位,全部简历挂;
字节,一开始担心算法没敢投,之后担心基础知识也没敢投,也很遗憾了;
人生,有所得就有所失,有所失就有所得。
最后,想给其他明后年参加秋招的同学一些提醒:
一定要早做准备,早点实习,早点刷算法题,早就是优势;
人生无常,意外太多,绝对不要 all in 一家公司;
鞋合不合适只有脚知道,自己总结的八股会更适合自己;
多刷 力扣 Hot 100,或者 Codetop 热门题,反复刷;
选择大于努力;
在寒气逼人的 2022,我们需要抱团取暖...
作者:OliQ
链接:http://www.cnblogs.com/yuanchuziwen/p/16770895.html
Android打造专有hook,让不规范的代码扼杀在萌芽之中
俗话说,无规矩不成方圆,同样的放在代码里也是十分的贴切,所谓在代码里的规矩,指的就是规范,在一定规范约束下的项目,无论是参与开发还是后期维护,都是非常的直观与便捷,不能说赏心悦目,也可以用健壮可维护来表示;毕竟协同开发的项目,每个人都有自己的一套开发标准,你没有一套规范,或者是规范没有落地执行,想想,长此以往,会发生什么?代码堆积如山?维护成本翻倍增加?新人接手困难?等等,所谓的问题会扑面而来。
正所谓规范是一个项目的基石,也是衡量一个项目,是否健壮,稳定,可维护的标准,可谓是相当重要的。我相信,大部分的公司都有自己的一套规范标准,我也相信,很多可能就是一个摆设,毕竟人员的众多,无法做到一一的约束,如果采取人工的检查,无形当中就会投入大量的时间和人力成本,基于此,所谓的规范,也很难执行下去。
介于人工和时间的投入,我在以往的研究与探索中,开发出了一个可视化的代码检查工具,之前进行过分享,《一个便捷操作的Android可视化规范检查》,想了解的老铁可以看一看,本篇文章不做过多介绍,当时只介绍了工具的使用,没有介绍相关功能的开发过程,后续,有时间了,我会整理开源出来,一直忙于开发,老铁们,多谅解。这个可视化的检查工具,虽然大大提高了检查效率,也节省了人力和时间,但有一个潜在的弊端,就是,只能检查提交之后的代码是否符合规范,对于提交之前没有进行检查,也就说,在提交之前,规范也好,不规范也罢,都能提交上来,用工具检查后,进行修改,更改不规范的地方后然后再提交,只能采取这样的一个模式检查。
这样的一个模式,比较符合,最后的代码检查,适用于项目负责人,开发Leader,对组员提交上来的代码进行规范的审阅,其实并不适用于开发人员,不适用不代表着不可用,只不过相对流程上稍微复杂了几步;应对这样的一个因素,如何适用于开发人员,方便在提交代码之前进行规范检查,便整体提上了研发日程,经过几日的研究与编写,一个简单便捷的Android端Git提交专有hook,便应运而生了。
说它简单,是因为不需要编写任何的代码逻辑,只需要寥寥几步命令,便安装完毕,通过配置文件,便可灵活定制属于自己的检查范围。
为了更好的阐述功能及讲述实现过程,便于大家定制自己的开发规范,再加上篇幅的约束,我总结了四篇文章来进行系统的梳理,还请大家,保持关注,今天这篇,主要讲述最终的开发成果,也就是规范工具如何使用,规范这个东西,其实大差不差,大家完全可以使用我自己已经开发好的这套。
这个工具的开发,利用的是git 钩子(hook),当然也是借助的是Node.js来实现的相关功能,下篇文章会详细介绍,我们先来安装程序,来目睹一下实际的效果,安装程序,只需要执行几步命令即可,无需代码介入,在实际的开发中需要开发人员,分别进行安装。
安装流程
1、安装 Node.js,如果已经安装,可直接第2步:
Node.js中允许使用 JavaScript 开发服务端以及命令行程序,我们可以去官网nodejs.org
下载最新版本的安装程序,然后一步一步进行安装就可以了,这个没什么好说的,都是开发人员。
2、安装android_standard
android_standard是最终的工具,里面包含着拦截代码判断的各种逻辑 , 在项目根目录下执行如下命令:
npm install android_standard --save-dev
执行完命令后,你会发现,你的项目下已经多了一个目录,还有两个json文件,如下图所示:
node_modules,用来存放下载安装的包文件夹,里面有我们要使用到的功能,其实和Android中lib目录很类似,都是一些提供功能的库。
package.json文件,是配置文件,比如应用的名字,作者,介绍,还有相关的依赖等,和Android中的build.gradle文件类似。
3、创建git配置文件,执行如下命令
node node_modules/android_standard/gitCommitConfig
命令执行成功会返回如下信息:
此命令执行完后,会在项目根目录下创建gitCommitConfig文件,这个文件很重要,是我们执行相关命令的配置文件,内容如下,大家可以根据自己实际项目需要进行更改。
项目下生成gitCommitConfig.android文件,.android是我自己定义的,至于什么格式,等你自己开发的时候,完全可以自定义,是个文件就行。
打开后,文件内容如下,此文件是比较重要的,后续所有的规范检查,都要根据这个文件里的参数来执行,大家在使用的时候,就可以通过这个文件来操作具体的规范检查。
4、更改执行文件,执行如下命令
执行文件,就是需要在上边生成的package.json文件,添加运行程序,使其在git提交时进行hook拦截。
node node_modules/android_standard/package
5、添加git过滤
因为执行完上述命令后,会产生几个文件,而这几个文件是不需要我们上传到远程仓库的,所以我们需要在.gitignore文件里添加忽略,直接复制即可。
/node_modules
package.json
package-lock.json
gitCommitConfig.android
6、后续如果有更新,可命令进行操作:
注:此命令在更新时执行
npm update android_standard --save-dev
7、删除操作
注:后续不想使用了,便可执行如下命令:
npm uninstall android_standard --save-dev
具体使用
通过上述的安装流程,短短几个命令,我们的规范检查便安装完毕,后续只需要通过gitCommitConfig.android文件,来动态的更改参数即可,是不是非常的方便,接下来,我们来实际的操作一番。
关于配置文件的相关参数,也都有注释,一看便知,这里简单针对最后的参数,做一个说明,也就是gitCommand这个参数,true为工具,false为命令方式;true也好,false也好,在主要的功能验证上,没有区别,唯一的区别就是,命令行的方式提交,会有颜色区分,后面有效果。
我们先来看下命令行下的执行效果,当配置文件开关gitCommitSwitch已开,并且gitCommand为false,其他的配置参数,大家可以根据需要进行改动,在代码提交的时候如下效果:
在Android studio中提交代码执行效果
TortoiseGit提交代码执行效果:
目前呢,针对Android端的规范检查,无论是java还是Kotlin,还是资源文件,都做了一定的适配,经过多方测试,一切正常,如果大家的公司也需要这样的一个hook工具,欢迎使用,也欢迎继续关注接下来的相关实现逻辑文章。
好了各位老铁,这篇文章先到这里,下篇文章会讲述,具体的实现过程,哦,忘记了,上篇结尾的遗留组件化还未更新,那就更新完组件化,接着更新这个,哈哈,敬请期待!
链接:https://juejin.cn/post/7140963362791227400
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。