公司没钱了,工资发不出来,作为员工怎么办?
公司没钱了,工资发不出来,作为员工怎么办?
现在大环境不好,很多公司都会遇到一些困难。如果公司真的有现金流还好,如果没有,还等着客户回款。那么就有点难办了。很多公司会采取延期发工资,先发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
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
由浅入深、详解Android中Drawable的那些事
引言
对于 Drawable
,一直没有专门记录,日常开发中,也是属于忘记了再搜一下。主要是使用程度有限(仅仅只是shape
或者 layer
等冰山一角),另一方面是 Android
对其的高度抽象,导致从没去关注过细节,从而对于 Drawable
没有真正的理解其设计与存在的意义。
反而是偶尔一次发现其他同学的运用,才明白了自己的狭隘,为此,怀着无比惭愧的心情,写下此篇,与君共勉。
鉴于此,本篇将完整的描述开发中常见各种 Drawable
,以及在工程化项目的背景下,如何更好的运用。总体难度较低,不涉及源码,适合轻松阅读。
来者何人
2022的今天,随便问一个Android开发,Drawable
是什么?
比如我。他(她)肯定会告诉你(鄙视的眼神),你si不si傻,
Drawable
都不知道,Drawable
,Drawble
,Drawable
不就是...😐
不就是经常用来设置的图像吗?🫣(不确定语气,似乎说的不完整)
上述说的有错吗,也没错。嗯,但总觉得差点什么,过于简单?细心的你肯定会觉得没这么简单。
那到底什么是Drawable?
Drawable
表示的是一种可以在Canvas上进行绘制的抽象概念。人话就是 就是指可在屏幕上绘制的图形。
就这?就这?就这?
这说了个啥,水水水,一天就知道水文章?🫵🏼
嗯🧐,在开发角度来看,Drawable
是一个抽象的类,用来表示可以绘制在屏幕上绘制的图形。我们常见有很多种 Drawable
,比如Bitmapxx,Colorxxx,Shapexxx,它们一般都用于表示图像,但严格上来说,又不全是图像。
后半句用人话怎么理解呢?
对于普通的图形或图片,我们肯定没法更改,因为其已经固定了(资源文件)。
但是对于
Drawable
,虽然某种程度上也是图形(矢量资源),但其具备处理或绘制具体显示逻辑的方式。也就是说,这是一个支持修改的图形,比如我们可以把一张图塞给了BitmapDrawable
,但依然可以做二次调整,比如拉伸一下,改一下位置,给这张图上再添加点别的什么东西。或者也可以理解为这是一个简化版的View,只不过它更简易,目的纯粹。其无法像View
一样接收事件或者和用户交互,其更像一个绘制板,指哪打哪,仅作为显示使用。
当然除了简单的绘图,Drawable
还提供了很多通用api,使得我们可以与正在绘制的内容进行交互,从而更加完善。
相应的,Drawable
内部其实也有自己的宽高、通过 intrinsicWidth
、intrinsicHeight
即可获取。需要注意的是:
Drawable
的宽高不等于其展示时的大小,我们可以认为Drawable
不存在大小的概念,因为其用于View背景时,其会被拉伸至View的同等大小。- 也并不是所有的
Drawable
都有内部宽高,比如,由一个图片所形成的Drawable
,其相应的宽高也就是图片的宽高,而由颜色所形成的Drawable
,相应的内部也不存在宽高。
Drawable的种类
如下所示,Drawable有如下类型:
好家伙,这也太多了吧,而且后续还会越来越多。
当然这么多,我们一般其实根本不可能全部用上,常见的有如下几种类别:
无状态
BitmapDrawable
<<bitmap
用于将图片转为BitmapDrawable;
ShapeDrawable
<<shape
通过颜色来构造Drawable;
VectorDrawable
<<vector
矢量图,Android5.0及以上支持。便于在缩放过程中保证显示质量,以及一个矢量图支持多个屏幕,减少apk大小;
TransitionDrawable
<<transition
用于实现Drawable间的淡入淡出效果;
InsetDrawable
<<inset
用于将其他Drawable内嵌到自己当中,并可以在四周留出一定的间距。当一个View希望自己的背景比实际的区域小时,可以采用其来实现。
有状态
StateListDrawable
<<selector
用于有状态交互时的View设置,比如 按下时 的背景,松开时 的背景,有焦点时的背景灯;
LevelListDrawable
<<level-list
根据等级(level)来切换不同的
Drawble
。在View中可以通过设置 setImageLevel 更改不同的Drawable
;
ScaleDrawable
<<scale
根据不同的等级(level)指定
Drawable
缩放到一定比例;
ClipDrwable
<<clip
根据当前等级(level)来裁剪
Drawable
;
常见的Drawable
BitmapDrawable
常见使用场景
用于表示一张图片,用于设置 bitmap
在 BitmapDrawable
区域内的绘制方式时使用,如水平平铺或者竖直平铺以及扩展铺满。
xml中的标签:
常见的属性有如下:
android:src
资源id
android:antialias
开启图片抗锯齿,用于让图片变得平滑,同时抗锯齿也会一定程度上降低图片清晰度,不过幅度几乎无法感知;
android:dither
开启抖动效果,为低像素机型做的自动降级显示,保证显示效果。比如当前图片彩色模式为ARGB8888,而设备屏幕色彩模式为RGB555,此时开启抖动就可以避免图片显示失真;
android:filter
过滤效果。在图片尺寸被拉伸或者压缩时,有助于保持显示效果;
android:gravity
当前图片小于容器尺寸时,此选项便于对图片进行定位,当titleMode开启时,此属性将失效;
android:mipMap
纹理映射开关,主要是为了应对图片大小缩放处理时,Android可以通过纹理映射技术提前按缩小的层级生成图片预存储在内存中,以此来提高速度与图片质量。默认情况下,mipmap文件夹里的默认开启此开关,drawable默认关闭。但需要注意,此开关只能建议系统开启此功能,至于最终是否真正开启,取决于系统。
android:tileMode
用于设置图片的平铺模式,有以下几个值:[
disabled
、clamp
、repeat
、mirror
]
disabled
(默认值) 关闭平铺模式clamp
图片四周的像素会扩展到周围区域repeat
水平和竖直方向上的平铺效果mirror
在水平和竖直方向的的镜面投影效果
示例代码:
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.ic_doge)
val drawable = BitmapDrawable(bitmap).apply {
setTileModeXY(Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
isFilterBitmap = true
gravity = Gravity.CENTER
setMipMap(true)
setDither(true)
}
ivDrawable.background = drawable
<?xml version="1.0" encoding="utf-8"?>
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
android:dither="true"
android:filter="true"
android:gravity="center"
android:mipMap="true"
android:src="@drawable/test"
android:tileMode="repeat" />
ShapeDrawable
常见使用场景
通过颜色来构造图形,作为背景描边或者背景色渐变时使用,可以说是最常见的一种 Drawable
。
xml中的标签:
常见的属性如下:
shape
表示图形的形状,如下四个选项:
rectangle
(矩形)、oval
(椭圆)、line
(横线)、ring
(圆环)
corners
表示shape的四个角的角度,只适用于矩形shape。
- android:
radius
为四个角设置相同的角度 - android:
topLeftRadius
设置左上角角度 - android:
bottomLeftRadius
设置左下角角度 - android:
bottomRightRadius
设定右下角的角度
- android:
gradient
用于表示渐变效果,与 标签互斥(其表示纯色填充)
- android:
angle
渐变的角度,默认为0,其值必须为45的倍数, 0表示从左向右,90表示从下到上 - android:
centerX
渐变中心点的横坐标 - android:
centerY
渐变中心点纵坐标 - android:
startColor
渐变的起始色 - android:
centerColor
渐变的中间点 - android:
endColor
渐变的结束色 - android:
gradientRadius
渐变半径,仅当android:type=“radial”时有效 - android:
useLevel
是否使用等级区分,在StateListDrawable
时有效,默认 false - android:
type
渐变类型,linear
(线性渐变)、radial
(径向渐变)、sweep
- android:
solid 表示纯色填充
stroke 用于设置描边
- android:
width
描边宽度 - android:
color
描边颜色 - android:
dashWidth
描边虚线时的宽度 - android:
dashGap
描边虚线时的间隔
- android:
padding
用于表示空白,其代表了在View中使用时的空白。但其在shape中并没有什么作用,可以在
layer-list
中进行使用。
size
用于表示
shape
的 固有大小 ,但其不代表shape最终显示的大小。因为对于shape来说,其没有宽/高的概念,因为其最终被设置到View上作为背景时,其会被自动拉伸或缩放。但作为drawable,它拥有着固有宽/高,即通过getIntrinsicWidth
,getIntrinsicHeight
获取。对于某些Drawable而言,比如BitMapDrawable时,其宽高就是图片大小;而对于shape时,其就不存在大小,默认为-1。当然你也可以通过 size 设置大小,但其最终代表的是shape的固有宽高,作为背景时其并不是最终显示时的宽高。
示例如下:
LayerDrawable
表示一种层次化的集合 drawable
,一般常见于需要多个 Drawable
叠加 摆放效果时使用。
一个 layer-list
中可以包含多个 item ,每个item表示一个 Drawable
,其常用的属性 android:top
,left
,right
,bottom
。相当于相对View的 上下左右 偏移量,单位为像素。此外也可以通过 Drawable
引用一个已有的 Drwable
资源。
xml中的标签:
示例如下:
StateListDrawable
用于为不同的 View状态 引用不同的 Drawable
,比如在View 按下 时,View 禁用 时等。
xml中的标签:
常用的属性如下:
constantSize
表示其固有大小是否随着状态而改变。
因为每次切换状态时,都会伴随着
Drawable
的改变,如果此时不是用于背景,则如果Drawable
的固有大小不一致,则会导致StateListDrawable
的大小发生变化。如果此值为 true ,则表示当前StateDrawable
的固有大小是当前其内部所有Drawable
中最大值。反之,则根据状态决定;
android:dither
是否开启抖动效果,用于在低质量屏幕上获得较好的显示效果;
variablePadding
表示padding是否随着状态而改变,true表示跟随状态而决定,取决于当前显示的drawable,false则选取drawable集合中padding最大值。
示例如下:
LevelListDrawable
用于根据不同的等级表示一个 Drawable
集合。
默认等级范围为0,最小为0,最大为10000,可以在View中使用 Drawable
从而设置相应的 level 切换不同的 Drawable
。如果这个drawable被用于ImageView 的 前景Drawable,还可以通过 ImageView.setImageViewLevel 来切换。
xml中的标签:
示例代码如下:
在代码中即可通过 setLevel切换。
view.background.level = 10
view.background.level = 200
TransitaionDrawable
用于实现两个 Drawable
之间的淡入淡出效果。
xml中的标签:
示例如下:
InsetDrawable
用于将其他 Drawable
内嵌到自己当中,并可以在四周留出一定的间距。比如当某个 View
希望自己的背景比自己的实际区域小时,可以采用这个 Drawable
,当然采用 LayerDrawable
也可以实现。
xml中的标签:
其属性分别如下:
- android:inset 表示四边内凹大小
- android:insetTop 表示顶部内凹大小
- android:insetLeft 表示左边内凹大小
- android:insetBottom 表示底部内凹大小
- android:insetRight 表示右边内凹大小
ScaleDrawable
用于根据等级(level
)将指定的 Drawable
缩放到一定比例。
xml中的标签:
相应的属性如下所示:
android:scaleGravity
类似于与android:gravity
android:scaleWidth
指定宽度的缩放比例(相对于原drawable缩放了多少)
android:scaleHeight
指定高度的缩放比例(相对于原drawable缩放了多少)
android:level(minSdk>=
24
)
指定缩放等级,默认为0,即最小,最高10000,此值越大,最终显示的drawable越大
需要注意的是,当level为0时,其不会显示,所以我们使用ScaleDrawable时,需要在代码中,将 drawable.level 调为1。
示例如下:
<?xml version="1.0" encoding="utf-8"?>
<scale xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/level2_drawable"
android:level="1"
android:scaleWidth="70%"
android:scaleHeight="70%"
android:scaleGravity="center" />
ClipDrawable
用于根据当前等级(level)来裁剪另一个 Drawable
。
xml中的标签:
具体属性如下:
android:drawable
需要裁剪的drawable
android:clipOrientation
裁剪方向,有水平(horizontal)、竖直(vertical) 两种
android:level(minSdk>=
24
)
设置等级,为0时表示完全裁剪(即隐藏),值越大,裁剪的越小。最大值10000(即不裁剪,原图)。
android:gravity
参数 含义 top 内部drawable位于容器顶部,不改变大小。ps: 竖直裁剪时,则从底部开始裁剪。 bottom 内部drawable位于容器底部,不改变大小。ps: 竖直裁剪时,则从顶部开始裁剪。 left(默认值) 内部drawable位于容器底部,不改变大小。ps: 水平裁剪时,则从顶部开始裁剪。 right 内部drawable位于容器右边,不改变大小。ps: 水平裁剪时,从右边开始裁剪。 start 同left end 同right center 使内部drawable在容器中居中,不改变大小。 ps:竖直裁剪时,从上下同时开始裁剪;水平裁剪时,从左右同时开始。 center_horizontal 内部的drawable在容器中水平居中,不改变大小。 ps:水平裁剪时,从左右两边同时开始裁剪。 center_vertical 内部的drawable在容器中垂直居中,不改变大小。 ps:竖直裁剪时,从上下两边同时开始裁剪。 fill 使内部drawable填充满容器。 ps:仅当level为0(0表示ClipDrawable被完全裁剪,即不可见)时,才具有裁剪行为。 fill_horizontal 使内部drawable在水平方向填充容器。 ps:如果水平裁剪,仅当level为0时,才具有裁剪行为。 fill_vertical 使内部drawable在竖直方向填充容器。 ps:如果垂直裁剪,仅当level为0时,才具有裁剪行为。 clip_horizontal 竖直方向裁剪。 clip_vertical 竖直方向裁剪。
示例如下:
自定义Drawable
通常情况下,我们往往用不到自定义 Drawable
,主要源于Android已经提供了很多通常会用到的功能,不过了解自定义 Drawable
在某些场景下可以非常便于我们开发体验。
自定义 Drawable
也很简单,我们只需要继承 Drawable
即可,从而实现:
draw()
实现自定义的绘制。
如果要获取当前
Drawable
绘制的边界大小,可以通过 getBounds() 获取;
如果需要获取当前
Drawable
的中心点,也可以通过 getBounds().exactCenterX() ,或者 getBounds().centerX() ,区别在于前者用于获取精确位置;
setAlpha()
设置透明度;
setColorFilter()
设置滤镜效果;
getOpacity()
返回当前
Drawable
的透明度;
比如画一个类似的 ProgressBar
,因为其是一个 Drawable
,所以可以用作任意的 View
。
class CustomCircleProgressDrawable : Drawable(), Animatable {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val rectF = RectF()
private var progress = 0F
private val valueAnimator by lazy(LazyThreadSafetyMode.NONE) {
ValueAnimator.ofFloat(0f, 1f).apply {
duration = 2000
repeatCount = Animation.INFINITE
interpolator = LinearInterpolator()
addUpdateListener {
progress = it.animatedValue as Float
invalidateSelf()
}
}
}
init {
paint.style = Paint.Style.STROKE
paint.strokeWidth = 10f
paint.strokeCap = Paint.Cap.ROUND
paint.color = Color.GRAY
start()
}
override fun draw(canvas: Canvas) {
var min = (min(bounds.bottom, bounds.right) / 2).toFloat()
paint.strokeWidth = min / 10
min -= paint.strokeWidth * 3
val centerX = bounds.exactCenterX()
val centerY = bounds.exactCenterY()
rectF.left = centerX - min
rectF.right = centerX + min
rectF.top = centerY - min
rectF.bottom = centerY + min
paint.color = Color.GRAY
canvas.drawArc(rectF, -90f, 360f, false, paint)
paint.color = Color.GREEN
canvas.rotate(360F * progress, centerX, centerY)
canvas.drawArc(rectF, -90F, 30F + 330F * progress, false, paint)
}
override fun setAlpha(alpha: Int) {
paint.alpha = alpha
invalidateSelf()
}
override fun setColorFilter(colorFilter: ColorFilter?) {
paint.colorFilter = colorFilter
invalidateSelf()
}
override fun getOpacity(): Int {
return PixelFormat.TRANSLUCENT
}
override fun start() {
if (valueAnimator.isRunning) return
valueAnimator.start()
}
override fun stop() {
if (valueAnimator.isRunning) valueAnimator.cancel()
}
override fun isRunning(): Boolean {
return valueAnimator.isRunning
}
}
原理也很简单,我们实现了 onDraw
方法,在其中利用 canvas
绘制了两个圆环,其中前者是作为背景,后者不断地利用属性动画进行变化,并且不断旋转 canvas
,从而实现类似进度条的效果。
效果如下:
实践推荐
比如我们现在有这样一个 View
,需要在左上角展示一个文字,背景是张图片,另外还有一个从顶部到下的半透明渐变阴影。
如下图所示:
一般情况下,我们肯定会不假思索的写出如下代码。
上述写法没有问题,但其并不是所有场景的最推荐方式。比如这种样式此时需要在 RecyclerView
中展示呢?
所以,此时就可以利用 Drawable
简化我们的 View
层级,改造思路如下:
如上所示,相对于最开始,我们将布局层级由 3 层降低为了 1 层,对于性能的提升也将指数级上升。
现在有同学可能要问了,你的这个 View
很简单,自定义一个 Drawable
还好说,那 View
很复杂呢?难不成我真要纯纯自定义吗?
要回答这个问题,其实很简单,我们要明确 Drawable
的意义,其只是一个可绘制的图像 。过于复杂的View,我们可以将其拆分为多个层级,然后对于其中纯展示的View,使用 Drawable
降低其复杂度。
从某个角度来说,Drawable也可以是我们自定义View的好帮手。
总结
合理利用 Drawable
会很大程度提高我们的使用体验。相应的,对于布局优化,Drawable
也是一大利器。问题回到文章最开始,如果现在再问你。Drawable
到底是什么? 如何自定义一个 Drawable
? 如何利用其做一些骚操作?我想,这都不是问题。
参考
链接:https://juejin.cn/post/7148630011010875422
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Flutter 动画剖析(一) 彻底掌握动画的使用
动画定义
早期的动画片是利用大量图片进行快速切换从而达到一种看似连续的动画效果,这就是最早期的帧动画,利用人的视觉延迟产生的一种连续的效果,其实现在的动画也是这个原理,在同一时间屏幕进行多次有规律的渲染次数,渲染次数越多,动画就越流畅,也就是我们平常说的屏幕刷新率。
本篇文章主旨让大家在使用Flutter动画的过程中游刃有余,不对具体源码进行解析。
- 动画关键属性:动画时长、动画轨迹。
动画其实就是对象在规定的时间内进行的特定规则运动的表现。所以我们需要关心的核心属性就是动画时长和动画轨迹。
理解了动画实现的原理,所谓万变不离其宗,Flutter动画亦是如此,下面首先看下Flutter
中使用动画的几个关键类。
动画核心类:
AnimationController 动画控制器
用来设置动画时长、动画开始结束数值、控制开始结束动画等操作。
构造方法以及常用方法:
_controller = AnimationController(
vsync: this,//设置Ticker 动画帧的回调函数
duration: const Duration(milliseconds: 2000),// 正向动画时长 //2s
reverseDuration: const Duration(milliseconds: 2000),// 反向动画时长 //2s
lowerBound: 0,// 开始动画数值 double类型
upperBound: 1.0,// 结束动画数值 double类型
animationBehavior: AnimationBehavior.normal ,// 动画器行为 是否重复动画 两个枚举值
debugLabel: "缩放动画",// 调试标签 动画过多时方便调式,toString时显示
// _controller.toString;
//输出: AnimationController#9d121(▶ 0.000; for 缩放动画)➩Tween<double>(0.0 → 1.0)➩0.0
);
// 常用方法:
// 监听动画运动
_controller.addListener(() { });
// 监听动画开始、停止等状态
_controller.addStatusListener((status) {
// dismissed 动画在起始点停止
// forward 动画正在正向执行
// reverse 动画正在反向执行
// completed 动画在终点停止
if (status == AnimationStatus.completed) {
_controller.reverse(); //反向执行 100-0
} else if (status == AnimationStatus.dismissed) {
_controller.forward(); //正向执行 0-100
}
});
// 启动动画
// _controller.forward();//正向开始动画
// _controller.reverse();//反向开始动画
_controller.repeat(); // 无限循环开始动画
vsync参数需要类混入:
SingleTickerProviderStateMixin
或TickerProviderStateMixin
,如果页面内只有一个动画控制器使用第一个,多个控制器使用第二个。
AnimatedBuilder 实现动画组件核心类
一般情况下,我们需要将需要有动画效果的组件上包裹一层AnimatedBuilder
从而监听动画控制器更新数据,内部也是通过有状态部件监听不断刷新页面实现。
构造方法:
const AnimatedBuilder({
Key? key,
required Listenable animation,//动画控制器
required this.builder,// 返回动画
this.child,// 传递给build的child子组件
})
以上两个组件就可以实现简单的动画效果了,下面我们使用AnimationController
和 AnimatedBuilder
实现一个简单的缩放动画。使FlutterLogo
组件大小不断变化。
// 开启动画
_controller.repeat(); // 无限循环开始动画
Center(
child: AnimatedBuilder(
child: FlutterLogo(),
animation: _controller,
builder: (context, child) {
return Container(
width: 100 * _controller.value,
height: 100 * _controller.value,
child: child,
);
}),
)
效果:
可以看到组件通过控制器0-1不断变化,logo大小也发生了变化,也就简单实现了缩放的效果。
注:AnimatedBuilder
是实现的局部组件刷新,并不会触发本身的build
方法。
Animation<T> 声明动画
以上我们通过控制器实现了简单的缩放动画效果,但是我们发现开始和结束的数据只能是double
类型的数字,中间的变化状态是需要我们来进行计算的,如果遇到较为复杂的过渡变化,计算也会同样变得复杂,那么为了解决这个问题,Animation<T>
应运而生,该类主要用来声明控制动画运动的数据类,数据为泛型类型,可自定义。
有了这个类,我们就可以实现自定义数据算法的过渡效果,例如颜色的渐变。
Animation
一般和Tween
配合使用。
Tween<T> 实现声明动画
为对象在开始和结束中间运动时变化的过程类,也称为补间动画,泛型和Animation
一致,通过Animation
给定泛型,生成Tween
类设置动画开始和动画结束数据并调用animate
方法设置动画控制器。
一般情况,动画开始和结束我们用0 ~ 1 表示,实际上,我们也可以使用其他数据设置开始和结束数据。例如颜色过渡ColorTween
、大小过渡SizeTween
、矩形过渡RectTween
变化等,而无需关心运动过程中的数值是如何计算的,因为这些官方已经帮我们计算好了。
源码中可以看到,这些扩展的Tween
类都实现了lerp
方法。
例如颜色渐变实现:
/// 返回0 ~ 1 颜色渐变过程中的色值。
@override
Color? lerp(double t) => Color.lerp(begin, end, t);
static Color? lerp(Color? a, Color? b, double t) {
///...略
// 颜色渐变算法 具体算法可以翻代码自行查看 都是数学知识
return Color.fromARGB(
_clampInt(_lerpInt(a.alpha, b.alpha, t).toInt(), 0, 255),
_clampInt(_lerpInt(a.red, b.red, t).toInt(), 0, 255),
_clampInt(_lerpInt(a.green, b.green, t).toInt(), 0, 255),
_clampInt(_lerpInt(a.blue, b.blue, t).toInt(), 0, 255),
);
}
举例:
// 开始动画
_controller.repeat(reverse: true); // 无限循环开始动画 结束倒放为true
/// 颜色渐变动画
Animation<Color?> animation;// 声明动画,数据为Color颜色
animation = ColorTween(begin: Colors.red, end: Colors.yellow).animate(_controller);// 实现动画,设置动画由红向黄渐变
// Size大小变化
Animation<Size?> animation2;
animation2 = SizeTween(begin: Size(100,50), end:Size(50,100)).animate(cure);
效果:
当然我们也可以自定义补间动画的过程,实现lerp
方法,这里就考验你数学知识的掌握了,就不展开了,掌握原理即可。
Curve & CurvedAnimation 动画运动曲线
上面的动画效果虽然实现了复杂过程的变化,但是还缺少我们动画的核心属性,就是运动轨迹,因为上面没有设置,默认的运动轨迹是线性变化的,所以给我们的效果都是非常平稳的。如果实现更为丰富的动画效果,那么Curve
应运而生,Curve
是一个数值转换器,可以理解为方程式,默认y=x;它可以让我们运动过程不再是匀速变化,而是让运动过程可以拥有加速、减速、越界等变化,并且Curves
里内置了非常丰富的运动轨迹可以直接使用。 在之前的文章也有用到过。
CurvedAnimation
是Curve
类的具体使用,将非线性曲线应用到动画中,使用也非常的简单。只需要将动画控制器赋值给CurvedAnimation
,上方Tween
的animate
方法里设置 CurvedAnimation
即可。
代码:
//构造
CurvedAnimation({
required this.parent,// 动画控制器
required this.curve,// 正向动画曲线
this.reverseCurve,// 反向动画曲线
});
//自定义运动曲线
CurvedAnimation cure = CurvedAnimation(parent: _controller, curve: Curves.easeIn);
// 使用 将_controller替换为cure
Animation<Color> animation;
animation = Tween<Color>(begin: Colors.black, end: Colors.white).animate(cure);
可以看到下方有非常丰富的曲线效果。
源码注释里有mp4效果演示,想方便了解效果可以看这篇文章。
Flutter 动画曲线Curves 效果一览。
自定义Curves
随便点击去一个Cubic类,实现方法很简单,
从源码中可以看到 Curve
里有可以实现两个方法,官方建议实现transformInternal
方法,因为transform
方法内部直接返回了transformInternal
这个方法。
那么实现就很简单了,定义一个类,继承Curves
,实现transformInternal
方法即可。transformInternal
就可以将我们给定的数值转换为我们想要的数值。
class MyCurve extends Curve {
@override
double transformInternal(double x) {
// 自定义变化曲线
// 默认 y= x; 线性运动
// y = x的立方。这里可以理解为定义方程 x可以理解为0-1的变化过程
// y即是返回0-1变化的的自定义算法,无需关心具体的动画运动轨迹是如何计算的。
return x * x * x;
}
}
注:Curve
只负责0 ~ 1(也就是动画开始 ~ 动画结束)的变化曲线,无论任何数据驱动的动画我们都可以用0 ~ 1来表示运动曲线。具体的过渡算法我们无需关心,那是补间动画需要做的事情,内置的补间动画Flutter
已经帮我们算了,使用也非常的方便。
其他 内置常用动画组件的使用
其实在Flutter中还内置了我们常用的动画效果组件,例如平移、渐变、缩放等组件,实现过程原理基本相当,区别是我们不需要自己计算了,直接设置动画器即可。
部分内置动画使用:
1、平移动画 SlideTransition
根据组件自身大小进行平移,接收Offset
数据,分别代表自身组件大小的倍数。
// 构造
const SlideTransition({
super.key,
required Animation<Offset> position,
this.transformHitTests = true,
this.textDirection,//阅读习惯方向
this.child,
})
2、渐变动画 FadeTransition
渐变动画一般指组件透明度渐变效果,接收double
类型,0 ~ 1为完全透明 ~ 完全不透明。
const FadeTransition({
super.key,
required Animation<double> opacity,
this.alwaysIncludeSemantics = false,
super.child,
})
3、缩放动画 ScaleTransition
缩放动画接收double
数据,0 ~ 1 为最小到最大,可以指定缩放中心。
const ScaleTransition({
super.key,
required Animation<double> scale,
this.alignment = Alignment.center,//缩放中心
this.filterQuality,//过滤器质量
this.child,
})
4、旋转动画 RotationTransition
旋转动画一般指平面二维的旋转,接收double
参数,0 ~1为旋转一圈,同样可指定旋转中心。
const RotationTransition({
super.key,
required Animation<double> turns,
this.alignment = Alignment.center,
this.filterQuality,
this.child,
})
5、3D旋转动画
3D动画系统没有现成的,需要我们使用矩阵变换自行计算,其实也很简单,通过Matrix4
类设置矩阵变换即可,下方为绕y轴进行旋转,范围是0~2pi。
AnimatedBuilder(
child: child,
animation: animation,
builder: (context, child) {
return Transform(
alignment: Alignment.center, //相对于坐标系原点的对齐方式
transform: Matrix4.identity()
..rotateX(0)//x轴不变
..rotateY(animation.value),//绕y轴旋转,0-2pi
child: Container(width: 100, height: 100, child: child));
});
5、组合动画
将上面所有动画效果组合起来也很简单,将以上动画通过子组件进行嵌套即可,最终的子组件为我们动画所需的组件。
核心代码:
animation = Tween(begin: 0.0, end: 1.0).animate(cure);
animation2 = Tween<Offset>(begin: Offset(0.0, 0.0), end: Offset(1.0, 0.0)).animate(cure);
animation3 = Tween(begin: 0.0, end: 1.0).animate(cure);
animation4 = Tween(begin: 0.0, end: pi * 2).animate(cure);
// // 平移
SlideTransitionLogo(
animation: animation2,
// 渐变
child: FadeTransition(
opacity: animation3,
// 二维旋转
child: RotationTransitionLogo(
// 缩放
child: ScaleTransition(
// 3D旋转
child: AnimatedBuilder(
child: FlutterLogo(),
animation: animation4,
builder: (context, child) {
return Transform(
alignment: Alignment.center, //相对于坐标系原点的对齐方式
transform: Matrix4.identity()
..rotateX(0)
..rotateY(animation4.value),
child: Container(width: 100, height: 100, child: child));
}),
scale: animation,
),
animation: animation3,
),将
),
),
效果:
其实系统内置的还有些其他现成的动画效果,有兴趣的小伙伴可以自己研究下,原理基本相同。
自定义动画效果
自定义动画一般和自绘制结合使用,根据绘制的组件和动画的运动曲线来达到我们想要的效果。下一篇有时间剖析下动画与绘制结合使用的方法与细节。
总结
动画归根结底是让数据不断变化来驱使UI产生变化,重点的就是我们如何处理这个变化过程中的数据,以上是对于Flutter动画使用方面的一些总结,希望对你有所帮助~ 如有疑问,欢迎指正。
链接:https://juejin.cn/post/7154662336182583309
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Android性能优化 - 从SharedPreferences跨越到DataStore
再谈SharedPreferences
对于android开发者们来说,SharedPreferences已经是一个有足够历史的话题了,之所以还在性能优化这个专栏中再次提到,是因为在实际项目中还是会有很多使用到的地方,同时它也有足够的“坑”,比如常见的主进程阻塞,虽然SharedPreferences 提供了异步操作api apply,但是apply方法依旧有可能造成ANR。
public void apply() {
final long startTime = System.currentTimeMillis();
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
if (DEBUG && mcr.wasWritten) {
Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
+ " applied after " + (System.currentTimeMillis() - startTime)
+ " ms");
}
}
};
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
// 写入队列
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
// Okay to notify the listeners before it's hit disk
// because the listeners should always get the same
// SharedPreferences instance back, which has the
// changes reflected in memory.
notifyListeners(mcr);
}
我们可以看到我们的runnable被写入了队列,而这个队列会在handleStopService() 、handlePauseActivity() 、 handleStopActivity() 的时候会一直等待 apply() 方法将数据保存成功,否则会一直等待,从而阻塞主线程造成 ANR。
@Override
public void handlePauseActivity(ActivityClientRecord r, boolean finished, boolean userLeaving,
int configChanges, PendingTransactionActions pendingActions, String reason) {
if (userLeaving) {
performUserLeavingActivity(r);
}
r.activity.mConfigChangeFlags |= configChanges;
performPauseActivity(r, finished, reason, pendingActions);
// Make sure any pending writes are now committed.
if (r.isPreHoneycomb()) {
// 这里就是元凶
QueuedWork.waitToFinish();
}
mSomeActivitiesChanged = true;
}
谷歌官方也有解释
虽然QueuedWork在android 8中有了新的优化,但是实际上依旧有ANR的出现,在低版本的机型上更加出现频繁,所以我们不可能把sp真的逃避掉。
目前业内有很多替代的方案,就是采用MMKV去解决,但是官方并没有采用像mmkv的方式去解决,而是另起炉灶,在jetpack中引入DataStore去替代旧时代的SharedPreferences。
DataStore
Jetpack DataStore 是一种数据存储解决方案,允许您使用协议缓冲区存储键值对或类型化对象。DataStore 使用 Kotlin 协程和 Flow 以异步、一致的事务方式存储数据。
DataStore 提供两种不同的实现:Preferences DataStore 和 Proto DataStore(基于protocol buffers)。我们这里主要以Preferences DataStore作为分析,同时在kotlin中,datastore采取了flow的良好架构,进行了内部的调度实现,同时也提供了java兼容版本(采用RxJava实现)
使用例子
val Context.dataStore : DataStore<Preferences> by preferencesDataStore(“文件名”)
因为datastore需要依靠协程的环境,所以我们可以有以下方式
读取
CoroutineScope(Dispatchers.Default).launch {
context.dataStore.data.collect {
value = it[booleanPreferencesKey(key)] ?: defValue
}
}
写入
CoroutineScope(Dispatchers.IO).launch {
context.dataStore.edit { settings ->
settings[booleanPreferencesKey(key) ] = value
}
}
其中booleanPreferencesKey代表着存入的value是boolean类型,同样的,假设我们需要存入的数据类型是String,相应的key就是通过stringPreferencesKey(key名) 创建。同时因为返回的是flow,我们是需要调用collect这种监听机制去获取数值的改变,如果想要像sp一样采用同步的方式直接获取,官方通过runBlocking进行获取,比如
val exampleData = runBlocking { context.dataStore.data.first() }
DataStore原理
DataStore提供给了我们非常简洁的api,所以我们也能够很快速的入门使用,但是其中的原理实现,我们是要了解的,因为其创建过程十分简单,我们就从数据更新(context.dataStore.edit)的角度出发,看看DataStore究竟做了什么。
首先我们看到edit方法
public suspend fun DataStore<Preferences>.edit(
transform: suspend (MutablePreferences) -> Unit
): Preferences {
return this.updateData {
// It's safe to return MutablePreferences since we freeze it in
// PreferencesDataStore.updateData()
it.toMutablePreferences().apply { transform(this) }
}
}
可以看到edit方法是一个suspend的函数,其主要的实现就是依靠updateData方法的调用
interface DataStore<T> 中:
public suspend fun updateData(transform: suspend (t: T) -> T): T
我们分析到DataStore是有两种实现,我们要看的就是Preferences DataStore的实现,其实现类是
internal class PreferenceDataStore(private val delegate: DataStore<Preferences>) :
DataStore<Preferences> by delegate {
override suspend fun updateData(transform: suspend (t: Preferences) -> Preferences):
Preferences {
return delegate.updateData {
val transformed = transform(it)
// Freeze the preferences since any future mutations will break DataStore. If a user
// tunnels the value out of DataStore and mutates it, this could be problematic.
// This is a safe cast, since MutablePreferences is the only implementation of
// Preferences.
(transformed as MutablePreferences).freeze()
transformed
}
}
}
可以看到PreferenceDataStore中updateData方法的具体实现其实在delegate中,而这个delegate的创建是在
PreferenceDataStoreFactory中
public fun create(
corruptionHandler: ReplaceFileCorruptionHandler<Preferences>? = null,
migrations: List<DataMigration<Preferences>> = listOf(),
scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
produceFile: () -> File
): DataStore<Preferences> {
val delegate = DataStoreFactory.create(
serializer = PreferencesSerializer,
corruptionHandler = corruptionHandler,
migrations = migrations,
scope = scope
) {
忽略
}
return PreferenceDataStore(delegate)
}
DataStoreFactory.create方法中:
public fun <T> create(
serializer: Serializer<T>,
corruptionHandler: ReplaceFileCorruptionHandler<T>? = null,
migrations: List<DataMigration<T>> = listOf(),
scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
produceFile: () -> File
): DataStore<T> =
SingleProcessDataStore(
produceFile = produceFile,
serializer = serializer,
corruptionHandler = corruptionHandler ?: NoOpCorruptionHandler(),
initTasksList = listOf(DataMigrationInitializer.getInitializer(migrations)),
scope = scope
)
}
DataStoreFactory.create 创建的其实是一个SingleProcessDataStore的对象,SingleProcessDataStore同时也是继承于DataStore,它就是所有DataStore背后的真正的实现者。而它的updateData方法就是一切谜团解决的钥匙。
override suspend fun updateData(transform: suspend (t: T) -> T): T {
val ack = CompletableDeferred<T>()
val currentDownStreamFlowState = downstreamFlow.value
val updateMsg =
Message.Update(transform, ack, currentDownStreamFlowState, coroutineContext)
actor.offer(updateMsg)
return ack.await()
}
我们可以看到,update方法中,有一个叫 ack的 CompletableDeferred对象,而CompletableDeferred,是继承于Deferred。我们到这里就应该能够猜到了,这个Deferred对象不正是我们协程中常用的异步调用类嘛!它提供了await操作允许我们等待异步的结果。
最后封装好的Message被放入actor.offer(updateMsg) 中,actor是消息处理类对象,它的定义如下
internal class SimpleActor<T>(
/**
* The scope in which to consume messages.
*/
private val scope: CoroutineScope,
/**
* Function that will be called when scope is cancelled. Should *not* throw exceptions.
*/
onComplete: (Throwable?) -> Unit,
/**
* Function that will be called for each element when the scope is cancelled. Should *not*
* throw exceptions.
*/
onUndeliveredElement: (T, Throwable?) -> Unit,
/**
* Function that will be called once for each message.
*
* Must *not* throw an exception (other than CancellationException if scope is cancelled).
*/
private val consumeMessage: suspend (T) -> Unit
) {
private val messageQueue = Channel<T>(capacity = UNLIMITED)
我们看到,我们所有的消息会被放到一个叫messageQueue的Channel对象中,Channel其实就是一个适用于协程信息通信的线程安全的队列。
最后我们回到主题,offer函数干了什么
省略前面
do {
// We don't want to try to consume a new message unless we are still active.
// If ensureActive throws, the scope is no longer active, so it doesn't
// matter that we have remaining messages.
scope.ensureActive()
consumeMessage(messageQueue.receive())
} while (remainingMessages.decrementAndGet() != 0)
其实就是通过consumeMessage消费了我们的消息。到这里我们再一次回到我们DataStore中的SimpleActor实现对象
private val actor = SimpleActor<Message<T>>(
scope = scope,
onComplete = {
it?.let {
downstreamFlow.value = Final(it)
}
// We expect it to always be non-null but we will leave the alternative as a no-op
// just in case.
synchronized(activeFilesLock) {
activeFiles.remove(file.absolutePath)
}
},
onUndeliveredElement = { msg, ex ->
if (msg is Message.Update) {
// TODO(rohitsat): should we instead use scope.ensureActive() to get the original
// cancellation cause? Should we instead have something like
// UndeliveredElementException?
msg.ack.completeExceptionally(
ex ?: CancellationException(
"DataStore scope was cancelled before updateData could complete"
)
)
}
}
) {
consumeMessage 实际
msg ->
when (msg) {
is Message.Read -> {
handleRead(msg)
}
is Message.Update -> {
handleUpdate(msg)
}
}
}
可以看到,consumeMessage其实就是以lambada形式展开了,实现的内容也很直观,如果是Message.Update就调用了handleUpdate方法
private suspend fun handleUpdate(update: Message.Update<T>) {
// 这里就是completeWith调用,也就是回到了外部Deferred的await方法
update.ack.completeWith(
runCatching {
when (val currentState = downstreamFlow.value) {
is Data -> {
// We are already initialized, we just need to perform the update
transformAndWrite(update.transform, update.callerContext)
}
...
最后通过了transformAndWrite调用writeData方法,写入数据(FileOutputStream)
internal suspend fun writeData(newData: T) {
file.createParentDirectories()
val scratchFile = File(file.absolutePath + SCRATCH_SUFFIX)
try {
FileOutputStream(scratchFile).use { stream ->
serializer.writeTo(newData, UncloseableOutputStream(stream))
stream.fd.sync()
// TODO(b/151635324): fsync the directory, otherwise a badly timed crash could
// result in reverting to a previous state.
}
if (!scratchFile.renameTo(file)) {
throw IOException(
"Unable to rename $scratchFile." +
"This likely means that there are multiple instances of DataStore " +
"for this file. Ensure that you are only creating a single instance of " +
"datastore for this file."
)
}
至此,我们整个过程就彻底分析完了,读取数据跟写入数据类似,只是最后调用的处理函数不一致罢了(consumeMessage 调用handleRead),同时我们也分析出来handleUpdate的update.ack.completeWith让我们也回到了协程调用完成后的世界。
SharedPreferences全局替换成DataStore
分析完DataStore,我们已经有了足够的了解了,那么是时候将我们的SharedPreferences迁移至DataStore了吧!
旧sp数据迁移
已存在的sp对象数据可以通过以下方法无缝迁移到datastore的世界
dataStore = context.createDataStore( name = preferenceName, migrations = listOf( SharedPreferencesMigration( context, "sp的名称" ) ) )
无侵入替换sp为DataStore
当然,我们项目中可能会存在很多历史遗留的sp使用,此时用手动替换会容易出错,而且不方便,其次是三方库所用到sp我们也无法手动更改,那么有没有一种方案可以无需对原有项目改动,就可以迁移到DataStore呢?嗯!我们要敢想,才敢做!这个时候就是我们的性能优化系列的老朋友,ASM登场啦!
我们来分析一下,怎么把
val sp = this.getSharedPreferences("test",0)
val editor = sp.edit()
editor.putBoolean("testBoolean",true)
editor.apply()
替换成我们想要的DataStore,不及,我们先看一下这串代码的字节码
LINENUMBER 24 L2
ALOAD 0
LDC "test"
ICONST_0
INVOKEVIRTUAL com/example/spider/MainActivity.getSharedPreferences (Ljava/lang/String;I)Landroid/content/SharedPreferences;
ASTORE 2
我们可以看到,我们的字节码中存在ALOAD ASTORE这种依赖于操作数栈环境的指令,就知道不能简单的实现指令替换,而是采用同类替换的方式去现实,即我们可以通过继承于SharedPreferences,在自定义SharedPreferences中实现DataStore的操作,严格来说,这个自定义SharedPreferences,其实就相当于一个壳子了。这种替换方式在Android性能优化-线程监控与线程统一也有使用到。
我们来看一下自定义的SharedPreferences操作,这里以putBoolean相关操作举例子
class DataPreference(val context: Context,name:String):SharedPreferences {
val Context.dataStore : DataStore<Preferences> by preferencesDataStore(name)
override fun getBoolean(key: String, defValue: Boolean): Boolean {
var value = defValue
runBlocking {
}
runBlocking {
context.dataStore.data.first {
value = it[booleanPreferencesKey(key)] ?: defValue
true
}
}
// CoroutineScope(Dispatchers.Default).launch {
// context.dataStore.data.collect {
//
// value = it[booleanPreferencesKey(key)] ?: defValue
// Log.e("hello","value os $value")
// }
// }
return value
}
override fun edit(): SharedPreferences.Editor {
return DataEditor(context)
}
inner class DataEditor(private val context: Context): SharedPreferences.Editor {
override fun putBoolean(key: String, value: Boolean): SharedPreferences.Editor {
CoroutineScope(Dispatchers.IO).launch {
context.dataStore.edit { settings ->
settings[booleanPreferencesKey(key) ] = value
}
}
return this
}
override fun commit(): Boolean {
// 空实现即可
}
override fun apply() {
// 空实现即可
}
}
}
因为putBoolean中其实就已经把数据存好了,所有我们的commit/apply都可以以空实现的方式替代。同时我们也声明一个扩展函数
StoreTest.kt
fun Context.getDataPreferences(name:String,mode:Int): SharedPreferences {
return DataPreference(this,name)
}
字节码部分操作也比较简单,我们只需要把原本的 INVOKEVIRTUAL com/example/spider/MainActivity.getSharedPreferences (Ljava/lang/String;I)Landroid/content/SharedPreferences; 指令替换成INVOKESTATIC的StoreTestKt扩展函数getDataPreferences调用即可,同时由于接受的是SharedPreferences类型而不是我们的DataPreference类型,所以需要采用CHECKCAST转换。
static void spToDataStore(
MethodInsnNode node,
ClassNode klass,
MethodNode method
) {
println("init ===> " + node.name+" --"+node.desc + " " + node.owner)
if (node.name.equals("getSharedPreferences")&&node.desc.equals("(Ljava/lang/String;I)Landroid/content/SharedPreferences;")) {
MethodInsnNode methodHookNode = new MethodInsnNode(Opcodes.INVOKESTATIC,
"com/example/spider/StoreTestKt",
"getDataPreferences",
"(Landroid/content/Context;Ljava/lang/String;I)Landroid/content/SharedPreferences;",
false)
TypeInsnNode typeInsnNode = new TypeInsnNode(Opcodes.CHECKCAST, "android/content/SharedPreferences")
InsnList insertNodes = new InsnList()
insertNodes.add(methodHookNode)
insertNodes.add(typeInsnNode)
method.instructions.insertBefore(node, insertNodes)
method.instructions.remove(node)
println("hook ===> " + node.name + " " + node.owner + " " + method.instructions.indexOf(node))
}
}
方案的“不足”
当然,我们这个方案并不是百分比完美的
editor.apply()
sp.getBoolean
原因是如果采用这种方式apply()后立马取数据,因为我们替换后putBoolean其实是一个异步操作,而我们getBoolean是同步操作,所以就有可能没有拿到最新的数据。但是这个使用姿势本身就是一个不好的使用姿势,同时业内的滴滴开源Booster的sp异步线程commit优化也同样有这个问题。因为put之后立马get不是一个规范写法,所以我们也不会对此多加干预。不过对于我们DataStore替换后来说,也有更加好的解决方式
CoroutineScope(Dispatchers.Default).launch {
context.dataStore.data.collect {
value = it[booleanPreferencesKey(key)] ?: defValue
Log.e("hello","value os $value")
}
}
通过flow的异步特性,我们完全可以对value进行collect,调用层通过collect进行数据的收集,就能够做到万无一失啦(虽然也带来了侵入性)
总结
到这里,我们又完成了性能优化的一篇,sp迁移至DataStore的后续适配,等笔者有空了会写一个工具库(挖坑),虽然sp是一个非常久远的话题了,但是依旧值得我们分析,同时也希望DataStore能够被真正利用起来,适当的选用DataStore与MMKV。
链接:https://juejin.cn/post/7151719783842381832
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
常用到的几个Kotlin开发技巧,减少对业务层代码的入侵
善用@get/@set: JvmName()
注解并搭配setter/getter
使用
假设当前存在下面三个类代码:
#Opt1
public class Opt1 {
private String mContent;
public String getRealContent() {
return mContent;
}
public void setContent(String mContent) {
this.mContent = mContent;
}
}
#Opt2
public class Opt2 {
public void opt2(Opt1 opt1) {
System.out.println(opt1.getRealContent());
}
}
@Opt3
public class Opt3 {
public void opt3(Opt1 opt1) {
System.out.println(opt1.getRealContent());
}
}
这个时候我想将Opt1
类重构成kotlin,我们先看下通过AS的命令Convert Java File to Kotlin File
自动转换的结果:
可以看到为了兼容Opt2
和Opt3
的调用,直接把我的属性名给改成了realContent
,kotlin会自动生成getRealContent()
和setRealContent()
方法,这样Opt2
和Opt3
就不用进行任何调整了,kotlin这样就显得太过于智能了。
这样看起来没啥问题,但是java重构kotlin,直接把属性名给我改
了,并隐式生成了属性的set和get方法,对于java而言不使用的方法会报灰提示或者只有当前类使用AS会警告可以声明成private
,但是对于kotlin生成的set、get方法是隐式的,容易忽略。
所以大家在使用Convert Java File to Kotlin File
命令将java重构kotlin的结果一定不能抱有百分之百的信任,即使它很智能,但还是一定要细细的看下转换后的代码逻辑,可能还有不少的优化空间。
这个地方就得需要我们手动进行修改了,比如不想对外暴露修改这个字段的set方法,调整如下:
class Opt1 {
var realContent: String? = null
private set
}
再比如保持原有的字段名mContent
,不能被改为realContent
,同时又要保证兼容Opt2
和Opt3
类的调用不能报错,且尽量避免去修改里面的代码,我们就可以做如下调整:
class Opt1 {
@get: JvmName("getRealContent")
var mContent: String? = null
private set
}
善用默认参数
+@JvmOverloads
减少模板代码编写
假设当前Opt1
有下面的方法:
public String getSqlCmd(String table) {
return "select * from " + table;
}
且被Opt2
和Opt3
进行了调用,这个时候如果有另一个类Opt3
想要调用这个函数并只想从数据库查询指定字段,如果用java实现有两种方式:
- 直接在
getSqlCmd()
方法中添加一个查询字段参数,如果传入的值为null,就查询所有的字段,否则就查询指定字段:
public String getSqlCmd(String table, String name) {
if (TextUtils.isEmpty(name)) {
return "select * from " + table;
}
return "select " + name + " from " + table;
}
这样一来,是不是原本Opt2
和Opt3
对getSqlCmd()
方法调用是不是需要改动,多传一个参数给方法,而在日常的项目开发中,有可能这个getSqlCmd()
被几十个地方调用,难道你一个个的改过去?不太现实且是一种非常糟糕的实现。
- 直接在
Opt1
中新增一个getSqlCmd()
的重载方法,传入指定的字段去查询:
public String getSqlCmd(String table,String name) {
return "select " + name + " from " + table;
}
这样做的好处就是不用调整Opt2
和Opt3
对getSqlCmd(String table)
方法调用逻辑,但是会编写很多模板代码,尤其是getSqlCmd()
这个方法体可能七八十行的情况下。
如果Opt1
类代码减少即200-400行且不负责的情况下,我们可以将其重构成kotlin,借助于默认参数来实现方法功能增加又不用编写模板代码的效果(如果你的Java类上千行又很复杂,请谨慎转换成kotlin使用下面这种方式
)。
@JvmOverloads
fun getSqlCmd(table: String, name: String? = null): String {
return "select ${if (name.isNullOrEmpty()) "*" else name} from $table"
}
添加默认参数name
时还要添加@JvmOverloads
注解,这样是为了保证java只传一个table
参数也能正常调用。
通过上面这种方式,我们就能保证实现了方法功能增加,又不用改动Opt2
、Opt3
对于getSqlCmd()
方法的调用逻辑,并且还不用编写额外的模板代码,一举多得。
总结
本篇文章主要介绍了在java重构成kotlin过程中比较常用到的两个技巧,最终实现效果是减少对业务逻辑代码的入侵,希望能对你有所帮助。
链接:https://juejin.cn/post/7148823027608715295
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
源码被外包误上传到 GitHub,丰田近 30 万数据遭泄露?
不久前,据路透社报道,丰田的 T-Connect 服务中的约 296,019 条客户信息可能遭到了泄露,引发了不少车主的恐慌。对此,丰田最新发公告证实了这一事件的真实性,并表示「对于给您带来的不便和担忧,我们深感歉意」,而泄露的来源或许与第三方外包公司有关。
1 源码被发布到了 GitHub
首先值得注意的是,丰田 T-Connect 是这家汽车制造商的官方连接应用程序,它的主要功能是可以让丰田汽车车主将自己的智能手机与车辆的信息娱乐系统连接,可以共享电话、音乐、导航、通知、驾驶数据、发送机状态和油耗等功能。
2022 年 9 月 15 日,丰田发现 T-Connect 用户站点的某些源代码在 GitHub 平台发布,这些源代码包含了对数据服务器的访问密钥,而这些密钥用于访问存储在数据服务器上的电子邮件地址和客户管理号码。
这使得未经授权的第三方可以在 2017 年 12 月至 2022 年 9 月 15 日期间访问 296,019 名的客户的详细信息。
不过,就在这一天,丰田紧急对 GitHub 存储库的访问设置限制,并在 9 月 17 日对数据服务器访问密钥进行了更改,清除了未经授权的第三方的所有潜在访问。
2 这一次外包不是“背锅侠”
在发现泄露事件的同时,丰田公司也即刻做出了排查,发现在 2017 年 12 月,T-Connect 网站开发外包公司违反处理规则,错误地将部分源代码上传到 GitHub 上,但是直到 2022 年 9 月 15 日才发现。
这也意味着,用户信息在这五年间都有外泄的风险。为此,丰田解释,客户姓名、信用卡数据和电话号码等信息未受到泄露,因为它们没有存储在公开的数据库中,不过“由于开发外包公司对源代码的处理不当,我们将与外包公司一起努力加强对客户个人信息处理的管理,并加强其安全功能。”
不过,虽然数据没有被盗用的迹象,丰田也提醒道,无法完全排除有人访问和窃取数据的可能性。
其说道,“安全专家的调查表明,尽管我们无法根据存储客户电子邮件地址和客户管理号码的数据服务器的访问历史记录来确认第三方的访问,但同时,我们不能完全否认它(会被第三方盗用的可能性)。”
因此,对于可能泄露了电子邮件地址和客户管理号码的客户,丰田公司称,分别向注册的电子邮件地址发送道歉信和通知。
3 人为因素是最大的变数
值得庆幸的是,存储在服务器上的客户管理号码对第三方来说用处并不大,但是也会有不法分子会通过邮件等形式以丰田公司的名义发送一些钓鱼网站。为此,丰田公司表示,提供了一个专用表单( https://www.toyota.co.jp/cmpnform/pub/co/contact-tconnect)并建立了专门的呼叫中心,以回答客户的问题和疑虑。同时,其建议所有在 2017 年 7 月至 2022 年 9 月之间注册的 T-Connect 用户保持警惕,并避免打开来自声称是丰田的未知发件人的电子邮件及附件。
与此同时,据《每日经济新闻》报道,丰田中国相关负责人回应道,这个情况是在日本发生的,不涉及中国用户,主要是使用 T-connect 服务的客户的邮箱地址和内部管理的号码有被窃取的可能,别的信息都不受影响。
至此,虽然“暴露”在外长达五年的漏洞侥幸没有造成太大的影响,但这类屡见不鲜的事件也时刻警醒着处于信息化时代下的各家公司。
据外媒 BleepingComputer 报道,在今年 9 月,赛门铁克的安全分析师曾公布,近 2000 个 iOS 和 Android 应用程序在其代码中包含硬编码的 AWS 凭证。造成这种情况的,往往是开发者的疏忽大意,他会经常在代码中存储凭证,以便在测试多个应用迭代中快速且轻松地获取资产、访问服务和更新配置。
按理来说,当软件准备好进行实际部署时,这些凭证应该被删除的,但是很多开发者总是会忽略,从而造成数据泄露。
另一边,为了减少漏洞的出现,全球最大的代码托管平台 GitHub 也在近年间致力于改进这一方面。去年 6 月,GitHub 宣布其将自动扫描公开 PyPI 和 RubyGems 机密的存储库,如凭据和 API 令牌。简单来看,当 GitHub 发现密码、API 令牌、私有 SSH 密钥或公共存储库中公开的其他受支持的机密时,它会通知注册表维护者。在今年,GitHub 还推出了一项由机器学习驱动的新代码扫描分析功能,该代码扫描功能可以针对站点脚本 (XSS)、路径注入、NoSQL 注入和 SQL 注入四种常见漏洞模式显示警报。
不过,归根究底,开发者在自身开发的时候需要具备足够强的技术能力同时,也需要有强烈的网络、系统等安全意识。
参考链接:
https://global.toyota/jp/newsroom/corporate/38095972.html
整理 | 苏宓
出品 | CSDN(ID:CSDNnews)
这个外包公司太恶心了。。进去请三思!
从ZH离开时,准备写点东西揭露下ZH对外包的一系列恶心措施,但是感觉蚍蜉撼树,什么也改变不了,自己倒霉就认了,最近流行向前看吗。
但是今天又听到有同事被离场,心中光有怒火,还是无可奈何。思来想去,决定写点东西,如果能给那些准备去ZH(合肥)做外包的提个醒,也不算坏事。
换句话说,ZH这个坑我只想竖个警示标志,跳不跳悉听尊便。
一、ZH的包工头有哪些:
文*辉
软*力
京*方
北*诚
联*通
宇*信
还有很多。。。。
他们或许在其他地方有项目,但是在合肥纯属ZH的包工头。如果还不确定是不是人头外包,直接问HR是项目外包还是人力外包,这几家HR还算比较诚实。
二、ZH对外包的管理:
外包各行各业都有,地主家的活干不完,农忙的时候会请临时工,富裕的地主还会长期养几个工人,简称长工。当然地主里面分善良和刻薄的,其他地主暂且不表,ZH可以说是银行里最刻薄和恶心的。
随便说几条
1、迟到晚一秒,半天工时(银行和包工头们之间的结算单位,一般按小时)没有,晚上下班忘打卡,不好意思,一天白干。
2、食堂吃饭,等地主家儿子们(行内人员)吃完长工,临时工才能去,提前去会被查刷卡记录,通报甚至离场(=开除)。现在不存在这个问题了,干活都不在地主院里了,被赶到租得场地(ODC)去了,吃饭自理。
3、不能带私人电脑,面向百度编程的码农们只有手机搜索,但是地主又规定,不能长时间看手机。
4、360无死角摄像头,监控工人们的一举一动,不是摆设,等地主准备赶你走的时候,没有人能禁的住调摄像头查。
5、近400个工人,四个厕所,加一起8个坑位,男女各四个坑位,如果你要拉肚子,那就祈祷你自己憋得住。
如果以上种种你都表示理解,恭喜你,有了做长工得觉悟。“拿工人得钱,好好干活不拉到了,别老想着翻身农奴把歌唱,养家糊口要紧”。好像这么想也有道理。
但是接着往下看:
地主要求把地里麦子割了,一人两亩,当天完成。有人加班加点,半天割完了,有人慢条斯理磨磨蹭蹭,刚好下班干完。按道理工作提前完成,在下一批任务到来前,时间可以相对自由安排吧,不好意思,不行!!学习看书也不行,必须对着电脑!!!
你想提高自己技术,回家看去,你想学习,回家去学,拿工资得时候不能干与工作无关得任何事情!
三、想赶你走,你连呼吸都是错的
哪天地主家得地里活差不多忙完得时候,这么多长工怎么办呢,找理由开呗。
1、玩手机超过半小时,开~。
2、中午午睡到上班点还在打瞌睡,开~
3、桌子上有与工作无关得书,查查监控是不是看了,看了就开~
4、脖子上挂个耳机,再听歌?开~
5、什么把柄都抓不到?不可能,我听儿子们反应哪个工人难沟通,开~
6…
包工头们接到老板得命令,找你谈话,希望你自己提离职,不要闹得不愉快,补偿是没有得!
什么?你要仲裁,走法律途径?你看看合同上是不是规定自己原因被地主开除得后果自负?是不是规定工作地不知这块地,还有可能到外省干活,你要去不了就不能怪我们了啊…
四.上证据!!
这里得每一句话都可以成为开除你的理由!
四个人一齐被开除:两个玩手机,两个看书。
带耳机被开除:
HR:恶心略有耳闻。
结论:如果看到这,你依然准备跳到这个坑了,我先敬你是条汉子,最后让我猜测你是属于那种类型:
A:培训班或者自学成才,苦无单位接受,混个工作经验
B.职业规划不重要,先挣点钱再说
C.年龄太大,被其他公司优化了
D.大专毕业又想干码农混个经验
以上都不是得话,那你要想想为啥还在坑了呆着。
来源:news.sohu.com/a/591098103_121124367
图片不压缩,前端要背锅
背景
🎨(美术): 这是这次需求的切图 📁 ,你看看有没问题?
🧑💻(前端): 好的。
页面上线 ...
🧑💼(产品): 这图片怎么半天加载不出来 💢 ?
🧑💻(前端): 我看看 🤔 (卑微)。
... 📁(size: 15MB)
🧑💻(前端): 😅。
很多时候,我们从 PS
、蓝湖
或摹客
等工具导出来的图片,或者是美术直接给到切图,都是未经过压缩的,体积都比较大。这里,就有了可优化的空间。
TinyPng
TinyPNG
使用智能的「有损压缩技术」来减少WEBP
、JPEG
和PNG
文件的文件大小。通过选择性地减少图像中的「颜色数量」,使用更少的字节来存储数据。这种效果几乎是看不见的,但在文件大小上有非常大的差别。
使用过TinyPng的都知道,它的压缩效果非常好,体积大幅度降低且显示效果几乎没有区别( 👀 看不出区别)。因此,选择其作为压缩工具,是一个不错的选择。
TinyPng
提供两种压缩方法:
通过在官网上进行手动压缩;
通过官方提供的
tinify
进行压缩;
身为一个程序员 🧑💻 ,是不能接受手动一张张上传压缩这种方法的。因此,选择第二种方法,通过封装一个工具,对项目内的图片自动压缩,彻底释放双手 🤲 。
工具类型
第一步,思考这个工具的「目的」是什么?没错,「压缩图片」。
第二步,思考在哪个「环节」进行压缩?没错,「发布前」。
这样看来,开发一个webpack plugin
是一个不错选择,在打包「生产环境」代码的时候,启用该plugin
对图片进行处理,完美 🥳 !
但是,这样会面临两个问题 🤔 :
页面迭代,新增了几张图片,重新打包上线时,会导致旧图片被多次压缩;
无法选择哪些图片要被压缩,哪些图片不被压缩;
虽然可以通过「配置」的方式解决上述问题,但每次打包都要特殊配置,略显麻烦,这样看来plugin
好像不是最好的选择。
以上两个问题,使用「命令行工具」就能完美解决。在打包「生产环境」代码之前,执行「压缩命令」,通过命令行交互,选择需要压缩的图片。
效果演示
话不多说,先上才艺 💃 !
安装
$ npm i yx-tiny -D
使用
$ npx tiny
根据命令行提示输入
流程:输入「文件夹名称-tinyImg
」,接着工具会找到当前项目下所有的tinyImg
,接着选择一或多个tinyImg
,紧接着,工具会找出tinyImg
下所有的png
、jpe?g
和svga
,最后选择压缩模式「全量」或「自定义」,选择需要压缩的图片。
从最后的输出结果可以看到,压缩前的资源体积为2.64MB
,压缩后体积为1.02MB
,足足压缩了1.62MB
👍 !
实现思路
总体分为五个过程:
查找:找出所有的图片资源;
分配:均分任务到每个进程;
上传:把原图上传到
TinyPng
;下载:从
TinyPng
中下载压缩好的图片;写入:用下载的图片覆盖本地图片;
项目地址:yx-tiny
查找
找出所有的图片资源。
packages/tiny/src/index.ts
/**
* 递归找出所有图片
* @param { string } path
* @returns { Array<imageType> }
*/
interface IdeepFindImg {
(path: string): Array<imageType>
}
let deepFindImg: IdeepFindImg
deepFindImg = (path: string) => {
// 读取文件夹的内容
const content = fs.readdirSync(path)
// 用于保存发现的图片
let images: Array<imageType> = []
// 遍历该文件夹内容
content.forEach(folder => {
const filePath = resolve(path, folder)
// 获取当前内容的语法信息
const info = fs.statSync(filePath)
// 当前内容为“文件夹”
if (info.isDirectory()) {
// 对该文件夹进行递归操作
images = [...images, ...deepFindImg(filePath)]
} else {
const fileNameReg = /\.(jpe?g|png|svga)$/
const shouldFormat = fileNameReg.test(filePath)
// 判断当前内容的路径是否包含图片格式
if (shouldFormat) {
// 读取图片内容保存到images
const imgData = fs.readFileSync(filePath)
images.push({
path: filePath,
file: imgData
})
}
}
})
return images
}
通过命令行交互后,拿到目标文件夹的路径path
,然后获取该path
下的所有内容,接着遍历所有内容。首先判断该内容的文件信息:若为“文件夹”,则把该文件夹路径作为path
,递归调用deepFindImg
;若不为“文件夹”,判断该内容为图片,则读取图片数据,push
到images
中。最后,返回所有找到的图片。
分配
均分任务到每个进程。
packages/tiny/src/index.ts
// ...
cluster.setupPrimary({
exec: resolve(__dirname, 'features/process.js')
})
// 若资源数小于则创建一个进程,否则创建多个进程
const works: Array<{
work: Worker;
tasks: Array<imageType>
}> =[]
if (list.length <= cpuNums) {
works.push({
work: cluster.fork(),
tasks: list
})
} else {
for (let i = 0; i < cpuNums; ++i) {
const work = cluster.fork()
works.push({
work,
tasks: []
})
}
}
// 平均分配任务
let workNum = 0
list.forEach(task = >{
if (works.length === 1) {
return
} else if (workNum >= works.length) {
works[0].tasks.push(task)
workNum = 1
} else {
works[workNum].tasks.push(task)
workNum += 1
}
})
// 用于记录进程完成数
let pageNum = works.length
// 初始化进度条
// ...
works.forEach(({
work,
tasks
}) = >{
// 发送任务到每个进程
work.send(tasks)
// 接收任务完成
work.on('message', (details: Idetail[]) = >{
// 更新进度条
// ...
pageNum--
// 所有任务执行完毕
if (pageNum === 0) {
// 关闭进程
cluster.disconnect()
}
})
})
使用cluster
,根据「cpu核心数」创建等量的进程,works
用于保存已创建的进程,list
中保存的是要处理的压缩任务,通过遍历list
,把任务依次分给每一个进程。接着遍历works
,通过send
方法发送进程任务。通过监听message
事件,利用pageNum
记录进程任务的完成情况,当所有进程任务执行完毕后,则关闭进程。
上传
官方提供的tinify
工具有「500张/月」的限额,超过限额后,需要付费。
由于家境贫寒,且出于学习的目的,就没有使用tinify
,而是通过构造随机IP
来直接请求「压缩接口」来达到「破解限额」的目的。大家在真正使用的时候,还是要使用tinyfy
来压缩,不要做这种投机取巧的事。
好了,回到正文。
把原图上传到TinyPng
。
packages/tiny/src/features/index.ts
/**
* 上传函数
* @param { Buffer } file 文件buffer数据
* @returns { Promise<DataUploadType> }
*/
interface Iupload {
(file: Buffer): Promise<DataUploadType>
}
export let upload: Iupload
upload = (file: Buffer) => {
// 生成随机请求头
const header = randomHeader()
return new Promise((resolve, reject) => {
const req = Https.request(header, res => {
res.on('data', data => {
try {
const resp = JSON.parse(data.toString()) as DataUploadType
if (resp.error) {
reject(resp)
} else {
resolve(resp)
}
} catch (err) {
reject(err)
}
})
})
// 上传图片buffer
req.write(file)
req.on('error', err => reject(err))
req.end()
})
}
使用node
自带的Https
模块,构造请求头,把deepFindImg
中返回的图片进行上传。上传成功后,会返回已经压缩好的图片的url
链接。
下载
从TinyPng
中下载压缩好的图片。
packages/tiny/src/features/index.ts
/**
* 下载函数
* @param { string } path
* @returns { Promise<string> }
*/
interface Idownload {
(path: string): Promise<string>
}
export let download: Idownload
download = (path: string) => {
const header = new Url.URL(path)
return new Promise((resolve, reject) => {
const req = Https.request(header, res => {
let content = ''
res.setEncoding('binary')
res.on('data', data => (content += data))
res.on('end', () => resolve(content))
})
req.on('error', err => reject(err))
req.end()
})
}
使用node
自带的Https
模块把upload
中返回的图片链接进行下载。下载成功后,返回图片的buffer
数据。
写入
把下载好的图片覆盖本地图片。
packages/tiny/src/features/process.ts
/**
* 接收进程任务
*/
process.on('message', (tasks: imageType[]) => {
;(async () => {
// 优化 png/jpg
const data = tasks
.filter(({ path }: { path: string }) => /\.(jpe?g|png)$/.test(path))
.map(ele => {
return compressImg({ ...ele, file: Buffer.from(ele.file) })
})
// 优化 svga
const svgaData = tasks
.filter(({ path }: { path: string }) => /\.(svga)$/.test(path))
.map(ele => {
return compressSvga(ele.path, Buffer.from(ele.file))
})
const details = await Promise.all([
...data.map(fn => fn()),
...svgaData.map(fn => fn())
])
// 写入
await Promise.all(
details.map(
({ path, file }) =>
new Promise((resolve, reject) => {
fs.writeFile(path, file, err => {
if (err) reject(err)
resolve(true)
})
})
)
)
// 发送结果
if (process.send) {
process.send(details)
}
})()
})
process.on
监听每个进程发送的任务,当接收到任务类型为「图片」,使用compressImg
方法来处理图片。当任务类型为「svga」,使用compressSvga
方法来处理svga
。最后把处理好的资源写入到本地覆盖旧资源。
compressImg
packages/tiny/src/features/process.ts
/**
* 压缩图片
* @param { imageType } 图片资源
* @returns { promise<Idetail> }
*/
interface IcompressImg {
(payload: imageType): () => Promise<Idetail>
}
let compressImg: IcompressImg
compressImg = ({ path, file }: imageType) => {
return async () => {
const result = {
input: 0,
output: 0,
ratio: 0,
path,
file,
msg: ''
}
try {
// 上传
const dataUpload = await upload(file)
// 下载
const dataDownload = await download(dataUpload.output.url)
result.input = dataUpload.input.size
result.output = dataUpload.output.size
result.ratio = 1 - dataUpload.output.ratio
result.file = Buffer.alloc(dataDownload.length, dataDownload, 'binary')
} catch (err) {
result.msg = `[${chalk.blue(path)}] ${chalk.red(JSON.stringify(err))}`
}
return result
}
}
compressImg
返回一个async
函数,该函数先调用upload
进行图片上传,接着调用download
进行下载,最终返回该图片的buffer
数据。
compressSvga
packages/tiny/src/features/process.ts
/**
* 压缩svga
* @param { string } path 路径
* @param { buffer } source svga buffer
* @returns { promise<Idetail> }
*/
interface IcompressSvga {
(path: string, source: Buffer): () => Promise<Idetail>
}
let compressSvga: IcompressSvga
compressSvga = (path, source) => {
return async () => {
const result = {
input: 0,
output: 0,
ratio: 0,
path,
file: source,
msg: ''
}
try {
// 解析svga
const data = ProtoMovieEntity.decode(
pako.inflate(toArrayBuffer(source))
) as unknown as IsvgaData
const { images } = data
const list = Object.keys(images).map(path => {
return compressImg({ path, file: toBuffer(images[path]) })
})
// 对svga图片进行压缩
const detail = await Promise.all(list.map(fn => fn()))
detail.forEach(({ path, file }) => {
data.images[path] = file
})
// 压缩buffer
const file = pako.deflate(
toArrayBuffer(ProtoMovieEntity.encode(data).finish() as Buffer)
)
result.input = source.length
result.output = file.length
result.ratio = 1 - file.length / source.length
result.file = file
} catch (err) {
result.msg = `[${chalk.blue(path)}] ${chalk.red(JSON.stringify(err))}`
}
return result
}
}
compressSvga
的「输入」、「输出」和compressImg
保持一致,目的是为了可以使用promise.all
同时调用。在compressSvga
内部,对svga
进行解析成data
,获取到svga
的图片列表images
,接着调用compressImg
对images
进行压缩,使用压缩后的图片覆盖data.images
,最后再把data
编码后,写入到本地覆盖原本的svga
。
最后
再说一遍,大家真正使用的时候,要使用官方的tinify
进行压缩。
参考文章:
祝大家生活愉快,工作顺利!
作者:JustCarryOn
链接:juejin.cn/post/7153086294409609229
开源项目|使用声网&环信 SDK 构建元宇宙应用 MetaTown 最佳实践
大家好!我们是美特兄弟三人组!前阵参加了【声网&环信 RTE2022 创新编程挑战赛】,整个大赛历时47天,除了对声网和环信的 SDK 有了很多的体验,还做了很多奇思妙想的结合。在此次大赛中我们团队基于声网&环信 SDK 构建了一个元宇宙应用 MetaTown,获得了环信专项奖,开心之余把这个项目介绍给大家,抛砖引玉,感兴趣的兄弟可以加入进来一起在元宇宙中闯荡江湖~
一、关于MetaTown
金钱是被铸造出来的自由——陀思妥耶夫斯基
在三次元的现实世界,你是否为了搞钱而终日奔波?忍受996甚至007的非人待遇?是否正经历着创业人的凛冬?疫情等因素带来的本轮经济下行落实在每个人身上都是真真切切的,对于经历了40年经济暴增的国人来讲更是史无前例的。
在荷包日瘪萎靡不振的日子里,精神慰藉尤为重要,元宇宙正是当下最时髦的,何不创造一个可以躺着赚钱的元宇宙小镇?不为别的,在有虚拟工作的前提下每天我的虚拟角色的金币都会涨,想一想岂不是有一点小欢愉?还能在这个小镇结交一些志同道合沉迷于搞钱的友友们!

自然这些虚拟财富目前还是无法转化为真正的成就感的(变为现实财富),但现在市面上开始有人吹web3.0了!坐等币圈大佬入局,我们有信心打造一款能让一部分人先富起来的元宇宙小镇!
---------------------------------------
MetaTown 是基于声网 RTC 和环信 IM 打造的模拟城市生活的元宇宙社交类 App。
初来乍到的玩家首次进入 MetaTown 先选择不同的职业,在这座城市首先要考虑的是如何赚钱,或做一名程序员上下班打卡,或自己创业开个酒吧/书店或去银行投资理财。除此以外,还要注意身体健康,可能哪天会随机生病需要去医院,支付挂号费咨询不同科室的医生。注意,没有核酸证明有可能看不了病嗷~不打工没钱也看不了病嗷~

这个小镇的所有公共场所,均可以随时发起与陌生人私聊,因共同兴趣结缘,充分体验在MetaTown小镇 搞钱 闯荡的日子!
项目 GitHub 地址:
https://github.com/AgoraIO-Community/RTE-2022-Innovation-Challenge/tree/main/Application-Challenge/%E9%A1%B9%E7%9B%AE243-metatown-metatown
二、MetaTown 核心技术
MetaTown 使用当下最流行的声网实时音视频以及环信即时通讯 SDK,具体场景如:医院场景中一对一咨询医生,进行远程实时问诊。社交场景中与好友实时音视频沟通,聊天。
1、环信即时通讯
MetaTown 在6大交互场景中运用了环信的即时通讯 IM(Instant Messaging),给 IM 赋予了新的场景活力,支持陌生人私聊,群聊及超大型聊天室。
2-1)会话列表 项目中 IM 会话列表如下图:
会话列表关键代码:
publicvoidshow(BaseActivity activity){
NiceDialog.init().setLayoutId(R.layout.dialog_message)
.setConvertListener(new ViewConvertListener() {
@Override
protectedvoidconvertView(ViewHolder holder, BaseNiceDialog dialog){
RecyclerView rv = holder.getView(R.id.rv);
List easeConversationInfos = initData();
rv.setLayoutManager(new LinearLayoutManager(dialog.getContext()));
DialogMsgAdapter dialogMsgAdapter = new DialogMsgAdapter(easeConversationInfos);
rv.setAdapter(dialogMsgAdapter);
dialogMsgAdapter.setOnItemClickListener(new DialogMsgAdapter.OnItemClickListener() {
@Override
publicvoidonItemClick(int pos,String name){
SoundUtil.getInstance().playBtnSound();
dialog.dismissAllowingStateLoss();
EMConversation item = (EMConversation) easeConversationInfos.get(pos).getInfo();
ChatDialog.getInstance().show(activity,item.conversationId(), name);
}
});
}
})
.setAnimStyle(R.style.EndAnimation)
.setOutCancel(true)
.setShowEnd(true)
.show(activity.getSupportFragmentManager());
}
2-2)IM 聊天
项目中 IM 聊天如下图:
发送消息关键代码:
@Override
public void sendMessage(EMMessage message) {
if(message == null) {
if(isActive()) {
runOnUI(() -> mView.sendMessageFail("message is null!"));
}
return;
}
addMessageAttributes(message);
if (chatType == EaseConstant.CHATTYPE_GROUP){
message.setChatType(EMMessage.ChatType.GroupChat);
}else if(chatType == EaseConstant.CHATTYPE_CHATROOM){
message.setChatType(EMMessage.ChatType.ChatRoom);
}
...
EMClient.getInstance().chatManager().sendMessage(message);
if(isActive()) {
runOnUI(()-> mView.sendMessageFinish(message));
}
}
接受消息关键代码:
public void onMessageReceived(List messages) {
super.onMessageReceived(messages);
LiveDataBus.get().with(Constants.RECEIVE_MSG, LiveEvent.class).postValue(new LiveEvent());
for (EMMessage message : messages) {
// in background, do not refresh UI, notify it in notification bar
if(!MetaTownApp.getInstance().getLifecycleCallbacks().isFront()){
getNotifier().notify(message);
}
//notify new message
getNotifier().vibrateAndPlayTone(message);
}
}
2、声网音视频
MetaTown 运用了声网的实时音视频功能。
1)集成声网 SDK
1-1)添加声网音视频依赖在 app module 的 build.gradle 文件的 dependencies 代码块中添加如下代码:
implementation 'io.agora.rtc:full-rtc-basic:3.6.2'
然后在app module的build.gradle文件的android->defaultConfig代码块中添加如下代码
ndk {
abiFilters "arm64-v8a"
}
// 设置支持的SO库架构(开发者可以根据需要,选择一个或多个平台的so)
1-2)添加必要权限 为了保证 SDK 能正常运行,我们需要在 AndroidManisfest.xml 文件中声明以下权限:
1-3)APP 在签名打包时防止出现混淆的问题需要在 proguard-rules.pro 文件里添加以下代码:
-keep classio.agora.**{*;}
2)创建并初始化 RtcEngine
创建并初始化 RtcEngine
private void initializeEngine() {
try {
EaseCallKitConfig config = EaseCallKit.getInstance().getCallKitConfig();
if(config != null){
agoraAppId = config.getAgoraAppId();
}
mRtcEngine = RtcEngine.create(getBaseContext(), agoraAppId, mRtcEventHandler);
//因为有小程序 设置为直播模式 角色设置为主播
mRtcEngine.setChannelProfile(CHANNEL_PROFILE_LIVE_BROADCASTING);
mRtcEngine.setClientRole(CLIENT_ROLE_BROADCASTER);
EaseCallFloatWindow.getInstance().setRtcEngine(getApplicationContext(), mRtcEngine);
//设置小窗口悬浮类型
EaseCallFloatWindow.getInstance().setCallType(EaseCallType.CONFERENCE_CALL);
} catch (Exception e) {
EMLog.e(TAG, Log.getStackTraceString(e));
throw new RuntimeException("NEED TO check rtc sdk init fatal error\n" + Log.getStackTraceString(e));
}
3)设置视频模式
privatevoidsetupVideoConfig(){
mRtcEngine.enableVideo();
mRtcEngine.muteLocalVideoStream(true);
mRtcEngine.setVideoEncoderConfiguration(new VideoEncoderConfiguration(
VideoEncoderConfiguration.VD_1280x720,
VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_15,
VideoEncoderConfiguration.STANDARD_BITRATE,
VideoEncoderConfiguration.ORIENTATION_MODE.ORIENTATION_MODE_FIXED_PORTRAIT));
//启动谁在说话检测
int res = mRtcEngine.enableAudioVolumeIndication(500,3,false);
}
4)设置本地视频显示属性
4-1)setupLocalVideo( VideoCanvas local ) 方法用于设置本地视频显示信息。应用程序通过调用此接口绑定本地视频流的显示视窗(view),并设置视频显示模式。在应用程序开发中,通常在初始化后调用该方法进行本地视频设置,然后再加入频道。
privatevoidsetupLocalVideo(){
if(isFloatWindowShowing()) {
return;
}
localMemberView = createCallMemberView();
UserInfo info = new UserInfo();
info.userAccount = EMClient.getInstance().getCurrentUser();
info.uid = 0;
localMemberView.setUserInfo(info);
localMemberView.setVideoOff(true);
localMemberView.setCameraDirectionFront(isCameraFront);
callConferenceViewGroup.addView(localMemberView);
setUserJoinChannelInfo(EMClient.getInstance().getCurrentUser(),0);
mUidsList.put(0, localMemberView);
mRtcEngine.setupLocalVideo(new VideoCanvas(localMemberView.getSurfaceView(), VideoCanvas.RENDER_MODE_HIDDEN, 0));
}
4-2)joinChannel(String token,String channelName,String optionalInfo,int optionalUid ) 方法让用户加入通话频道,在同一个频道内的用户可以互相通话,多个用户加入同一个频道,可以群聊。使用不同 App ID 的应用程序是不能互通的。如果已在通话中,用户必须调用 leaveChannel() 退出当前通话,才能进入下一个频道。
privatevoidjoinChannel(){
EaseCallKitConfig callKitConfig = EaseCallKit.getInstance().getCallKitConfig();
if(listener != null && callKitConfig != null && callKitConfig.isEnableRTCToken()){
listener.onGenerateToken(EMClient.getInstance().getCurrentUser(),channelName, EMClient.getInstance().getOptions().getAppKey(), new EaseCallKitTokenCallback(){
@Override
publicvoidonSetToken(String token,int uId){
EMLog.d(TAG,"onSetToken token:" + token + " uid: " +uId);
//获取到Token uid加入频道
mRtcEngine.joinChannel(token, channelName,null,uId);
//自己信息加入uIdMap
uIdMap.put(uId,new EaseUserAccount(uId,EMClient.getInstance().getCurrentUser()));
}
@Override
public void onGetTokenError(int error, String errorMsg) {
EMLog.e(TAG,"onGenerateToken error :" + error + " errorMsg:" + errorMsg);
//获取Token失败,退出呼叫
exitChannel();
}
});
}
}
完成以上配置后就可以发起呼叫了,其它一些摄像头控制,声音控制可以参考声网官网的API,这里不再赘述。
3、场景原画
1)人物行走可分为踏步、水平移动两种动作,分别通过踏步动画和控制人物及背景 scrollview 移动实现。
2)关键点在于人物向左走过半屏继续向左行走,或向右走过半屏继续向右走的情况,以向右走为例,如果人物未超过屏幕中线,则控制人物向右移动;如果超出屏幕中线继续向右移动,则将人物固定在中线位置,背景向左滑动;如果背景向左已滑动至尽头,则保持背景不动,人物继续向右移动;如果人物移动至右边缘,则只控制人物原地踏步,背景和人物均不水平移动。
动画关键代码:
if (isToRight) {
ivPerson.setRotationY(180f);
}
isToRight = false;
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) ivPerson.getLayoutParams();
if (sv != null) {
if (layoutParams.leftMargin > DisplayUtil.getHeight(MetaTownApp.getApplication()) || !sv.canScrollHorizontally(-1)) {
layoutParams.leftMargin -= STEP;
layoutParams.leftMargin = Math.max(layoutParams.leftMargin, 50);
ivPerson.setLayoutParams(layoutParams);
} else {
sv.smoothScrollBy(-STEP, 0);
}
} else {
layoutParams.leftMargin -= STEP;
layoutParams.leftMargin = Math.max(layoutParams.leftMargin, 50);
ivPerson.setLayoutParams(layoutParams);
}
mAnimationDrawable.start();
if (!isToRight) {
ivPerson.setRotationY(0f);
}
isToRight = true;
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) ivPerson.getLayoutParams();
if (sv != null) {
if (layoutParams.leftMargin < DisplayUtil.getHeight(MetaTownApp.getApplication()) || !sv.canScrollHorizontally(1)) {
layoutParams.leftMargin += STEP;
layoutParams.leftMargin = Math.min(layoutParams.leftMargin, DisplayUtil.getWidth(MetaTownApp.getApplication()) - 100);
ivPerson.setLayoutParams(layoutParams);
} else {
sv.smoothScrollBy(STEP, 0);
}
} else {
layoutParams.leftMargin += STEP;
layoutParams.leftMargin = Math.min(layoutParams.leftMargin, DisplayUtil.getWidth(MetaTownApp.getApplication()) - 100);
ivPerson.setLayoutParams(layoutParams);
}
mAnimationDrawable.start();
写字楼打工
银行投资理财
再说说2.0版本后续计划,有时间有兴趣的小伙伴欢迎留言加入我们兴趣小组一起搞事情~
1)把现有的几个场景补齐(II期)
2)开发新场景,丰富搞钱路数
3)等一位币圈大佬掉到碗里。
以上是 MetaTown 作品在 RTE2022 编程挑战赛期间的实践分享,更多开源项目可以访问环信开源项目频道:https://www.imgeek.org/code/
备注“开源项目”加入环信开发者开源项目交流群
学长突然问我用过 Symbol 吗,我哽咽住了(准备挨骂)
这天在实验室和学长一起写学校的项目,学长突然问我一句:“你用过 Symbol 吗?” 然而我的大脑却遍历不出这个关键性名词,啊,又要补漏了
Symbol
对于一些前端小白(比如我)来讲,没有特别使用过,只是在学习 JS 的时候了解了大概的概念,当时学习可能并没有感觉到 Symbol
在开发中有什么特别的作用,而在学习一段时间后回头看一遍,顿悟!
而本文将带读者从基本使用,特性应用到内置 Symbol 三个方面,带大家深入 Symbol
这个神奇的类型!
什么是 Symbol
😶🌫️
Symbol
作为原始数据类型的一种,表示独一无二的值,在之前,对象的键以字符串的形式存在,所以极易引发键名冲突问题,而 Symbol
的出现正是解决了这个痛点,它的使用方式也很简单。
Symbol
的使用
创建一个 Symbol
与创建 Object
不同,只需要 a = Symbol()
即可
let a = Symbol()
typeof a
使用时需要注意的是:不可以使用 new
来搭配 Symbol()
构造实例,因为其会抛出错误
let a = new Symbol()
typeof a // Symbol is not a constructor
通常使用 new
来构造是想要得到一个包装对象,而 Symbol
不允许这么做,那么如果我们想要得到一个 Symbol()
的对象形式,可以使用 Object()
函数
let a = Symbol()
let b = Object(a)
typeof b // object
介绍到这里,问题来了,Symbol
看起来都一样,我们怎么区分呢?我们需要传入一个字符串的参数用来描述 Symbol()
let a = Symbol()
let b = Symbol()
上面看来 a
和 b
的值都是 Symbol
,代码阅读上,两者没区分,那么我们调用 Symbol()
函数的时候传入字符串用来描述我们构建的 Symbol()
let a = Symbol("a")
let b = Symbol("b")
Symbol 的应用✌️
Symbol 的应用其实利用了唯一性的特性。
作为对象的属性
大家有没有想过,如果我们在不了解一个对象的时候,想为其添加一个方法或者属性,又怕键名重复引起覆盖的问题,而这个时候我们就需要一个唯一性的键来解决这个问题,于是 Symbol 出场了,它可以作为对象的属性的键,并键名避免冲突。
let a = Symbol()
let obj = {}
obj[a] = "hello world"
我在上面创建了一个 symbol
作为键的对象,其步骤如下
创建一个 Symbol
创建一个对象
通过 obj[]
将 Symbol
作为对象的键
值得注意的是我们无法使用.
来调用对象的 Symbol
属性,所以必须使用 []
来访问 Symbol
属性
降低代码耦合
我们经常会遇到这种代码
if (name === "猪痞恶霸") {
console.log(1)
}
又或者
switch (name) {
case "猪痞恶霸"
console.log(1)
case "Ned"
console.log(2)
}
"猪痞恶霸"
与 "Ned"
被称为魔术字符串,即与代码强耦合的字符串,可以理解为:与我们的程序代码强制绑定在一起,然而这会导致一个问题,在条件判断复杂的情况下,我们想要更改我们的判断条件,就需要更改每一个判断控制,维护起来非常麻烦,所以我们可以换一种形式来解决字符串与代码强耦合。const judge = {
name_1:"猪痞恶霸"
name_2:"Ned"
}
switch (name) {
case judge.name_1
console.log(1)
case judge.name_2
console.log(2)
}
我们声明了一个存储判断条件字符串的对象,通过修改对象来自如地控制判断条件,当然本小节的主题是 Symbol
,所以还能继续优化!
const judge = {
rectangle:Symbol("rectangle"),
triangle:Symbol("triangle")
}
function getArea(model, size) {
switch (model) {
case judge.rectangle:
return size.width * size.height
case judge.triangle:
return size.width * size.height / 2
}
}
let area = getArea(judge.rectangle ,{width:100, height:200})
console.log(area)
为了更加直观地了解我们优化的过程,上面我创建了一个求面积的工具函数,利用 Symbol
的特性,我们使我们的条件判断更加精确,而如果是字符串形式,没有唯一的特点,可能会出现判断错误的情况。
全局共享 Symbol
如果我们想在不同的地方调用已经同一 Symbol
即全局共享的 Symbol
,可以通过 Symbol.for()
方法,参数为创建时传入的描述字符串,该方法可以遍历全局注册表中的的 Symbol
,当搜索到相同描述,那么会调用这个 Symbol
,如果没有搜索到,就会创建一个新的 Symbol
。
为了更好地理解,请看下面例子
let a = Symbol.for("a")
let b = Symbol.for("a")
a === b // true
如上创建 Symbol
首先通过 Symbol.for()
在全局注册表中寻找描述为 a
的 Symbol
,而目前没有符合条件的 Symbol
,所以创建了一个描述为 a
的 Symbol
当声明 b
并使用 Symbol.for()
在全局注册表中寻找描述为 a
的 Symbol
,找到并赋值
比较 a
与 b
结果为 true
反映了 Symbol.for()
的作用
let a = Symbol("a")
let b = Symbol.for("a")
a === b // false
woc,结果竟然是 false
,与上面的区别仅仅在于第一个 Symbol
的创建方式,带着惊讶的表情,来一步一步分析一下为什么会出现这样的结果、
使用 Symbol("a")
直接创建,所以该 Symbol("a")
不在全局注册表中
使用 Symbol.for("a")
在全局注册表中寻找描述为 a
的 Symbol
,并没有找到,所以在全局注册表中又创建了一个描述为 a
的新的 Symbol
秉承 Symbol
创建的唯一特性,所以 a
与 b
创建的 Symbol
不同,结果为 false
问题又又又来了!我们如何去判断我们的 Symbol
是否在全局注册表中呢?
Symbol.keyFor()
帮我们解决了这个问题,他可以通过变量名查询该变量名对应的 Symbol
是否在全局注册表中
let a = Symbol("a")
let b = Symbol.for("a")
Symbol.keyFor(a) // undefined
Symbol.keyFor(b) // 'a'
如果查询存在即返回该 Symbol
的描述,如果不存在则返回 undefined
以上通过使用 Symbol.for()
实现了 Symbol
全局共享,下面我们来看看 Symbol
的另一种应用
内置 Symbol
值又是什么❔
上面的 Symbol
使用是我们自定义的,而 JS 有内置了 Symbol
值,个人的理解为:由于唯一性特点,在对象内,作为一个唯一性的键并对应着一个方法,在对象调用某方法的时候会调用这个 Symbol
值对应的方法,并且我们还可以通过更改内置 Symbol
值对应的方法来达到更改外部方法作用的效果。
为了更好地理解上面这一大段话,咱们以 Symbol.hasInstance
作为例子来看看内置 Symbol
到底是个啥!
class demo {
static [Symbol.hasInstance](item) {
return item === "猪痞恶霸"
}
}
"猪痞恶霸" instanceof demo // true
Symbol.hasInstance
对应的外部方法是 instanceof
,这个大家熟悉吧,经常用于判断类型。而在上面的代码片段中,我创建了一个 demo
类,并重写了 Symbol.hasInstance
,所以其对应的 instanceof
行为也会发生改变,其内部的机制是这样的:当我们调用 instanceof
方法的时候,内部对应调用 Symbol.hasInstance
对应的方法即 return item === "猪痞恶霸"
注:更多相关的内置 Symbol
可以查阅相关文档😏
链接:https://juejin.cn/post/7143252808257503240
埋点统计优化,优化首屏加载速度提升
埋点统计
在我们业务里经常有遇到,或者很普遍的,我们自己网站也会加入第三方统计,我们会看到动态加载方式去加载jsdk
,也就是你常常看到的insertBefore
操作,我们很少考虑到为什么这么做,直接同步加载不行吗?统计代码会影响业务首屏加载吗?同步引入方式,当然会,我的业务代码还没加载,首屏就加载一大段统计的jsdk
,在移动端页面打开要求比较高的苛刻条件下,首屏优化,你可以在埋点统计
上做些优化,那么页面加载会有一个很大的提升,本文是一篇笔者关于埋点优化的笔记,希望看完在项目中有所思考和帮助。
正文开始...
最近遇到一个问题,先看一段代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>埋点</title>
<script>
window.formateJson = (data) => JSON.stringify(data, null, 2);
</script>
<script async defer>
(function (win, head, attr, script) {
console.log("---111---");
win[attr] = win[attr] || [];
const scriptDom = document.createElement(script);
scriptDom.async = true;
scriptDom.defer = true;
scriptDom.src = "./js/tj.js";
scriptDom.onload = function () {
win[attr].push({
id: "maic",
});
win[attr].push({
id: "Tom",
});
console.log("---2222---");
console.log(formateJson(win[attr]));
};
setTimeout(() => {
console.log("setTimeout---444---");
head.parentNode.insertBefore(scriptDom, head);
}, 1000);
})(window, document.getElementsByTagName("head")[0], "actd", "script");
</script>
<script async defer src="./js/app.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>
我们会发现,打印的顺序结果是下面这样的:
---111---
app.js:2 ---333--- start load app.js
app.js:4 [
{
"id": "pink"
}
]
(index):30 setTimeout---444---
(index):26 ---2222---
(index):27 [
{
"id": "pink"
},
{
"id": "maic"
},
{
"id": "Tom"
}
]
冥思苦想,我们发现最后actd
的结果是
[
{
"id": "pink"
},
{
"id": "maic"
},
{
"id": "Tom"
}
]
其实我想要的结果是先添加maic
,Tom
,最后添加pink
,需求就是,必须先在这个ts.js
执行后,预先添加基础数据,然后在其他业务app.js
添加其他数据,所以此时,无论如何都是满足不了我的需求。
试下想,为什么没有按照我的预期的要求走,问题就是出现在这个onload
方法上
onload事件
于是查询资料寻得,onload事件
是会等引入的外部资源
加载完毕后才会触发
外部资源加载完毕是什么意思?
举个栗子,我在引入的index2.html
引入index2.js
,然后在引入脚本上写一个onload
事件测试loadIndex2
方法是否在我延时加载后进行调用的
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script>
function loadIndex2() {
console.log("script loader...");
}
</script>
<script src="./js/index2.js" onload="loadIndex2()"></script>
</body>
</html>
index2.js
中写入一段代码
var startTime = Date.now()
const count = 1000;
let wait = 10000;
/// 设置延时
const time = wait * count;
for (let i = 0; i < time; i++) { }
var endTime = Date.now()
console.log(startTime, endTime)
console.log(`延迟了:${Math.ceil((endTime - startTime) / 1000)}s后执行的`)
最后看下打印结果
所以可以证实,onload
是会等资源下载完了后,才会立即触发
所以我们回头来看
在浏览器的事件循环中,同步任务主线程肯定优先会先顺序执行
从打开印---111---
,
然后到onload
此时不会立即执行
遇到定时器,定时器设置了1s
后会执行,是个宏任务,会放入队列中,此时不会立即执行
然后接着会执行<script async defer src="./js/app.js"></script>
脚本
所以此时,执行该脚本后,我们可以看到会先执行push
方法。
所以我们看到pink
就最先被推入数组中,当该脚本执行完毕后,此时会去执行定时器
定时器里我们看到我们插入方式insertBefore
,当插入时成功时,此时会调用onload
方法,所以此时就会添加maic
与Tom
很明显,我们此时的需求不满足我们的要求,而且一个onload
方法已经成了拦路虎
那么我去掉onload
试试,因为onload
方法只会在脚本加载完毕后去执行,他只会等执行定时器后,成功插入脚本后才会真正执行,而此时其他脚本已经优先它的执行了。
那该怎么解决这个问题呢?
我把onload
去掉试试,于是我改成了下面这样
<script async defer>
(function (win, head, attr, script) {
console.log("---111---");
win[attr] = win[attr] || [];
const scriptDom = document.createElement(script);
scriptDom.async = true;
scriptDom.defer = true;
scriptDom.src = "./js/tj.js";
win[attr].push({
id: "maic",
});
win[attr].push({
id: "Tom",
});
console.log("---2222---");
console.log(formateJson(win[attr]));
setTimeout(() => {
console.log("setTimeout---444---");
head.parentNode.insertBefore(scriptDom, head);
}, 1000);
})
(window, document.getElementsByTagName("head")
[0], "actd", "script");
</script>
去掉onload
后,我确实达到了我想要的结果
最后的结果是
[
{
"id": "maic"
},
{
"id": "Tom"
},
{
"id": "pink"
}
]
但是你会发现
我先保证了window.actd
添加了我预定提前添加的基础信息,但是此时,这个脚本并没有真正添加到dom中,我们执行完同步任务后,就会执行app.js
,当1s
后,我才真正执行了这个插入的脚本,而且我统计
脚本你会发现此时是在先执行了app.js
再加载tj.js
的
当执行setTimeout
时,我们会发现先执行了内部脚本,然后才执行打印
<script async defer>
(function (win, head, attr, script) {
console.log("---111---");
win[attr] = win[attr] || [];
const scriptDom = document.createElement(script);
scriptDom.async = true;
scriptDom.defer = true;
scriptDom.src = "./js/tj.js";
win[attr].push({
id: "maic",
});
win[attr].push({
id: "Tom",
});
console.log("---2222---");
console.log(formateJson(win[attr]));
setTimeout(() => {
console.log("setTimeout---444444---");
window.actd.push({
id: "setTimeout",
});
head.parentNode.insertBefore(scriptDom, head);
console.log(formateJson(window.actd));
}, 1000);
})(window, document.getElementsByTagName("head")[0], "actd", "script");
</script>
最后的结果,可以看到是这样的
[
{
"id": "maic"
},
{
"id": "Tom"
},
{
"id": "pink"
},
{
"id": "setTimeout"
}
]
看到这里不知道你心里有没有一个疑问,为什么在动态插入脚本时,我要用一个定时器1s
钟?为什么我需要用insertBefore
这种方式插入脚本?,我同步方式引入不行吗?不要定时器又会有什么样的结果?
我们通常在接入第三方统计时,貌似都是一个这样一个insertBefore
插入的jsdk
方式(但是一般我们都是同步方式引入jsdk
)
没有使用定时器(3237ms
)
<script async defer>
(function (win, head, attr, script) {
...
console.log("setTimeout---444444---");
window.actd.push({
id: "setTimeout",
});
head.parentNode.insertBefore(scriptDom, head);
console.log(formateJson(window.actd));
})(window, document.getElementsByTagName("head")[0], "actd", "script");
</script>
结果:
[
{
"id": "maic"
},
{
"id": "Tom"
},
{
"id": "setTimeout"
},
{
"id": "pink"
},
]
使用用定时器的(1622ms
)
<script async defer>
(function (win, head, attr, script) {
...
setTimeout(() => {
console.log("setTimeout---444444---");
window.actd.push({
id: "setTimeout",
});
head.parentNode.insertBefore(scriptDom, head);
console.log(formateJson(window.actd));
}, 1000);
})(window, document.getElementsByTagName("head")[0], "actd", "script");
</script>
当我们用浏览器的Performance
去比较两组数据时,我们会发现总长时间,使用定时器
的性能大概比没有使用定时器
的性能时间上大概要少50%
,在summary
中所有数据均有显著的提升。
不经感叹,就一个定时器
这一点点的改动,对整个应用提升有这么大的提升,我领导说,快应用在线加载时,之前因为这个统计js的加载明显阻塞了业务页面打开速度,做了这个优化后,打开应用显著提升不少。
我们再继续上一个问题,为什么不同步加载?
我把代码改造一下,去除了一些无关紧要的代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>js执行的顺序问题</title>
<script>
window.formateJson = (data) => JSON.stringify(data, null, 2);
</script>
<script async defer src="./js/tj.js"></script>
<script async defer>
(function (win, head, attr, script) {
win[attr] = win[attr] || [];
win[attr].push({
id: "maic",
});
win[attr].push({
id: "Tom",
});
console.log("---2222---");
console.log(formateJson(win[attr]));
})(window, document.getElementsByTagName("head")[0], "actd", "script");
</script>
<script async defer src="./js/app.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>
结果
[
{
"id": "maic"
},
{
"id": "Tom"
},
{
"id": "pink"
}
]
嘿,需求是达到了,因为我的业务app.js
加的数据是最后一条,说明业务功能上是ok
的,但是我们看下分析数据
首先肯定是加载顺序会发生变化,会先加载tj.js
然后再加载业务app.js
,你会发现同步加载这种方式有个弊端,假设tj.js
很大,那么是会阻塞影响页面首屏打开速度的,所以在之前采用异步,定时器方式,首屏加载会有显著提升。
同步加载(1846ms
)
我们发现tj.js
与app.js
相隔的时间很少,且我们从火焰图中分析看到,Summary
的数据是1846ms
综上比较,虽然同步加载
依然比不上使用定时器
的加载方式,使用定时器
相比较同步加载
,依然是领先11%
左右
异步标识async/defer
在上面的代码中,我们多次看到async
和defer
标识,在之前文章中笔者有写过一篇你真的了解esModule吗,阐述一些关于script
标签中type="moudle", defer,async
的几个标识,今天再次回顾下
其实从脚本优先级来看,同步的永远优先最高,当一个script
标签没有指定任何标识时,此时根据js引擎执行
来说,谁放前面,谁就会优先执行,前面没执行完,后面同步的script
就不会执行
注意到没有,我在脚本上有加async
与defer
在上面栗子中,我们使用insertBefore
方式,这就将该插入的js
脚本的优先级降低了。
我们从上面火焰图中可以分析得处结论,排名先后顺序依次如下
1、setTimeout+insertBefore
执行顺序:app.js->tj.js
2、同步脚本加载
执行顺序:tj.js->app.js
3、不使用定时器+insertBefore
执行顺序:app.js->tj.js
当我们知道在1
中,app.js
优先于tj.js
因为insertBefore
就是一种异步动态加载方式
举个例子
<script async defer>
// 执行
console.log(1)
// 2 insertBefore 这里再动态添加js
</script>
<script async defer>
// 执行
console.log(3)
</script>
执行关系就是1,3,2
关于async
与defer
谁先执行时,defer
的优先级比较低,会等异步标识的async
下载完后立马执行,然后再执行defer
的脚本,具体可以参考以前写的一篇文章你真的了解esModule吗
总结
统计脚本,我们可以使用
定时器+insertBefore
方式可以大大提高首屏的加载速度,这也给我们了一些启发,首屏加载,非业务代码,比如埋点统计
可以使用该方案做一点小优化加快首屏加载速度如果使用
insertBefore
方式,非常不建议同步方式
+insertBefore
,这种方式还不如同步加载统计脚本在特殊场景下,我们需要加载统计脚本,有基础信息的依赖后,我们也需要在业务代码使用统计,我们不要在动态加载脚本的同时使用
onload
,在onload
中尝试添加基础信息,实际上这种方式并不能满足你的需求一些关于
async
与defer
的特性,记住,执行顺序,同步任务会优先执行,async
是异步,脚本下载完就执行,defer
优先级比较低。本文示例code example
作者:Maic
来源:juejin.cn/post/7153216620406505480
一盏茶的功夫,拿捏作用域&作用域链
酸奶喝对,事半功倍!对于一些晦涩难懂,近乎神话的专业名词,切莫抓耳挠腮,我们直接上代码,加上通俗易懂地语言去渲染,且看今天我们如何拿捏javascript中的小山丘--作用域&作用域链,不止精解。
前言
我们需要先知道的是引擎,引擎的工作简单粗暴,就是负责javascript从头到尾代码的执行。引擎的一个好朋友是编译器,主要负责代码的分析和编译等;引擎的另一个好朋友就是今天的主角--作用域。那么作用域用来干什么呢?作用域链跟作用域又有什么关系呢?
一、作用域(scope)
作用域的定义:作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。
1、作用域的分类
(1)全局作用域
var name="global";
function foo(){
console.log(name);
}
foo();//global
这里函数foo()内部并没有声明name变量,但是依然打印了name的值,说明函数内部可以访问到全局作用域,读取name变量。再来一个例子:
hobby='music';
function foo(){
hobby='book';
console.log(hobby);
}
foo();//book
这里全局作用域和函数foo()内部都没有声明hobby这个变量,为什么不会报错呢?这是因为hobby='music';
写在了全局作用域,就算没有var,let,const的声明,也会被挂在window对象上,所以函数foo()不仅可以读取,还可以修改值。也就是说hobby='music';
等价于window.hobby='music';
。
(2)函数体作用域 函数体的作用域是通过隐藏内部实现的。换句话说,就是我们常说的,内层作用域可以访问外层作用域,但是外层作用域不能访问内层。原因,说到作用域链的时候就迎刃而解了。
function foo(){
var age=19;
console.log(age);
}
console.log(age);//ReferenceError:age is not defined
很明显,全局作用域下并没有age变量,但是函数foo()内部有,但是外部访问不到,自然而然就会报错了,而函数foo()没有调用,也就不会执行。
(3)块级作用域 块级作用域更是见怪不怪,像我们接触的let作用域,代码块{},for循环用let时的作用域,if,while,switch等等。然而,更深刻理解块级作用域的前提是,我们需要先认识认识这几个名词:
--标识符:能在作用域生效的变量。函数的参数,变量,函数名。需要格外注意的是:函数体内部的标识符外部访问不到
。
--函数声明:function 函数名(){}
--函数表达式: var 函数名=function(){}
--自执行函数: (function 函数名(){})();自执行函数前面的语句必须有分号
,通常用于隐藏作用域。
接下来我们就用一个例子,一口气展示完吧
function foo(sex){
console.log(sex);
}
var f=function(){
console.log('hello');
}
var height=180;
(
function fn(){
console.log(height);
}
)();
foo('female');
//依次打印:
//180
//female
分析一下:标识符:foo,sex,height,fn;函数声明:function foo(sex){};函数表达式:var f=function(){};自执行函数:(function fn(){})();需要注意,自执行函数fn()前面的var height=180;
语句,分号不能抛弃
。否则,你可以试一下。
二、预编译
说好只是作用域和作用域链的,但是考虑到理解作用域链的必要性,这里还是先聊聊预编译吧。先讨论预编译在不同环境发生的情况下,是如何进行预编译的。
1. 发生在代码执行之前
(1)声明提升
console.log(b);
var b=123;//undefined
这里打印undefined,这不是报错,与Refference:b is not defined不同。这是代码执行之前,预编译的结果,等同于以下代码:
var b;//声明提升
console.log(b);//undefined
b=123;
(2)函数声明整体提升
test();//hello123 调用函数前并没有声明,但是任然打印,是因为函数声明整体提升了
function test(){
var a=123;
console.log('hello'+a);
}
2.发生在函数执行之前
理解这个只需要掌握四部曲
:
(1)创建一个AO(Activation Object)
(2)找形参和变量声明,然后将形参和变量声明作为AO的属性名,属性值为undefined
(3)将实参和形参统一
(4)在函数体内找函数声明,将函数名作为AO对象的属性名,属性值予函数体 那么接下来就放大招了:
var global='window';
function foo(name,sex){
console.log(name);
function name(){};
console.log(name);
var nums=123;
function nums(){};
console.log(nums);
var fn=function(){};
console.log(fn);
}
foo('html');
这里的结果是什么呢?分析如下:
//从上到下
//1、创建一个AO(Activation Object)
AO:{
//2、找形参和变量声明,然后将形参和变量声明作为AO的属性名,属性值为undefined
name:undefined,
sex:undefined,
nums=undefined,
fn:undefined,
//3、将实参和形参统一
name:html,
sex:undefined,
nums=123,
fn:function(){},
//4、在函数体内找函数声明,将函数名作为AO对象的属性名,属性值予函数体
name:function(){},
sex:undefined,
fn:function(){},
nums:123//这里不仅存在nums变量声明,也存在nums函数声明,但是取前者的值
以上步骤得到的值,会按照后面步骤得到的值覆盖前面步骤得到的值
}
//依次打印
//[Function: name]
//[Function: name]
//123
//[Function: fn]
3.发生在全局(内层作用域可以访问外层作用域)
同发生在函数执行前一样,发生在全局的预编译也有自己的三部曲
:
(1)创建GO(Global Object)对象
(2)找全局变量声明,将变量声明作为GO的属性名,属性值为undefined
(3)在全局找函数声明,将函数名作为GO对象的属性名,属性值赋予函数体 举个栗子:
var global='window';
function foo(a){
console.log(a);
console.log(global);
var b;
}
var fn=function(){};
console.log(fn);
foo(123);
console.log(b);
这个例子比较简单,一样的步骤和思路,就不在赘述分析了,相信你已经会了。打印结果依次是:
[Function: fn]
123
window
ReferenceError: b is not defined
好啦,进入正轨,我们接着说作用域链。
三、作用域链
作用域链就可以帮我们找到,为什么内层可以访问到外层,而外层访问不到内层?但是同样的,在认识作用域链之前,我们需要见识见识一些更加晦涩抽象的名词。
执行期上下文
:当函数执行的时候,会创建一个称为执行期上下文的对象(AO对象),一个执行期上下文定义了一个函数执行时的环境。 函数每次执行时,对应的执行上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行期上下文,当函数执行完毕,它所产生的执行期上下文会被销毁。查找变量
:从作用域链的顶端依次往下查找。 3.[[scope]]
:作用域属性,也称为隐式属性,仅支持引擎自己访问。函数作用域,是不可访问的,其中存储了运行期上下文的结合。
我们先看一眼函数的自带属性:
function test(){//函数被创建的那一刻,就携带name,prototype属性
console.log(123);
}
console.log(test.name);//test
console.log(test.prototype);//{} 原型
// console.log(test[[scope]]);访问不到,作用域属性,也称为隐式属性
// test() --->AO:{}执行完毕会回收
// test() --->AO:{}执行完毕会回收
接下来看看作用域链怎么实现的:
var global='window';
function foo(){
function fn(){
var fn=222;
}
var foo=111;
console.log(foo);
}
foo();
分析:
GO:{
foo:function(){}
}
fooAO:{
foo:111,
fn:function(){}
}
fnAO:{
fn:222
}
// foo定义时 foo.[[scope]]---->0:GO{}
// foo执行时 foo.[[scope]]---->0:AO{} 1:GO{} 后访问的在前面
//fn定义时 fn.[[scope]]---->0:fnAO{} 1:fooAO{} 2:GO{}
fnAO:fn的AO对象;fooAO:foo的AO对象
综上而言:作用域链就是[[scope]]中所存储的执行期上下文对象的集合,这个集合呈链式链接,我们把这种链式链接叫做作用域链。
作者:来碗盐焗星球
来源:juejin.cn/post/7116516393100853284
聊一聊前端程序员的现状与挑战
前端这一块,得益于日益更新的前端框架降低了入门门槛,得益于目前全自动、半自动化的开发、测试、上线流程,也得益于目前越来越标准的产品设计流程和规范,等等这些都会让你的开发效率和工作量评估更加透明化。
可能让某些类别的前端工作逐步从一个脑力工作者变为劳动密集型的体力工作者。
前端是一个很广很大的领域,有一定的广度和深度;但是不可否认,也许80%的工作都是简单与单调的,随着技术的升级、技术门槛的降低,经过一些简单快速的培训,越来越多的人可以从事这80%的工作 => 整体看,前端的从业人员越来越多,好像越来越卷了。
但是剩余20%的具有一定复杂性、创造性、创新性、架构设计性、挑战性的工作,却不会受到太多影响,大部分情况,也正是这大约20%的工作,决定了一个产品、一个公司、一个团队的关键部分,所以如何具有足够的能力、经验和理论来承担、组织更具有价值和挑战性的这20%的工作,伴随解决挑战性问题的实战积累更多的经验和解决问题的能力,进入一个正向循环是在开发过程中不断成长和晋升的关键。
所以,前端的入门门槛低了,原本对你来说已经掌握技术的和不容易实现的内容,现在大家也许跳一跳就能伸手够到了。那么自然会要求你也不能安于现状,要学习和掌握更多东西,知识的更新周期也要缩短,才能时刻保持前端技术的领先性。
难点一:快
"前"端,顾名思义,是冲在"前"面的,好比直接服务于群众的派出所和整体把控的市局、省厅,跑在前面的派出所在响应突发事件和执行任务上的反应速度和周期都有更高的要求。
前端的"快",体现在用户需求变化快,技术更新迭代快,和开发响应需要快等方面,都要求你不得不"快"起来。
1.1 用户需求变化快
用户使用的大部分产品的都是具有前端页面的产品,自然相关需求大部分是面向前端的。
后端可能开发一个接口,输入输出保持足够的通用性,只要没有大的变动,几周或者几个月不用变化,更多关注性能和扩展性。
从 需求 => 原型 => 设计 => 实现 的几个环节,前端也是研发岗位中更多和需求打交道的岗位。
所以需要有足够的经验和技术积累,能够对产品经理/客户提供足够灵活和可行(技术可行,时间可行,成本可行)的技术方案,来响应快速变化的需求。
1.2 技术更新迭代快
硬件升级速度快 => 客户需求变化快,相对于后端,大数据,运维,前端的技术更新迭代快是必然的。
一个接一个的框架,一个接一个的版本,今年还在用的16.7,明年发现已经过时了,17.0的生产力和便利度提升一大截。
所以要时刻保持技术的新鲜度,才能保持自身的领先性。
否则很可能一个新框架,新技术的产生,生产力直接翻倍,你学的稍微慢一点,差上半年一年,可能就让新人弯道超车了。
1.3 开发响应需要快
同样,需求要求快,技术迭代快,生产力提升快,自然开发、上线、测试、发布的周期也更加快,而前端是最受这个影响的。
因为看得到,摸得着,你的工作量是透明的,所以你的工期是可预估的。
也许下面的场景会经常出现:
上午出设计,预估开发6小时,测试0.5小时,那么今晚上线
11点测试出bug,预计修复半小时,12:00 之前要修复上线
难点二: 广
前端是一个同时具有广度和深度的领域,要解决的问题种类和范围覆盖面过于广,所以即使你的数据结构算法掌握的不够牢固、代码基本功不够扎实、对操作系统、线程、并发的概念理解的不够深入,但如果你能够具有一定前端知识的广度和经验,也能够让你在能够让你在部分的前端开发场景中游刃有余,而这里面的每一个知识和经验也许并不需要太多的技术积累,知道了就是知道了,不知道就是不知道,所以前端的积累很重要。
临时想到的一些比如,数不完,且不同领域用到的都会不一样
基础类: 常见的开发框架(React, Vue, Angular)有什么不同,开发框架的不同版本有什么特性(Vue 2-3, React 16-17-18), ECMAScript 2021,2022最近更新了什么特性等等
基础组件: 数据持久化, 数据状态管理, 路由管理, UI框架等
部署与发布: 打包过程控制, 依赖管理, web服务搭建, SEO, 性能优化等
布局类: 不同的CSS框架, 常见布局设计模式等
通信/协议类: HTTP1&2, HTTPS, RESTful, 常见认证协议, CORS, 长连接, SSO, DNS, TCP等
可视化: 2D, 3D, 常见可视化组件使用熟练度, canvas, webGL, 基础/进阶动画效果等
多媒体: 视频播放, 地图, 支付, 分享, 埋点, 兼容性, 声音等
框架/大前端: 微前端, 常见native开发框架, 小程序等
开发流程: 常用测试框架, 常见设计框架, 产品设计流程, 软件工程等
开源细节能力积累: 日期, 二维码, 水印, 动效, 加密, 压缩等
难点三: 深
如果你接触的产品日活达到数十万人,如果你开发的模块是团队内的公共模块,如果你要负责一条核心产品线前端整体开发把控,如果你要负责企业多个前端团队的系统开发的多条产品线,会遇到更多样,更复杂,更具有挑战性的前端问题,简单总结起来大致有如下几点。
3.1 代码基本功与设计模式: 能够处理复杂的数据状态与程序逻辑
这个属于所有程序员都需要面对和掌握的基本功,但这一块近几年由于框架的兴起,很多人投身于前端新知识的学习和普通功能页面的开发上,认为能够开发出来就行,从而忽视了作为程序员最基本的要素:代码。
但是要知道,框架、新的语法糖、新的语言会一直在变的,重要的和不变的还是代码基本功(涵盖很多方面),因为这个很大程度上可能会决定了你的发展上限,在关键时刻是你和别人拉开差距的关键,以及你的开发效率,开发质量,表现出的靠谱程度,能够同时掌控的产品线数量,都和你的代码基本功息息相关。
3.2 具有丰富表现能力的可视化效果
既然是前端,另一块不可忽视的就是可视化效果,这里说的不是表格,表单,按钮,弹框,css样式这种基础的可视化,目前的文档完善程度和强大的搜索引擎,都应该能够让你很快按照设计稿完成静态页面的样式渲染。
这里的可视化效果是通过使用svg, canva, webGL等技术进行更加灵活的可视化渲染的方案,可以说在前端开发中是很独立也很不同的一块。
将这些技术和相关的技术框架echarts, d3.js, three.js用活用好具有一定的挑战和理论知识,也十分也场景相关,但是这一块的熟练人才目前在市场上还是稀缺的,很多高阶的开源组件和大型产品的核心页面,都少不了这些技术的加持。
3.3 具有合理设计的前端公共组件
这一块其实也可以算代码基本功,也可以算设计模式与面向对象思想。当你接触到中大型项目或者企业中的多个团队都需要使用你的组件时,你需要对组件的设计、接口、内部实现进行充分的考虑和设计:是否符合企业统一的设计风格,是否在不同浏览器不同分辨率上都能正常显示,是否兼容不同的框架版本,源码是否容易维护是否可以内部/外部开源,组件调用接口是否合理是否足够灵活易于维护,如何安装与升级等等等等。
就好比要大家可能经常用的Ant Design或者其他UI框架的table组件,一般说明文档就十几页,对应背后的程序要经过更加精细的设计与实现。
3.4 前端架构设计 & 工作流程把控
当你负责多条产线的前端研发时,面对相对频繁的人员流动,面对日新月异的技术框架,面对公司内部的安全、部署、风格、规范要求,面对第三方测评公司或者合作单位的技术要求时,你可能会发现,如果没有一个统一的,良好设计的前端架构,会给团队之间的切换、合作、新人的培训、技术方案的统一带来很多麻烦,直接影响就是人效不足,分支混乱,经过一段时间以上的代码难以维护,好像狗熊掰棒子一样,好像都很忙,但只会越来越忙丝毫得不到改善。
这就需要一个更大的框架设计和工作流程定义,这个既依赖你的技术深度,也依赖你的技术经验广度;你的技术和经验要能够服众,你的方案要足够灵活能够适应互联网和企业的发展,在你定义的框架下前端整体的效率、质量能够得到保障与提升。
作者:JosiahZhao
来源:juejin.cn/post/7113560877067927560
搜索中常见数据结构与算法探究
1 前言
ES 现在已经被广泛的使用在日常的搜索中,Lucene 作为它的内核值得我们深入研究,比如 FST,下面就用两篇分享来介绍一些本文的主题:
- 第一篇主要介绍数据结构和算法基础和分析方法,以及一些常用的典型的数据结构;
- 第二篇主要介绍图论,以及自动机,KMP,FST 等算法;
下面开始第一篇
2 引言
“算法是计算机科学领域最重要的基石之一 “
“编程语言虽然该学,但是学习计算机算法和理论更重要,因为计算机算法和理论更重要,因为计算机语言和开发平台日新月异,但万变不离其宗的是那些算法和理论,例如数据结构、算法、编译原理、计算机体系结构、关系型数据库原理等等。“——《算法的力量》
2.1 提出问题
2.1.1 案例一
设有一组 N 个数而要确定其中第 k 个最大者,我们称之为选择问题。常规的解法如下:
- 该问题的一种解法就是将这 N 个数读进一个数组中,在通过某种简单的算法,比如冒泡排序法,以递减顺序将数组排序,然后返回位置 k 上的元素。
- 稍微好一点的算法可以先把前 k 个元素读入数组并对其排序。接着,将剩下的元素再逐个读入。当新元素被读到时,如果它小于数组中的第 k 个元素则忽略之,否则就将其放到数组中正确的位置上,同时将数组中的一个元素挤出数组。当算法终止时,位于第 k 个位置上的元素作为答案返回。
这两种算法编码都很简单,但是我们自然要问:哪个算法更好?哪个算法更重要?还是两个算法都足够好?使用 N=30000000 和 k=15000000 进行模拟将发现,两个算法在合理的时间量内均不能结束;每一种算法都需要计算机处理若干时间才能完成。
其实还有很多可以解决这个问题,比如二叉堆,归并算法等等。
2.2.2 案例二
输入是由一些字母构成的一个二维数组以及一组单词组成。目标是要找出字谜中的单词,这些单词可能是水平、垂直、或沿对角线上任何方向放置。下图所示的字谜由单词 this、two、fat 和 that 组成。
现在至少也有两种直观的算法来求解这个问题:
- 对单词表中的每个单词,我们检查每一个有序三元组(行,列,方向)验证是否有单词存在。这需要大量嵌套的 for 循环,但它基本上是直观的算法。
- 对于每一个尚未越出迷板边缘的有序四元组(行,列,方向,字符数)我们可以测试是否所指的单词在单词表中。这也导致使用大量嵌套的 for 循环。
上述两种方法相对来说都不难编码,但如果增加行和列的数量,则上面提出的两种解法均需要相当长的时间。
以上两个案例中,我们可以看到要写一个工作程序并不够。如果这个程序在巨大的数据集上运行,那么运行时间就变成了重要问题。
那么,使用自动机理论可以快速的解决这个问题,下一篇中给大家详细的分析。
3 数据结构与算法基础
3.1 数据结构基础
3.1.1 什么是数据结构
在计算机领域中,数据是信息的载体,是能够输入到计算机中并且能被计算机识别、存储和处理的符号的总称。数据结构是指数据元素和数据元素之间的相互关系或数据的组织形式。数据元素是数据的的基本单位,数据元素有若干基本项组成。
3.1.2 数据之间的关系
数据之前的关系分为两类:
- 逻辑关系
表示数据之间的抽象关系,按每个元素可能具有的前趋数和直接后继数将逻辑结构分为线性结构和非线性结构。逻辑关系或逻辑结构有如下特点:
- 只是描述数据结构中数据元素之间的联系规律;
- 是从具体问题中抽象出来的数学模型,是独立于计算机存储器的(与硬件无关)
逻辑结构的分类如下:
- 线性结构
- 树形结构
- 图状结构
- 其他结构
- 物理关系
逻辑关系在计算中的具体实现方法,分为顺序存储方法、链式存储方法、索引存储方法、散列存储方法。物理关系或物理结构有如下特点:
- 是数据的逻辑结构在计算机存储其中的映像;
- 存储结构是通过计算机程序来实现,因而是依赖于具体的计算机语言的;
物理结构分类如下:
- 顺序结构
- 链式结构
- 索引结构
3.2 算法基础
3.2.1 基础概念
算法是为求解一个问题需要遵循的、被清楚指定的简单指令的集合。对于一个问题,一旦某种算法给定并且被确定是正确的,那么重要的一步就是确定该算法将需要多少诸如时间或空间等资源量的问题。如果一个问题的求解算法竟然需要长达一年时间,那么这种算法就很难能有什么用处。同样,一个需要若干个 GB 的内存的算法在当前的大多数机器上也是无法使用的。
3.2.2 数学基础
一般来说,估算算法资源消耗所需的分析是一个理论问题,因此需要一套数学分析法,我们先从数学定义开始。
- 定理 1:如果存在正常数 c 和 n0,使得当 N>= n0 时,T (N) <= cf (N),则记为 T (N) = O (f (N))。
- 定理 2:如果存在正常数 c 和 n0,使得当 N>=n0 时,T (N) <= cg (N),则记为 T (N) = Ω(g (N))。
- 定理 3:T (N) = θ(h (N)) 当且仅当 T (N) = O (h (N)) 和 T (N) = Ω(h (N))。
- 定理 4:如果对每一个正常数 c 都存在常数 n0 使得当 N>n0 时,T (N) < cp (N),则 T (N) = o (p (N))。
这些定义的目的是要在函数间建立一种相对的级别。给定两个函数,通常存在一些点,在这些点上一个函数的值小于另一个函数的值,因此,一般宣称 f (N)<g (N),是没有什么意义的。于是,我们比较他们的相对增长率。当将相对增长率应用到算法分析时,会明白它是重要的度量。
如果用传统的不等式来计算增长率,那么第一个定义 T (N) = O (f (N)) 是说 T (N) 的增长率小于或者等于 f (N) 的增长率。第二个定义 T (N) = Ω(g (N)) 是说 T (N) 增长率大于或者等于 g (N) 的增长率。第三个定义 T (N) = θ(h (N)) 是说 T (N) 的增长率等于 h (N) 的增长率。最后一个定义 T (N) = o (p (N)) 说的则是 T (N) 的增长率小于 p (N) 的增长率。他不同于大 O,因为大 O 包含增长率相同的可能性。
要证明某个函数 T (N) = O (f (N)) ,通常不是形式的使用这些定义,而是使用一些已知的结果(比如说 T (N) = O (log (N)))。一般来说,这就意味着证明是非常简单的计算而不应涉及微积分,除非遇到特殊情况。如下是常见的已知函数结果
- c(常数函数)
- logN(对数函数)
- logN^2(对数平方函数)
- N(线性函数)
- NlogN
- N^2(二次函数)
- N^3(三次函数)
- 2^N(指数函数)
在使用已知函数结果时,有几点需要注意:
- 首先,将常数或低阶项放进大 O 是非常坏的习惯。不要写成 T (N) = O (2*N^2) 或 T (N) = O (N^2 + N)。这两种情形下,正确的形式是 T (N) = O (N^2)。也就是说低阶项一般可以被忽略,而常数也可以弃掉。
- 其次,我们总能够通过计算极限 limN→∞f (N)/g (N)(极限公式)来确定两个函数 f (N) 和 g (N) 的相对增长率。该极限可以有四种可能的值:
极限是 0:这意味着 f (N) = o (g (N))。
极限是 c != 0: 这意味着 f (N) = θ(g (N))。
极限是∞ :这意味着 g (N) = o (f (N))。
极限摆动:二者无关。
3.2.3 复杂度函数
正常情况下的复杂度函数包含如下两种:
时间复杂度
空间复杂度
时间和空间的度量并没有一个固定的标准,但是在正常情况下,时间复杂度的单位基本上是以一次内存访问或者一次 IO 来决定。空间复杂度是指在算法执行过程中需要占用的存储空间。对于一个算法来说,时间复杂度和空间复杂度往往是相互影响,当追求一个好的时间复杂度时,可能会使空间复杂度变差,即可能占用更多的存储空间;反之,当追求一个较好的空间复杂度时,可能会使时间复杂度变差,即可能占用较长的运算时间。
3.3 知识储备
3.3.1 质数分辨定理(HashTree 的理论基础)
简单的说就是,n 个不同的质数可以分辨的连续数的个数和他们的乘机相同。分辨是指这些连续的整数不可能有相同的余数序列。
3.3.2 Hash 算法
1)Hash
Hash 一般翻译成散列,也可以直接音译成哈希,就是把任意长度的输入,通过散列算法变换成固定长度的输出,该输入就是散列值。不同的输入可能散列成相同的值,确定的散列值不可能确定一个输入。
- 常见的 Hash 算法
- MD4:消息摘要算法;
- MD5:消息摘要算法,MD4 的升级版本;
- SHA-1:SHA-1 的设计和 MD4 相同原理,并模仿该算法
自定义 HASH 算法:程序设计者可以自定义 HASH 算法,比如 java 中重写的 hashCode () 方法
- Hash 碰撞
解决 Hash 碰撞常见的方法有一下几种:
- 分离链接法(链表法):做法是将散列到同一个值的所有元素保留在一个表中,例如 JDK 中的 HashMap;
- 探测散列表:当发生 Hash 碰撞时,尝试寻找另外一个单元格,直到知道到空的单元为止。包括:线性探测法,平方探测法,双散列。
3.3.3 树结构的基本概念
- 树的递归定义:一棵树是一些节点的集合。这个集合可以是空集;若不是空集,则树由根节点 root 以及 0 个或多个非空的子树组成,这些子树中每一棵的根都被来自根 root 的一条有向的边所连接;
- 树叶节点:没有儿子节点称为树叶;
- 深度:对于任意节点 ni,ni 的深度为从根到 ni 的唯一路径的长;
- 高度:对于任意节点 ni,ni 的高度为从 ni 到一片树叶的最长路径的长。
- 树的遍历:树的遍历分为两种,先序遍历和后续遍历;
3.3.4 二叉搜索树
二叉搜索树是一棵二叉树,其中每个节点都不能有多于两个子节点。
对于二叉查找树的每一个节点 X,它的左子树中所有项的值都小于 X 节点中的项,而它的右子树中所有项的值大于 X 中的项;
4 常见数据结构与算法分析
4.1 线性数据结构
4.1.1 HashMap
总述
HashMap 是开发中最常用的数据结构之一,数据常驻于内存中,对于小的数据量来说,HashMap 的增删改查的效率都非常高,复杂度接近于 O (1)。数据结构和算法
- HashMap 由一个 hash 函数和一个数组组成;
- 数据插入,当进入到 map 的时候,根据 hash (key) 找到对应点位置,如果位置为空,直接保存,如果位置不为空,则使用链表的方式处理;为了解决遍历链表所增加的时间,JDK 中的链表在大小增大到 8 时,将会演变成红黑树以降低时间复杂度。为什么开始使用链表,后面使用红黑树:
- 数据量较小的时候,链表的查询效率相对来说也比较高,使用红黑树占用空间比链表要大;
- 为什么选择 8,请参考泊松分布;
- 查找和删除的过程,同插入的过程类似;
- HashMap 可以支持自动扩容,扩容机制需要看具体的实现;
- 优缺点
- 优点:动态可变长存储数据,快速的查询速度,查询复杂度接近 O (1);
- 缺点:只支持小数据量的内存查询;
- 使用场景
- 在内存中小数据量的数据保存和快速查找;
4.1.2 Bloom Filter(布隆过滤器)
- 总述
布隆过滤器算法为大数据量的查找提供了快速的方法,时间复杂度为 O (k),布隆过滤器的语义为:
- 布隆过滤器的输出为否定的结果一定为真;
- 布隆过滤器的输出为肯定的结果不一定为真;
- 数据结构和算法
布隆过滤器的具体结构和算法为:
- 布隆过滤器包含 k 个 hash 函数,每个函数可以把 key 散列成一个整数(下标);
- 布隆过滤器包含了一个长度为 n 的 bit 数组(向量数组),每个 bit 的初始值为 0;
- 当某个 key 加入的时候,用 k 个 hash 函数计算出 k 个散列值,并把数组中对应的比特置为 1;
- 判断某个 key 是否在集合时,用 k 个 hash 函数算出 k 个值,并查询数组中对应的比特位,如果所有的 bit 位都为 1,认为在集合中;
- 布隆过滤器的大小需要提前评估,并且不能扩容;
布隆过滤器的插入过程如下:
判断某个 key 是否在集合时,用 k 个 hash 函数算出 k 个值,并查询数组中对应的比特位,如果所有的 bit 位都为 1,认为在集合中
- 布隆过滤器无法删除数据;
- 布隆过滤器查询的时间复杂度为 O (k);
- 布隆过滤器空间的占用在初始化的时候已经固定不能扩容。
- 优缺点
- 优点:布隆过滤器在时间和空间上都有巨大的优势。布隆过滤器存储空间和插入 / 查找时间都是常数。布隆过滤器不需要存储数据本身,节省空间。
- 缺点:布隆过滤器的缺点是有误差。元素越多误差越高。可以通过提高 hash 函数的个数和扩大 bit 数组的长度来降低误差率;
- 场景
- 使用场景:缓存击穿,判断有无。
4.1.3 SkipList(跳表)
总述
跳表是一种特殊的链表,相比一般的链表有更高的查找效率,可比拟二差查找树,平均期望的插入,查找,删除的时间复杂度都是 O (logN);数据结构和算法
跳表可视为水平排列(Level)、垂直排列(Row)的位置(Position)的二维集合。每个 Level 是一个列表 Si,每个 Row 包含存储连续列表中相同 Entry 的位置,跳表的各个位置可以通过以下方式进行遍历。
- After (P):返回和 P 在同一 Level 的后面的一个位置,若不存在则返回 NULL;
- Before (P):返回和 P 在同一 Level 的前面的一个位置,若不存在则返回 NULL;
- Below (P):返回和 P 在同一 Row 的下面的一个位置,若不存在则返回 NULL;
- Above (P):返回和 P 在同一 Row 的上面的一个位置,若不存在则返回 NULL;
有顺序关系的多个 Entry (K,V) 集合 M 可以由跳表实现,跳表 S 由一系列列表 {S0,S1,S2,……,Sh} 组成,其中 h 代表的跳表的高度。每个列表 Si 按照 Key 顺序存储 M 项的子集,此外 S 中的列表满足如下要求:
- 列表 S0 中包含了集合 M 的每个一个 Entry;
- 对于 i = 1 ,…… ,h-1 列表 Si 包含列表 Si-1 中 Entry 的随机子集;
Si 中的 Entry 是从 Si-1 中的 Entry 集合中随机选择的,对于 Si-1 中的每一个 Entry,以 1/2 的概率来决定是否需要拷贝到 Si 中,我们期望 S1 有大约 n/2 个 Entry,S2 中有大约 n/4 个 Entry,Si 中有 n/2^i。跳表的高度 h 大约是 logn。从一个列表到下一个列表的 Entry 数减半并不是跳表的强制要求;
插入的过程描述,以上图为例,插入 Entry58:
- 找到底层列表 S0 中 55 的位置,在其后插入 Entry58;
- 假设随机函数取值为 1,紧着回到 20 的位置,在其后插入 58,并和底层列表 S0 的 - Entry58 链接起来形成 Entry58 的 Row;
- 假设随机函数取值为 0,则插入过程终止;
下图为随机数为 1 的结果图:
删除过程:同查找过程。
时间复杂度
- 查找包括两个循环,外层循环是从上层 Level 到底层 Level,内层循环是在同一个 Level,从左到右;
- 跳表的高度大概率为 O (logn),所以外层循环的次数大概率为 O (logn);
- 在上层查找比对过的 key,不会再下层再次查找比对,任意一个 key 被查找比对的概率为 1/2,因此内存循环比对的期望次数是 2 也就是 O (1);
- 因此最终的时间复杂度函数 O (n) = O (1)*O (logn) 也就是 O (logn);
空间复杂度
- Level i 期望的元素个数为 n/2^i;
- 跳表中所有的 Entry(包含同一个 Entry 的 Row 中的元素) Σ n/2^i = nΣ1/2^i,其中有级数公式得到 Σ1/2^i < 2;
- 期望的列表空间为 O (n);
- 优缺点
- 优点:快速查找,算法实现简单;
- 缺点:跳表在链表的基础上增加了多级索引以提升查询效率,使用空间来换取时间,必然会增加存储的负担。
- 使用场景
许多开源的软件都在使用跳表:
- Redis 中的有序集合 zset
- LevelDB Hbase 中的 memtable
- Lucene 中的 Posting List
4.2 简单非线性数据结构
4.2.1 AVL
总述
AVL 树是带有平衡条件的二叉查找树,这个平衡条件必须要容易保持,而且它保证树的深度必须是 O (logN)。在 AVL 树中任何节点的两个子树的高度最大差别为 1。数据结构和算法
AVL 树本质上还是一棵二叉查找树,有以下特点:
- AVL 首先是一棵二叉搜索树;
- 带有平衡条件:每个节点的左右子树的高度之差的绝对值最多为 1;
- 当插入节点或者删除节点时,树的结构发生变化导致破坏特点二时,就要进行旋转保证树的平衡;
针对旋转做详细分析如下:
我们把必须重新平衡的节点叫做 a,由于任意节点最多有两个儿子,因此出现高度不平衡就需要 a 点的两棵子树的高度差 2。可以看出,这种不平衡可能出现一下四种情况:
- 对 a 的左儿子的左子树进行一次插入;
- 对 a 的左儿子的右子树进行一次插入;
- 对 a 的右儿子的左子树进行一次插入;
- 对 a 的右儿子的柚子树进行一次插入;
情形 1 和 4 是关于 a 的对称,而 2 和 3 是关于 a 点的对称。因此理论上解决两种情况。
第一种情况是插入发生在外侧的情况,该情况通过对树的一次单旋转而完成调整。第二种情况是插入发生在内侧的情况,这种情况通过稍微复杂些的双旋转来处理。
单旋转的简单示意图如下:
双旋转的简单示意图如下:
- 优缺点
- 优点:使用二叉查找算法时间复杂度为 O (logN),结构清晰简单;
- 缺点:插入和删除都需要进行再平衡,浪费 CPU 资源;
- 使用场景
- 少量数据的查找和保存;
.4.2.2 Red Black Tree
总述
红黑树是一种自平衡的二叉查找树,是 2-3-4 树的一种等同,它可以在 O (logN) 内做查找,插入和删除。数据结构和算法
在 AVL 的基础之上,红黑树又增加了如下特点:
- 每个节点或者是红色,或者是黑色;
- 根节点是黑色;
- 如果一个节点时红色的,那么它的子节点必须是黑色的;
- 从一个节点到一个 null 引用的每一条路径必须包含相同数目的黑色节点;
红黑树的示意图如下(图片来源于网络):
那么将一个节点插入到红黑树中,需要执行哪些步骤呢?
- 将红黑树当做一棵二叉搜索树,将节点插入;
- 将插入的节点着色为红色;
- 通过一系列的旋转和着色等操作,使之重新成为一棵红黑树;
在第二步中,被插入的节点被着为红色之后,他会违背哪些特性呢
- 对于特性 1,显然是不会违背;
- 对于特性 2,显然也是不会违背;
- 对于特性 4,显然也是不会违背;
- 对于特性 3,有可能会违背,我们将情况描述如下
- 被插入的节点是根节点:直接把此节点涂为黑色;
- 被插入的节点的父节点是黑色:什么也不需要做。节点被插入后,仍然是红黑树;
- 被插入的节点的父节点是红色:此种情况下与特性 3 违背,所以将情况分析如下:
- 当前节点的父节点是红色,且当前节点的祖父节点的另一个子节点也是红色。处理策略为:将父节点置为黑色、将叔叔节点置为黑色、将祖父节点置为红色;
- 当前节点的父节点是红色,叔叔节点时黑色,且当前节点是其父节点的右子节点。将父节点作为新的当前节点、以新的当前节点作为支点进行左旋;
- 当前节点的父节点是红色,叔叔节点时黑色,且当前节点时父节点的左子节点。将父节点置为黑色、将祖父节点置为红色、以祖父节点为支点进行右旋;
定理:一棵含有 n 个节点的红黑树的高度至多为 2log (N+1),证明过程请查看参考资料。
由此定理可推论红黑树的时间复杂度为 log (N);
- 优缺点
- 优点:查询效率高,插入和删除的失衡的代销比 AVL 要小很多;
- 缺点:红黑树不追求完全平衡;
- 使用场景
- 红黑树的应用很广泛,主要用来存储有序的数据,时间复杂度为 log (N),效率非常高。例如 java 中的 TreeSet、TreeMap、HashMap 等
4.2.3 B+Tree
- 总述
提起 B+Tree 都会想到大名鼎鼎的 MySql 的 InnoDB 引擎,该引擎使用的数据结构就是 B+Tree。B+Tree 是 B-Tree(平衡多路查找树)的一种改良,使得更适合实现存储索引结构,也是该篇分享中唯一一个与磁盘有关系的数据结构。首先我们先了解一下磁盘的相关东西。
系统从磁盘读取数据到内存时是以磁盘块(block)为基本单位,位于同一块磁盘块中的数据会被一次性读取出来。InnoDB 存储引擎中有页(Page)的概念,页是引擎管理磁盘的基本单位。
- 数据结构和算法
首先,先了解一下一棵 m 阶 B-Tree 的特性:
- 每个节点最多有 m 个子节点;
- 除了根节点和叶子结点外,其他每个节点至少有 m/2 个子节点;
- 若根节点不是叶子节点,则至少有两个子节点;
- 所有的叶子结点都是同一深度;
- 每个非叶子节点都包含 n 个关键字
- 关键字的个数的关系为 m/2-1 < n < m -1
B-Tree 很适合作为搜索来使用,但是 B-Tree 有一个缺点就是针对范围查找支持的不太友好,所以才有了 B+Tree;
那么 B+Tree 的特性在 B-Tree 的基础上又增加了如下几点:
- 非叶子节点只存储键值信息;
- 所有的叶子节点之间都有一个链指针(方便范围查找);
- 数据记录都存放在叶子节点中;
我们将上述特点描述整理成下图(假设一个页(Page)只能写四个数据):
这样的数据结构可以进行两种运算,一种是针对主键的范围查找和分页查找,另外一种是从根节点开始,进行随机查找;
- 优缺点
- 优点:利用磁盘可以存储大量的数据,简单的表结构在深度为 3 的 B+Tree 上可以保存大概上亿条数据;B+Tree 的深度大概也就是 2~4,深度少就意味这 IO 会减少;B+Tree 的时间复杂度 log (m) N
- 缺点:插入或者删除数据有可能会导致数据页分裂;即使主键是递增的也无法避免随机写,这点 LSM-Tree 很好的解决了;无法支持全文索引;
- 使用场景
- 使用场景大多数数据库的引擎,例如 MySql,MongoDB 等
4.2.4 HashTree
总述
HashTree 是一种特殊的树状结构,根据质数分辨定理,树每层的个数为 1、2、3、5、7、11、13、17、19、23、29…..数据结构和算法
从 2 起的连续质数,连续 10 个质数接可以分辨大约 6464693230 个数,而按照目前 CPU 的计算水平,100 次取余的整数除法操作几乎不算什么难事。
我们选择质数分辨算法来构建一颗哈希树。选择从 2 开始的连续质数来构建一个 10 层的哈希树。第一层节点为根节点,根节点先有 2 个节点,第二层的每个节点包含 3 个子节点;以此类推,即每层节点的数据都是连续的质数。对质数进行取余操作得到的数据决定了处理的路径。下面我们以随机插入 10 个数(442 9041 3460 3164 2997 3663 8250 908 8906 4005)为例,来图解 HashTree 的插入过程,如下:
HashTree 的节点查找过程和节点插入过程类似,就是对关键字用质数取余,根据余数确定下一节点的分叉路径,知道找到目标节点。如上图,在从对象中查找所匹配的对象,比较次数不超过 10 次,也就是说时间复杂度最多是 o (1).
删除的过程和查找类似。
- 优缺点:
- 优点:结构简单,查找迅速,结构不变。
- 缺点:非有序性。
4.2.5 其他数据结构
作者:欧子有话说
链接:https://juejin.cn/post/7153071026916720677
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Android 带你玩转单元测试
前言
为什么要用到单元测试呢,一般开发谁会写单元测试,反正我认识的人都不会做,又耗时间,效果又一般,要是在单元测试的代码里面又出BUG的话又要改半天,麻烦。
但是有的时候真的是不得不用,比如说你有一步逻辑操作,你想去判断这逻辑操作是否正确。但是运行这步操作之前有10步操作,然后这个逻辑操作的情况一共有10种(举个比较极端的栗子)。那如果你运行Debug检验每一种情况的时候,都需要每种情况先执行10步操作才能验证,那就很麻烦啊。
所以这时候你可能就会需要用到单元测试,直接对单步操作进行测试,也不用把整个项目都跑起来,直接对特定的方法进行测试。
但说句实在话,虽然开发流程中规定要进行单元测试。但这单元测试谁来做,还不是研发来做,我们代码平时都很赶,还有什么时间去写单元测试的逻辑和用例,所以我觉得仅仅对某部分base库或者重要的逻辑做测试就够了。
搭建环境
搭建环境很简单,在gradle中添加依赖
testImplementation 'org.mockito:mockito-core:2.25.1'
版本号肯定不是固定的,可以直接在File-Project Structure中查找这个库,这样肯定是最新版本,不过要记得把implementation变成testImplementation 。
然后我们创建相应的测试类,也很简单,以前我是手动创建的,之前get到别人的一招。
光标放到你想测的类的类名,然后alt + enter , 选择Create Test\
自动会帮你填好name,你想改也行,下面可以选before和after,就是你想在测试前和测试后做的操作的方法。再下面Member可惜选着对应的方法。
选择好之后点击OK,然后会让你选择androidTest下还是test下,默认创建android项目不是帮你创建3个文件夹嘛\
我们因为是只对某个方法做测试,所以选择test(两个文件夹的区别以后再说)。
单元测试
假如我想测一个功能,就测我以前写的那个Gson解析泛型的功能吧。
public T getDataContent(String jsondata){
Gson gson = new Gson();
Type type = getClass().getGenericSuperclass();
Type[] types = ((ParameterizedType) type).getActualTypeArguments();
Type ty = new ParameterizedTypeImpl(BaseResponse.class, new Type[]{types[0]});
BaseResponse<T> data = gson.fromJson(jsondata, ty);
return data.content;
}
看看BaseResponse
public class BaseResponse<T> {
public String ret;
public String msg;
public T content;
}
因为这个是一个很重要的功能,每个地方的网络请求都会走这段代码,所以我要测试它,看看不同的情况是否能得到我想要的结果。
按照上面的做法生成一个测试的类和方法
public class HttpCallBackTest {
@Test
public void getDataContent(){
}
}
可以发现在androidstudio里面,getDataContent方法左边有个运行按钮,点击就可以单独对这个方法进行测试。
现在我们要测试这个功能,那么就需要写测试用例,假如我这边写4个测试用例看看能不能都成功解析,4个json字符串(在代码里面加了换行符所以可能有点难看)。
String mockData = "{\n" +
"\t"ret":"1",\n" +
"\t"msg":"success",\n" +
"\t"content":{\n" +
"\t\t"id":"10000",\n" +
"\t\t"sex":"男",\n" +
"\t\t"age":18\n" +
"\t}\n" +
"}";
String mockData2 = "{\n" +
"\t"ret":"1",\n" +
"\t"msg":"success",\n" +
"\t"content":[\n" +
"\t\t{\n" +
"\t\t\t"id":"10000",\n" +
"\t\t\t"sex":"男",\n" +
"\t\t\t"age":"18"\n" +
"\t\t},\n" +
"\t\t{\n" +
"\t\t\t"id":"10001",\n" +
"\t\t\t"sex":"女",\n" +
"\t\t\t"age":"16"\n" +
"\t\t}\n" +
"\t]\n" +
"}";
String mockData3 = "{\n" +
"\t"ret":"1",\n" +
"\t"msg":"success",\n" +
"\t"content": "aaa"\n" +
"}";
String mockData4 = "{\n" +
"\t"ret":"1",\n" +
"\t"msg":"success",\n" +
"\t"content": []\n" +
"}";
写个对象来接收
public static class TestData{
public String id;
public String sex;
public int age;
}
现在来写测试的代码
(1)第一个测试用例
@Test
public void getDataContent(){
httpCallBack = new HttpCallBack<TestData>();
TestData testData = (TestData) httpCallBack .getDataContent(mockData);
assertEquals("10000",testData.id);
assertEquals("男",testData.sex);
assertEquals(18,testData.age);
}
测试用到的assertEquals方法,这个之后会详细讲。
可以看到下边会有打印 Process finished with exit code 0 说明测试通过,如果不通过会显示详细的不通过的信息。
比如说我写的 assertEquals(12,testData.age); ,错误的情况会提示
如果是代码错误的话也会报出详细的Exception信息。
(2)第二个测试用例
@Test
public void getDataContent(){
httpCallBack = new HttpCallBack<Lits<TestData>>();
Lits<TestData> testDatas = (Lits<TestData>) httpCallBack .getDataContent(mockData2);
assertEquals("女",testDatas.get(1).sex);
}
(3)第三个测试用例
@Test
public void getDataContent(){
httpCallBack = new HttpCallBack<String>();
String testData = (String ) httpCallBack .getDataContent(mockData3);
assertEquals("aaa",testData);
}
(4)第四个测试用例
@Test
public void getDataContent(){
httpCallBack = new HttpCallBack<Lits<TestData>>();
Lits<TestData> testDatas = (Lits<TestData>) httpCallBack .getDataContent(mockData4);
assertEquals(0,testDatas.size());
}
4个用例如果都通过,说明我这个解析json泛型的方法基本不会有问题。
当然,可以把4种情况都写在一起,这样就只用跑一次,我这里是为了看清楚点所有分开写。
这样就是一个简单的单元测试的流程。
assert
从上面可以看出最主要判断测试正确和错误的方法是用assert(断言)。
而这些方法都是属于Assert类,大概的断言方法有这些
其中 assertThat 是一个比较高级的用法,这个以后再说,不过我个人基本是没有用过assertThat ,单单其它的几个方法基本就够用了。
补充
可能有的朋友有些时候觉得测一个类难以下手,比如还是我说的解析代码,你是这样写的。
public void requestFinish(String jsonData){
......
......
Gson gson = new Gson();
Type type = getClass().getGenericSuperclass();
Type[] types = ((ParameterizedType) type).getActualTypeArguments();
Type ty = new ParameterizedTypeImpl(BaseResponse.class, new Type[]{types[0]});
BaseResponse<T> data = gson.fromJson(jsondata, ty);
// 假如用回调的方式
callback.finish(data.content);
......
}
比如这样,要怎么断言,我这个方法中又不仅仅只有解析的代码,还有其他的代码,而且我这个方法是一个void方法,不像上面一样有返回值的。
其实很简单,要不然就判断这个方法的外层那个方法,要不然就像我一样单独把那块功能代码抽出来。我是建议抽出来,也符合单一职权。
总结
这是我自己旧博客的文章,原地址 http://www.jianshu.com/p/472c4c35e… ,现在使用单元测试会比之前更方便,当你写了一个很复杂的方法,但你想测试不同的输入会输出不同的情况,如果你不用单元测试,你就需要每次改输入的变量然后run,这种情况下使用单元测试会帮助你剩下很多的时间,具体的还要视情况而定。
作者:流浪汉kylin
链接:https://juejin.cn/post/7149750533207621668
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »
超级全面的Flutter性能优化实践
前言
Flutter是谷歌的移动UI框架,可以快速在iOS和Android上构建高质量的原生用户界面。 Flutter可以与现有的代码一起工作。在全世界,Flutter正在被越来越多的开发者和组织使用,并且Flutter是完全免费、开源的,可以用一套代码同时构建Android和iOS应用,性能可以达到原生应用一样的性能。但是,在较为复杂的 App 中,使用 Flutter 开发也很难避免产生各种各样的性能问题。在这篇文章中,我将介绍一些 Flutter 性能优化方面的应用实践。
一、优化检测工具
flutter编译模式
Flutter支持Release、Profile、Debug编译模式。
Release模式,使用AOT预编译模式,预编译为机器码,通过编译生成对应架构的代码,在用户设备上直接运行对应的机器码,运行速度快,执行性能好;此模式关闭了所有调试工具,只支持真机。
Profile模式,和Release模式类似,使用AOT预编译模式,此模式最重要的作用是可以用DevTools来检测应用的性能,做性能调试分析。
Debug模式,使用JIT(Just in time)即时编译技术,支持常用的开发调试功能hot reload,在开发调试时使用,包括支持的调试信息、服务扩展、Observatory、DevTools等调试工具,支持模拟器和真机。
通过以上介绍我们可以知道,flutter为我们提供 profile模式启动应用,进行性能分析,profile模式在Release模式的基础之上,为分析工具提供了少量必要的应用追踪信息。
如何开启profile模式?
如果是独立flutter工程可以使用flutter run --profile启动。如果是混合 Flutter 应用,在 flutter/packages/flutter_tools/gradle/flutter.gradle 的 buildModeFor 方法中将 debug 模式改为 profile即可。
检测工具
1、Flutter Inspector (debug模式下)
Flutter Inspector有很多功能,其中有两个功能更值得我们去关注,例如:“Select Widget Mode” 和 “Highlight Repaints”。
Select Widget Mode点击 “Select Widget Mode” 图标,可以在手机上查看当前页面的布局框架与容器类型。
通过“Select Widget Mode”我们可以快速查看陌生页面的布局实现方式。
Select Widget Mode模式下,也可以在app里点击相应的布局控件查看
Highlight Repaints
点击 “Highlight Repaints” 图标,它会 为所有 RenderBox 绘制一层外框,并在它们重绘时会改变颜色。
这样做帮你找到 App 中频繁重绘导致性能消耗过大的部分。
例如:一个小动画可能会导致整个页面重绘,这个时候使用 RepaintBoundary Widget 包裹它,可以将重绘范围缩小至本身所占用的区域,这样就可以减少绘制消耗。
2、Performance Overlay(性能图层)
在完成了应用启动之后,接下来我们就可以利用 Flutter 提供的渲染问题分析工具,即性能图层(Performance Overlay),来分析渲染问题了。
我们可以通过以下方式开启性能图层
性能图层会在当前应用的最上层,以 Flutter 引擎自绘的方式展示 GPU 与 UI 线程的执行图表,而其中每一张图表都代表当前线程最近 300 帧的表现,如果 UI 产生了卡顿,这些图表可以帮助我们分析并找到原因。 下图演示了性能图层的展现样式。其中,GPU 线程的性能情况在上面,UI 线程的情况显示在下面,蓝色垂直的线条表示已执行的正常帧,绿色的线条代表的是当前帧:
如果有一帧处理时间过长,就会导致界面卡顿,图表中就会展示出一个红色竖条。下图演示了应用出现渲染和绘制耗时的情况下,性能图层的展示样式:
如果红色竖条出现在 GPU 线程图表,意味着渲染的图形太复杂,导致无法快速渲染;而如果是出现在了 UI 线程图表,则表示 Dart 代码消耗了大量资源,需要优化代码执行时间。
3、CPU Profiler(UI 线程问题定位)
在视图构建时,在 build 方法中使用了一些复杂的运算,或是在主 Isolate 中进行了同步的 I/O 操作。 我们可以使用 CPU Profiler 进行检测:
你需要手动点击 “Record” 按钮去主动触发,在完成信息的抽样采集后,点击 “Stop” 按钮结束录制。这时,你就可以得到在这期间应用的执行情况了。
其中:
x 轴:表示单位时间,一个函数在 x 轴占据的宽度越宽,就表示它被采样到的次数越多,即执行时间越长。
y 轴:表示调用栈,其每一层都是一个函数。调用栈越深,火焰就越高,底部就是正在执行的函数,上方都是它的父函数。
通过上述CPU帧图我们可以大概分析出哪些方法存在耗时操作,针对性的进行优化
一般的耗时问题,我们通常可以 使用 Isolate(或 compute)将这些耗时的操作挪到并发主 Isolate 之外去完成。
例如:复杂JSON解析子线程化
Flutter的isolate默认是单线程模型,而所有的UI操作又都是在UI线程进行的,想应用多线程的并发优势需新开isolate 或compute。无论如何await,scheduleTask 都只是延后任务的调用时机,仍然会占用“UI线程”, 所以在大Json解析或大量的channel调用时,一定要观测对UI线程的消耗情况。
二、Flutter布局优化
Flutter 使用了声明式的 UI 编写方式,而不是 Android 和 iOS 中的命令式编写方式。
声明式:简单的说,你只需要告诉计算机,你要得到什么样的结果,计算机则会完成你想要的结果,声明式更注重结果。
命令式:用详细的命令机器怎么去处理一件事情以达到你想要的结果,命令式更注重执行过程。
flutter声明式的布局方式通过三棵树去构建布局,如图:
Widget Tree: 控件的配置信息,不涉及渲染,更新代价极低。
Element Tree : Widget树和RenderObject树之间的粘合剂,负责将Widget树的变更以最低的代价映射到RenderObject树上。
RenderObject Tree : 真正的UI渲染树,负责渲染UI,更新代价极大。
1、常规优化
常规优化即针对 build() 进行优化,build() 方法中的性能问题一般有两种:耗时操作和 Widget 层叠。
1)、在 build() 方法中执行了耗时操作
我们应该尽量避免在 build() 中执行耗时操作,因为 build() 会被频繁地调用,尤其是当 Widget 重建的时候。 此外,我们不要在代码中进行阻塞式操作,可以将一般耗时操作等通过 Future 来转换成异步方式来完成。 对于 CPU 计算频繁的操作,例如图片压缩,可以使用 isolate 来充分利用多核心 CPU。
2)、build() 方法中堆叠了大量的 Widget
这将会导致三个问题:
1、代码可读性差:画界面时需要一个 Widget 嵌套一个 Widget,但如果 Widget 嵌套太深,就会导致代码的可读性变差,也不利于后期的维护和扩展。
2、复用难:由于所有的代码都在一个 build(),会导致无法将公共的 UI 代码复用到其它的页面或模块。
3、影响性能:我们在 State 上调用 setState() 时,所有 build() 中的 Widget 都将被重建,因此 build() 中返回的 Widget 树越大,那么需要重建的 Widget 就越多,也就会对性能越不利。
所以,你需要 控制 build 方法耗时,将 Widget 拆小,避免直接返回一个巨大的 Widget,这样 Widget 会享有更细粒度的重建和复用。
3)、尽可能地使用 const 构造器
当构建你自己的 Widget 或者使用 Flutter 的 Widget 时,这将会帮助 Flutter 仅仅去 rebuild 那些应当被更新的 Widget。 因此,你应该尽量多用 const 组件,这样即使父组件更新了,子组件也不会重新进行 rebuild 操作。特别是针对一些长期不修改的组件,例如通用报错组件和通用 loading 组件等。
4)、列表优化
尽量避免使用 ListView默认构造方法
不管列表内容是否可见,会导致列表中所有的数据都会被一次性绘制出来
建议使用 ListView 和 GridView 的 builder 方法
它们只会绘制可见的列表内容,类似于 Android 的 RecyclerView。
其实,本质上,就是对列表采用了懒加载而不是直接一次性创建所有的子 Widget,这样视图的初始化时间就减少了。
2、深入光栅化优化
优化光栅线程
屏幕显示器一般以60Hz的固定频率刷新,每一帧图像绘制完成后,会继续绘制下一帧,这时显示器就会发出一个Vsync信号,按60Hz计算,屏幕每秒会发出60次这样的信号。CPU计算好显示内容提交给GPU,GPU渲染好传递给显示器显示。 Flutter遵循了这种模式,渲染流程如图:
flutter通过native获取屏幕刷新信号通过engine层传递给flutter framework
所有的 Flutter 应用至少都会运行在两个并行的线程上:UI 线程和 Raster 线程。
UI 线程
构建 Widgets 和运行应用逻辑的地方。
Raster 线程
用来光栅化应用。它从 UI 线程获取指令将其转换成为GPU命令并发送到GPU。
我们通常可以使用Flutter DevTools-Performance 进行检测,步骤如下:
在 Performance Overlay 中,查看光栅线程和 UI 线程哪个负载过重。
在 Timeline Events 中,找到那些耗费时间最长的事件,例如常见的 SkCanvas::Flush,它负责解决所有待处理的 GPU 操作。
找到对应的代码区域,通过删除 Widgets 或方法的方式来看对性能的影响。
三、Flutter内存优化
1、const 实例化
const 对象只会创建一个编译时的常量值。在代码被加载进 Dart Vm 时,在编译时会存储在一个特殊的查询表里,仅仅只分配一次内存给当前实例。
我们可以使用 flutter_lints 库对我们的代码进行检测提示
2、检测消耗多余内存的图片
Flutter Inspector:点击 “Highlight Oversizeded Images”,它会识别出那些解码大小超过展示大小的图片,并且系统会将其倒置,这些你就能更容易在 App 页面中找到它。
通过下面两张图可以清晰的看出使用“Highlight Oversizeded Images”的检测效果
针对这些图片,你可以指定 cacheWidth 和 cacheHeight 为展示大小,这样可以让 flutter 引擎以指定大小解析图片,减少内存消耗。
3、针对 ListView item 中有 image 的情况来优化内存
ListView 不会销毁那些在屏幕可视范围之外的那些 item,如果 item 使用了高分辨率的图片,那么它将会消耗非常多的内存。
ListView 在默认情况下会在整个滑动/不滑动的过程中让子 Widget 保持活动状态,这一点是通过 AutomaticKeepAlive 来保证,在默认情况下,每个子 Widget 都会被这个 Widget 包裹,以使被包裹的子 Widget 保持活跃。 其次,如果用户向后滚动,则不会再次重新绘制子 Widget,这一点是通过 RepaintBoundaries 来保证,在默认情况下,每个子 Widget 都会被这个 Widget 包裹,它会让被包裹的子 Widget 仅仅绘制一次,以此获得更高的性能。 但,这样的问题在于,如果加载大量的图片,则会消耗大量的内存,最终可能使 App 崩溃。
通过将这两个选项置为 false 来禁用它们,这样不可见的子元素就会被自动处理和 GC。
4、多变图层与不变图层分离
在日常开发中,会经常遇到页面中大部分元素不变,某个元素实时变化。如Gif,动画。这时我们就需要RepaintBoundary,不过独立图层合成也是有消耗,这块需实测把握。
这会导致页面同一图层重新Paint。此时可以用RepaintBoundary包裹该多变的Gif组件,让其处在单独的图层,待最终再一块图层合成上屏。
5、降级CustomScrollView,ListView等预渲染区域为合理值
默认情况下,CustomScrollView除了渲染屏幕内的内容,还会渲染上下各250区域的组件内容,例如当前屏幕可显示4个组件,实际仍有上下共4个组件在显示状态,如果setState(),则会进行8个组件重绘。实际用户只看到4个,其实应该也只需渲染4个, 且上下滑动也会触发屏幕外的Widget创建销毁,造成滚动卡顿。高性能的手机可预渲染,在低端机降级该区域距离为0或较小值。
四、总结
Flutter为什么会卡顿、帧率低?总的来说均为以下2个原因:
UI线程慢了-->渲染指令出的慢
GPU线程慢了-->光栅化慢、图层合成慢、像素上屏慢
所以我们一般使用flutter布局尽量按照以下原则
Flutter优化基本原则:
尽量不要为 Widget 设置半透明效果,而是考虑用图片的形式代替,这样被遮挡的 Widget 部分区域就不需要绘制了;
控制 build 方法耗时,将 Widget 拆小,避免直接返回一个巨大的 Widget,这样 Widget 会享有更细粒度的重建和复用;
对列表采用懒加载而不是直接一次性创建所有的子 Widget,这样视图的初始化时间就减少了。
五、其他
如果大家对flutter动态化感兴趣,我们也为大家准备了flutter动态化平台-Fair
欢迎大家使用 Fair,也欢迎大家为我们点亮star
作者:58技术
链接:https://juejin.cn/post/7145730792948252686
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Android消息机制中Message常用的几种监控方式
本篇文章主要是讲解Android消息机制中
Message
执行的几种监控方式:
Printer
监听Message
执行的起始时机
Observer
监听Message
执行的起始时机并将Message
作为参数传入
dump
方式打印消息队列中Message
快照
上面几种方式各有其优缺点及适用场景,下面我们一一进行分析(其中,Android SDK32中Looper
的源码发生了一些变化,不过不影响阅读)。
Printer
方式
对应Looper
源码中的:
我们直接深入到Looper
的核心方法loopOnce()
(基于SDK32的源码)进行分析:
private static boolean loopOnce(final Looper me, final long ident, final int thresholdOverride) {
Message msg = me.mQueue.next(); // might block
...
final Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " "
+ msg.callback + ": " + msg.what);
}
long origWorkSource = ThreadLocalWorkSource.setUid(msg.workSourceUid);
try {
msg.target.dispatchMessage(msg);
...
}
...
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
...
msg.recycleUnchecked();
return true;
}
其中msg.target.dispatchMessage()
就是我们消息分发执行的地方,而在这个执行前后都会调用Printer.println()
方法。
所以如果我们能够将这个Printer
对象替换成我们自定义的,不就可以监听Message
执行和结束的时机,所幸,Looper
也确实提供了一个方法setMessageLogging()
支持外部自定义Printer
传入:
public void setMessageLogging(@Nullable Printer printer) {
mLogging = printer;
}
这个有什么用呢,比如可以用来监听耗时的Message
,从而定位到业务代码中卡顿的代码位置进行优化,ANRWatchDog
据我所知就使用了这样的原理。
Observer
方式
这个定位到Looper
源码中就是:
可以看到这个接口提供的方法参数更加丰富,我们看下它在源码中的调用位置(精简后的代码如下):
private static boolean loopOnce(final Looper me, final long ident, final int thresholdOverride) {
Message msg = me.mQueue.next(); // might block
final Observer observer = sObserver;
Object token = null;
if (observer != null) {
token = observer.messageDispatchStarting();
}
try {
msg.target.dispatchMessage(msg);
if (observer != null) {
observer.messageDispatched(token, msg);
}
} catch (Exception exception) {
if (observer != null) {
observer.dispatchingThrewException(token, msg, exception);
}
throw exception;
}
}
和上面的Printer
调用有点相似,也是在消息执行前、消息执行后
调用,其中执行后分为两种:
正常执行后调用
messageDispatched()
;
异常执行后调用
dispatchingThrewException()
;
下面我们简单的介绍Observer
这三个接口方法:
messageDispatchStarting()
在Message
执行之前进行调用,并且可以返回一个标识
来标识这条Message
消息,这样当消息正常执行结束后,调用messageDispatched()
方法传入这个标识和当前分发的Message
,我们就可以建立这个标识和Message
之间的映射关系;出现异常的时候就会调用dispatchingThrewException()
方法,除了传入标识和分发的Message
外,还会传入捕捉到的异常。
不过很遗憾的是,Observer
是个被@Hide
标记的,不允许开发者进行调用,如果大家真要使用,可以参考这篇文章:监控Android Looper Message调度的另一种姿势。
dump
方式
这个可以打印当前消息队列中每条消息的快照信息,可以根据需要进行调用:
Looper.dump()
:
public void dump(@NonNull Printer pw, @NonNull String prefix) {
pw.println(prefix + toString());
mQueue.dump(pw, prefix + " ", null);
}
MessageQueue.dump()
void dump(Printer pw, String prefix, Handler h) {
synchronized (this) {
long now = SystemClock.uptimeMillis();
int n = 0;
for (Message msg = mMessages; msg != null; msg = msg.next) {
if (h == null || h == msg.target) {
pw.println(prefix + "Message " + n + ": " + msg.toString(now));
}
n++;
}
pw.println(prefix + "(Total messages: " + n + ", polling=" + isPollingLocked()
+ ", quitting=" + mQuitting + ")");
}
}
很直观的可以看到,当调用dump()
方法时会传入一个Printer
对象实例,就会遍历消息队列mMessages
,通过传入的Printer
打印每条消息的内容。
其中Message
重写了toString()
方法:
大家可以根据需要自行使用。
作者:长安皈故里
链接:https://juejin.cn/post/7150992884844462087
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »
【Flutter 异步编程 - 叁】 | 初步认识 Stream 类的使用
一、分析 Stream 对象
要了解一个事物,最好去思考它存在的 价值
。当你可以意识到某个事物的作用,缺少它会有什么弊端,自然会有兴趣去了解它。而不是稀里糊涂的看别人怎么用,自己死记硬背 API
有哪些,分别表示什么意思。一味的堆砌知识点,这样无论学什么都是流于表面,不得要领。
1. Stream 存在的必要性
可能很多朋友都没有在开发中使用过 Stream
对象,知道它挺重要,但又不知道他的具体的用途。有种只可远观,不可亵玩的距离感。Stream
可以弥补 Future
的短板,它对于异步来说是一块很重要的版块。
一个 Future
对象诞生的那一刻,无论成败,它最终注定只有一个结果。就像一个普通的网络接口,一次请求只会有一个响应结果。应用开发在绝大多数场景是一个 因
,对应一个 果
,所以和 Future
打交道比较多。
但有些场景,任务无法一次完成,对于 一次
请求,会有 若干次
响应。比如现实生活中,你追更一部小说,在你订阅后,作者每次新时,都会通知你。在这个场景下,小说完结代表任务结束,期间会触发多次响应通知,这是 Future
无法处理的。
另外,事件通知的时间不确定的,作者创作的过程也是非常耗时的,所以机体没有必要处于同步等待
的阻塞状态。像这种 异步事件序列
被形象的称之为 Stream 流
。
在人类科学中,一件重要事物的存在,必然有其发挥效用的场所,在这片领域之下,它是所向披靡的王。在接触新知识、新概念时,感知这片领域非常重要,一个工具只有在合适的场景下,才能发挥最大的效力。
2.从读取文件认识 Stream 的使用
File
对象可以通过 readAsString
异步方法读取文件内容,返回 Future<String>
类型对象。而 Future
异步任务只有一次响应机会,通过 then
回调,所以该方法会将文件中的 所有字符
读取出来。
---->[File#readAsString]---
Future<String> readAsString({Encoding encoding = utf8});
但有些场景中没有必要
或 不能
全部读取。比如,想要在一个大文件中寻找一些字符,找到后就 停止读取
;想要在读取文件时 显示
读取进度。这时,只能响应一次事件的 Future
就爱莫能助了,而这正是 Stream
大显身手的领域。在 File
类中有 openRead
方法返回 Stream
对象,我们先通过这个方法了解一下 Stream
的使用方式。
Stream<List<int>> openRead([int? start, int? end]);
现在的场景和上面 追更小说
是很相似的:
小说作者
无需一次性向读者
提供所有的章节;小说是一章章
进行更新的,每次更新章节,都需要通知读者
进行阅读。操作系统
不用一次性读取全部文件内容,返回给请求的机体
;文件是一块块
进行读取的,每块文件读取完,需要通知机体
进行处理。
在对 Stream
的理解中,需要认清两个角色: 发布者
和 订阅者
。其中发布者是真正处理任务的机体,是结果的生产者,比如 作者
、操作系统
、服务器
等,它们有 发送通知
的义务。订阅者是发送请求的机体,对于异步任务,其本身并不参与到执行过程中,可以监听通知来获取需要的结果数据。
代码处理中 Stream
对象使用 listen
方法 监听通知
,该方法的第一入参是回调函数,每次通知时都会被触发。回调函数的参数类型是 Stream
的泛型,表示此次通知时携带的结果数据。
StreamSubscription<T> listen(void onData(T event)?,
{Function? onError, void onDone()?, bool? cancelOnError});
如下是通过 Stream
事件读取文件,显示读取进度的处理逻辑。当 openRead
任务分发之后,操作系统会一块一块地对文件进行读取,每读一块会发送通知。Dart
代码中通过 _onData
函数进行监听,回调的 bytes
就是读取的字节数组结果。
在 _onData
函数中根据每次回调的字节数,就可以很轻松地计算出读取的进度。 onDone
指定的函数,会在任务完成时被触发,任务完成也就表示不会再有事件通知了。
void readFile() async {
File file = File(path.join(Directory.current.path, "assets", "Jane Eyre.txt"));
print("开始读取 Jane Eyre.txt ");
fileLength = await file.length();
Stream<List<int>> stream = file.openRead();
stream.listen(_onData,onDone: _onDone);
}
void _onData(List<int> bytes) {
counter += bytes.length;
double progress = counter * 100 / fileLength;
DateTime time = DateTime.now();
String timeStr = "[${time.hour}:${time.minute}:${time.second}:${time.millisecond}]";
print(timeStr + "=" * (progress ~/ 2) + '[${progress.toStringAsFixed(2)}%]');
}
void _onDone() {
print("读取 Jane Eyre.txt 结束");
}
3.初步认识 StreamSubscription
Stream#listen
方法监听后,会返回一个 StreamSubscription
对象,表示此次对流的订阅。
StreamSubscription<T> listen(void onData(T event)?,
{Function? onError, void onDone()?, bool? cancelOnError});
通过这个订阅对象,可以暂停 pause
或恢复 resume
对流的监听,以及通过 cancel
取消对流的监听。
---->[StreamSubscription]----
void pause([Future<void>? resumeSignal]);
void resume();
Future<void> cancel();
比如下面当进度大于 50
时,取消对流的订阅:通过打印日志可以看出 54.99%
时,订阅取消,流也随之停止,可以注意一个细节。此时 onDone
回调并未触发,表示当 Stream
任务被取消订阅时,不能算作完成。
late StreamSubscription<List<int>> subscription;
void readFile() async {
File file = File(path.join(Directory.current.path, "assets", "Jane Eyre.txt"));
print("开始读取 Jane Eyre.txt ");
fileLength = await file.length();
Stream<List<int>> stream = file.openRead();
// listen 方法返回 StreamSubscription 对象
subscription = stream.listen(_onData,onDone: _onDone);
}
void _onData(List<int> bytes) async{
counter += bytes.length;
double progress = counter * 100 / fileLength;
DateTime time = DateTime.now();
String timeStr = "[${time.hour}:${time.minute}:${time.second}:${time.millisecond}]";
print(timeStr + "=" * (progress ~/ 2) + '[${progress.toStringAsFixed(2)}%]');
if(progress > 50){
subscription.cancel(); // 取消订阅
}
}
二、结合应用理解 Stream 的使用
单看 Dart
代码在控制台打印,实在有些不过瘾。下面通过一个有趣的小例子,介绍 Stream
在 Flutter
项目中的使用。这样可以更形象地认识 Stream
的用途,便于进一步理解。
1. 场景分析
现实生活中如果细心观察,会发现很多 Stream
概念的身影。比如在银行办理业务时,客户可以看作 Stream
中的一个元素,广播依次播报牌号,业务员需要对某个元素进行处理。在餐馆中,每桌的客人可以看作 Stream
中的一个元素,客人下单完成,厨师根据请求准备饭菜进行处理。这里,通过模拟 红绿灯
的状态变化,来说明 Stream
的使用。
可以想象,在一个时间轴上,信号灯的变化是一个连续不断的事件。我们可以将每次的变化视为 Stream
中的一个元素,信号灯每秒的状态信息都会不同。也就是说,这个 Stream
每秒会产出一个状态,要在应用中模拟红绿灯,只需要监听每次的通知,更新界面显示即可。
这里将信号灯的状态信息通过 SignalState
类来封装,成员变量有当前秒数 counter
和信号灯类型 type
。 其中信号灯类型通过 SignalType
枚举表示,有如下三种类型:
const int _kAllowMaxCount = 10;
const int _kWaitMaxCount = 3;
const int _kDenialMaxCount = 10;
class SignalState {
final int counter;
final SignalType type;
SignalState({
required this.counter,
required this.type,
});
}
enum SignalType {
allow, // 允许 - 绿灯
denial, // 拒绝 - 红灯
wait, // 等待 - 黄灯
}
2. 信号灯组件的构建
如下所示,信号灯由三个 Lamp
组件和数字构成。三个灯分别表示 红、黄、绿
,某一时刻只会量一盏,不亮的使用灰色示意。三个灯水平排列,有一个黑色背景装饰,和文字呈上下结构。
先看灯 Lamp
组件的构建:逻辑非常简单,使用 Container
组件显示圆形,构造时可指定颜色值,为 null
时显示灰色。
class Lamp extends StatelessWidget {
final Color? color;
const Lamp({Key? key, required this.color}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: color ?? Colors.grey.withOpacity(0.8),
shape: BoxShape.circle,
),
);
}
}
如下是 SignalLamp
组件的展示效果,其依赖于 SignalState
对象进行显示。根据 SignalType
确定显示的颜色和需要点亮的灯,状态中的 counter
成员用于展示数字。
class SignalLamp extends StatelessWidget {
final SignalState state;
const SignalLamp({Key? key, required this.state}) : super(key: key);
Color get activeColor {
switch (state.type) {
case SignalType.allow:
return Colors.green;
case SignalType.denial:
return Colors.red;
case SignalType.wait:
return Colors.amber;
}
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10),
decoration: BoxDecoration(
color: Colors.black, borderRadius: BorderRadius.circular(30),),
child: Wrap(
alignment: WrapAlignment.center,
crossAxisAlignment: WrapCrossAlignment.center,
spacing: 15,
children: [
Lamp(color: state.type == SignalType.denial ? activeColor : null),
Lamp(color: state.type == SignalType.wait ? activeColor : null),
Lamp(color: state.type == SignalType.allow ? activeColor : null),
],
),
),
Text(
state.counter.toString(),
style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 50, color: activeColor,
),
)
],
);
}
}
4. Stream 事件的添加与监听
这样,指定不同的 SignalState
就会呈现相应的效果,如下是黄灯的 2 s
:
SignalLamp(
state: SignalState(counter: 2, type: SignalType.wait),
)
在使用 Stream
触发更新之前,先说一下思路。Stream
可以监听一系列事件的触发,每次监听会获取新的信号状态,根据新状态渲染界面即可。如下在 SignalState
中定义 next
方法,便于产出下一状态。逻辑很简单,如果数值大于一,类型不变,数值减一,比如 红灯 6
的下一状态是 红灯 5
; 如果数值等于一,会进入下一类型的最大数值,比如 红灯 1
的下一状态是 黄灯 3
。
---->[SignalState]----
SignalState next() {
if (counter > 1) {
return SignalState(type: type, counter: counter - 1);
} else {
switch (type) {
case SignalType.allow:
return SignalState(
type: SignalType.denial, counter: _kDenialMaxCount);
case SignalType.denial:
return SignalState(type: SignalType.wait, counter: _kWaitMaxCount);
case SignalType.wait:
return SignalState(type: SignalType.allow, counter: _kAllowMaxCount);
}
}
}
把每个事件通知看做元素,Stream
应用处理事件序列,只不过序列中的元素在此刻是未知的,何时触发也是不定的。Stream
基于 发布-订阅
的思想通过监听来处理这些事件。 其中两个非常重要的角色: 发布者
是元素的生产者,订阅者
是元素的消费者。
在引擎中的 async
包中封装了 StreamController
类用于控制元素的添加操作,同时提供 Stream
对象用于监听。代码处理如下,tag1
处,监听 streamController
的 stream
对象。事件到来时触发 emit
方法 ( 方法名任意
),在 emit
中会回调出 SignalState
对象,根据这个新状态更新界面即可。然后延迟 1s
继续添加下一状态。
---->[_MyHomePageState]----
final StreamController<SignalState> streamController = StreamController();
SignalState _signalState = SignalState(counter: 10, type: SignalType.denial);
@override
void initState() {
super.initState();
streamController.stream.listen(emit); // tag1
streamController.add(_signalState);
}
@override
void dispose() {
super.dispose();
streamController.close();
}
void emit(SignalState state) async {
_signalState = state;
setState(() {});
await Future.delayed(const Duration(seconds: 1));
streamController.add(state.next());
}
这样 streamController
添加元素,作为 发布者
;添加的元素可以通过 StreamController
的 stream
成员进行监听。
5. Stream 的控制与异常监听
在前面介绍过 Stream#listen
方法会返回一个 StreamSubscription
的订阅对象,通过该对象可以暂停、恢复、取消对流的监听。如下所示,通过点击按钮执行 _toggle
方法,可以达到 暂停/恢复
切换的效果:
---->[_MyHomePageState]----
late StreamSubscription<SignalState> _subscription;
@override
void initState() {
super.initState();
_subscription = streamController.stream.listen(emit);
streamController.add(_signalState);
}
void _toggle() {
if(_subscription.isPaused){
_subscription.resume();
}else{
_subscription.pause();
}
setState(() {});
}
另外,StreamController
在构造时可以传入四个函数来监听流的状态:
final StreamController<SignalState> streamController = StreamController(
onListen: ()=> print("=====onListen====="),
onPause: ()=> print("=====onPause====="),
onResume: ()=> print("=====onResume====="),
onCancel: ()=> print("=====onCancel====="),
);
onListen
会在 stream
成员被监听时触发一次;onPause
、onResume
、onCancel
分别对应订阅者的 pause
、 resume
、cancel
方法。如下是点击暂停和恢复的日志信息:
在 Stream#listen
方法中还有另外两个可选参数用于异常的处理。 onError
是错误的回调函数,cancelOnError
标识用于控制触发异常时,是否取消 Stream
。
StreamSubscription<T> listen(void onData(T event)?,
{Function? onError, void onDone()?, bool? cancelOnError});
如下所示,在 emit
中故意在 红 7
时通过 addError
添加一个异常元素。这里界面简单显示错误信息,在 3 s
后异常被修复,继续添加新元素。
void emit(SignalState state) async {
_signalState = state;
setState(() {});
await Future.delayed(const Duration(seconds: 1));
SignalState nextState = state.next();
if (nextState.counter == 7 && nextState.type == SignalType.denial) {
streamController.addError(Exception('Error Signal State'));
} else {
streamController.add(nextState);
}
}
在 listen
方法中使用 onError
监听异常事件,进行处理:其中逻辑是渲染错误界面,三秒后修复异常,继续产出下一状态:
_subscription = streamController.stream.listen(
emit,
onError: (err) async {
print(err);
renderError();
await Future.delayed(const Duration(seconds: 3));
fixError();
emit(_signalState.next());
},
cancelOnError: false,
);
关于异常的处理,这里简单地提供 hasError
标识进行构建逻辑的区分:
bool hasError = false;
void renderError(){
hasError = true;
setState(() {});
}
void fixError(){
hasError = false;
}
最后说一下 listen
中 cancelOnError
的作用,它默认是 false
。如果 cancelOnError = true
,在监听到异常之后,就会取消监听 stream
,也就是说之后控制器添加的元素就会监听了。这样异常时 StreamController
会触发 onCancel
回调:
三、异步生成器函数与 Stream
前面介绍了通过 StreamController
获取 Stream
进行处理的方式,下面再来看另一种获取 Stream
的方式 - 异步生成器函数
。
1. 思考 Stream 与 Iterable
通过前面对 Stream
的认识,我们知道它是在 时间线
上可拥有若干个可监听的事件元素。而 Iterable
也可以拥有多个元素,两者之间是有很大差距的。Iterable
在 时间
和 空间
上都对元素保持持有关系;而 Stream
只是在时间上监听若干元素的到来,并不在任意时刻都持有元素,更不会在空间上保持持有关系。
对于一个 Type
类型的数据,在异步任务中,Stream<T>
是 Future<T>
就是多值和单值的区别,它们的结果都不能在 当前时刻
得到,只能通过监听在 未来
得到值。 与之相对的就是 Iterable<Type>
和 Type
,它们代表此时此刻,实实在在的对象,可以随时使用。
单值 | 多值 | |
---|---|---|
同步 | Type | Iterable<Type> |
异步 | Future<Type> | Stream<Type> |
2. 通过异步生成器函数获取 Stream 对象
Future
对象可以通过 async/awiat
关键字,简化书写,更方便的获取异步任务结果。 对于 Stream
也有类似的 async*/yield
关键字。 如下所示, async*
修饰的方法需要返回一个 Stream
对象。
在方法体中通过 yield
关键字 产出
泛型结果对象,如下是对 信号状态流
元素产生出的逻辑:遍历 count
次,每隔 1 s
产出一个状态。
class SignalStream{
SignalState _signalState = SignalState(counter: 10, type: SignalType.denial);
Stream<SignalState> createStream({int count = 100}) async*{
for(int i = 0 ; i < count; i++){
await Future.delayed(const Duration(seconds: 1));
_signalState = _signalState.next();
yield _signalState;
}
}
}
这样,在 _MyHomePageState
中通过 signalStream.createStream()
就可以创建一个有 100
个元素的流,进行监听。每次接收到新状态时,更新界面,也可以达到目的:
---->[_MyHomePageState]---
final SignalStream signalStream = SignalStream();
_subscription = signalStream.createStream().listen(
emit,
);
void emit(SignalState state) async {
_signalState = state;
setState(() {});
}
到这里,关于 Stream
的初步认识就结束了,当然 Stream
的知识还有很多,在后面会陆续介绍。通过本文,你只需要明白 Stream
是什么,通过它我们能干什么就行了。下一篇我们将分析一下 FutureBuilder
和 StreamBuilder
组件的使用和源码实现。它们是 Flutter
对异步对象的封装组件,通过对它们的认识,也能加深我们对 Future
和 Stream
的立即。 那本文就到这里,谢谢观看 ~
作者:张风捷特烈
链接:https://juejin.cn/post/7147881475688366093
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »
Android 系统 Bar 沉浸式完美兼容方案(下)
完整代码
@file:Suppress("DEPRECATION")
package com.bytedance.heycan.systembar.activity
import android.app.Activity
import android.graphics.Color
import android.os.Build
import android.util.Size
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.FrameLayout
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.bytedance.heycan.systembar.R
/**
* Created by dengchunguo on 2021/4/25
*/
fun Activity.setLightStatusBar(isLightingColor: Boolean) {
val window = this.window
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (isLightingColor) {
window.decorView.systemUiVisibility =
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
} else {
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
}
}
}
fun Activity.setLightNavigationBar(isLightingColor: Boolean) {
val window = this.window
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && isLightingColor) {
window.decorView.systemUiVisibility =
window.decorView.systemUiVisibility or if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR else 0
}
}
/**
* 必须在Activity的onCreate时调用
*/
fun Activity.immersiveStatusBar() {
val view = (window.decorView as ViewGroup).getChildAt(0)
view.addOnLayoutChangeListener { v, _, _, _, _, _, _, _, _ ->
val lp = view.layoutParams as FrameLayout.LayoutParams
if (lp.topMargin > 0) {
lp.topMargin = 0
v.layoutParams = lp
}
if (view.paddingTop > 0) {
view.setPadding(0, 0, 0, view.paddingBottom)
val content = findViewById<View>(android.R.id.content)
content.requestLayout()
}
}
val content = findViewById<View>(android.R.id.content)
content.setPadding(0, 0, 0, content.paddingBottom)
window.decorView.findViewById(R.id.status_bar_view) ?: View(window.context).apply {
id = R.id.status_bar_view
val params = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, statusHeight)
params.gravity = Gravity.TOP
layoutParams = params
(window.decorView as ViewGroup).addView(this)
(window.decorView as ViewGroup).setOnHierarchyChangeListener(object : ViewGroup.OnHierarchyChangeListener {
override fun onChildViewAdded(parent: View?, child: View?) {
if (child?.id == android.R.id.statusBarBackground) {
child.scaleX = 0f
}
}
override fun onChildViewRemoved(parent: View?, child: View?) {
}
})
}
setStatusBarColor(Color.TRANSPARENT)
}
/**
* 必须在Activity的onCreate时调用
*/
fun Activity.immersiveNavigationBar(callback: (() -> Unit)? = null) {
val view = (window.decorView as ViewGroup).getChildAt(0)
view.addOnLayoutChangeListener { v, _, _, _, _, _, _, _, _ ->
val lp = view.layoutParams as FrameLayout.LayoutParams
if (lp.bottomMargin > 0) {
lp.bottomMargin = 0
v.layoutParams = lp
}
if (view.paddingBottom > 0) {
view.setPadding(0, view.paddingTop, 0, 0)
val content = findViewById<View>(android.R.id.content)
content.requestLayout()
}
}
val content = findViewById<View>(android.R.id.content)
content.setPadding(0, content.paddingTop, 0, -1)
val heightLiveData = MutableLiveData<Int>()
heightLiveData.value = 0
window.decorView.setTag(R.id.navigation_height_live_data, heightLiveData)
callback?.invoke()
window.decorView.findViewById(R.id.navigation_bar_view) ?: View(window.context).apply {
id = R.id.navigation_bar_view
val params = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, heightLiveData.value ?: 0)
params.gravity = Gravity.BOTTOM
layoutParams = params
(window.decorView as ViewGroup).addView(this)
if (this@immersiveNavigationBar is FragmentActivity) {
heightLiveData.observe(this@immersiveNavigationBar) {
val lp = layoutParams
lp.height = heightLiveData.value ?: 0
layoutParams = lp
}
}
(window.decorView as ViewGroup).setOnHierarchyChangeListener(object : ViewGroup.OnHierarchyChangeListener {
override fun onChildViewAdded(parent: View?, child: View?) {
if (child?.id == android.R.id.navigationBarBackground) {
child.scaleX = 0f
bringToFront()
child.addOnLayoutChangeListener { _, _, top, _, bottom, _, _, _, _ ->
heightLiveData.value = bottom - top
}
} else if (child?.id == android.R.id.statusBarBackground) {
child.scaleX = 0f
}
}
override fun onChildViewRemoved(parent: View?, child: View?) {
}
})
}
setNavigationBarColor(Color.TRANSPARENT)
}
/**
* 当设置了immersiveStatusBar时,如需使用状态栏,可调佣该函数
*/
fun Activity.fitStatusBar(fit: Boolean) {
val content = findViewById<View>(android.R.id.content)
if (fit) {
content.setPadding(0, statusHeight, 0, content.paddingBottom)
} else {
content.setPadding(0, 0, 0, content.paddingBottom)
}
}
fun Activity.fitNavigationBar(fit: Boolean) {
val content = findViewById<View>(android.R.id.content)
if (fit) {
content.setPadding(0, content.paddingTop, 0, navigationBarHeightLiveData.value ?: 0)
} else {
content.setPadding(0, content.paddingTop, 0, -1)
}
if (this is FragmentActivity) {
navigationBarHeightLiveData.observe(this) {
if (content.paddingBottom != -1) {
content.setPadding(0, content.paddingTop, 0, it)
}
}
}
}
val Activity.isImmersiveNavigationBar: Boolean
get() = window.attributes.flags and WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION != 0
val Activity.statusHeight: Int
get() {
val resourceId =
resources.getIdentifier("status_bar_height", "dimen", "android")
if (resourceId > 0) {
return resources.getDimensionPixelSize(resourceId)
}
return 0
}
val Activity.navigationHeight: Int
get() {
return navigationBarHeightLiveData.value ?: 0
}
val Activity.screenSize: Size
get() {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Size(windowManager.currentWindowMetrics.bounds.width(), windowManager.currentWindowMetrics.bounds.height())
} else {
Size(windowManager.defaultDisplay.width, windowManager.defaultDisplay.height)
}
}
fun Activity.setStatusBarColor(color: Int) {
val statusBarView = window.decorView.findViewById<View?>(R.id.status_bar_view)
if (color == 0 && Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
statusBarView?.setBackgroundColor(STATUS_BAR_MASK_COLOR)
} else {
statusBarView?.setBackgroundColor(color)
}
}
fun Activity.setNavigationBarColor(color: Int) {
val navigationBarView = window.decorView.findViewById<View?>(R.id.navigation_bar_view)
if (color == 0 && Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
navigationBarView?.setBackgroundColor(STATUS_BAR_MASK_COLOR)
} else {
navigationBarView?.setBackgroundColor(color)
}
}
@Suppress("UNCHECKED_CAST")
val Activity.navigationBarHeightLiveData: LiveData<Int>
get() {
var liveData = window.decorView.getTag(R.id.navigation_height_live_data) as? LiveData<Int>
if (liveData == null) {
liveData = MutableLiveData()
window.decorView.setTag(R.id.navigation_height_live_data, liveData)
}
return liveData
}
val Activity.screenWidth: Int get() = screenSize.width
val Activity.screenHeight: Int get() = screenSize.height
private const val STATUS_BAR_MASK_COLOR = 0x7F000000
扩展
对话框适配
有时候需要通过 Dialog 来显示一个提示对话框、loading 对话框等,当显示一个对话框时,即使设置了 activity 为深色状态栏和导航栏文字颜色,这时候状态栏和导航栏的文字颜色又变成白色,如下所示:
这是因为对 activity 设置的状态栏和导航栏颜色是作用 于 activity 的 window,而 dialog 和 activity 不是同一个 window,因此 dialog 也需要单独设置。
完整代码
@file:Suppress( DEPRECATION )
package com.bytedance.heycan.systembar.dialog
import android.app.Dialog
import android.os.Build
import android.view.View
import android.view.ViewGroup
/**
* Created by dengchunguo on 2021/4/25
*/
fun Dialog.setLightStatusBar(isLightingColor: Boolean) {
val window = this.window ?: return
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (isLightingColor) {
window.decorView.systemUiVisibility =
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
} else {
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
}
}
}
fun Dialog.setLightNavigationBar(isLightingColor: Boolean) {
val window = this.window ?: return
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && isLightingColor) {
window.decorView.systemUiVisibility =
window.decorView.systemUiVisibility or if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR else 0
}
}
fun Dialog.immersiveStatusBar() {
val window = this.window ?: return
(window.decorView as ViewGroup).setOnHierarchyChangeListener(object : ViewGroup.OnHierarchyChangeListener {
override fun onChildViewAdded(parent: View?, child: View?) {
if (child?.id == android.R.id.statusBarBackground) {
child.scaleX = 0f
}
}
override fun onChildViewRemoved(parent: View?, child: View?) {
}
})
}
fun Dialog.immersiveNavigationBar() {
val window = this.window ?: return
(window.decorView as ViewGroup).setOnHierarchyChangeListener(object : ViewGroup.OnHierarchyChangeListener {
override fun onChildViewAdded(parent: View?, child: View?) {
if (child?.id == android.R.id.navigationBarBackground) {
child.scaleX = 0f
} else if (child?.id == android.R.id.statusBarBackground) {
child.scaleX = 0f
}
}
override fun onChildViewRemoved(parent: View?, child: View?) {
}
})
}
效果如下:
快速使用
Activity 沉浸式
immersiveStatusBar() // 沉浸式状态栏
immersiveNavigationBar() // 沉浸式导航栏
setLightStatusBar(true) // 设置浅色状态栏背景(文字为深色)
setLightNavigationBar(true) // 设置浅色导航栏背景(文字为深色)
setStatusBarColor(color) // 设置状态栏背景色
setNavigationBarColor(color) // 设置导航栏背景色
navigationBarHeightLiveData.observe(this) {
// 监听导航栏高度变化
}
Dialog 沉浸式
val dialog = Dialog(this, R.style.Heycan_SampleDialog)
dialog.setContentView(R.layout.dialog_loading)
dialog.immersiveStatusBar()
dialog.immersiveNavigationBar()
dialog.setLightStatusBar(true)
dialog.setLightNavigationBar(true)
dialog.show()
Demo 效果
可实现与 iOS 类似的页面沉浸式导航条效果:
作者:字节跳动技术团队
来源:juejin.cn/post/7075578574362640421
Android 系统 Bar 沉浸式完美兼容方案(上)
引言
自 Android 5.0 版本,Android 带来了沉浸式系统 bar(状态栏和导航栏),Android 的视觉效果进一步提高,各大 app 厂商也在大多数场景上使用沉浸式效果。但由于 Android 碎片化比较严重,每个版本的系统 bar 效果可能会有所差异,导致开发者往往需要进行兼容适配。为了简化系统 bar 沉浸式的使用,以及统一机型、版本差异所造成的效果差异,本文将介绍系统 bar 的组成以及沉浸式适配方案。
背景
问题一:沉浸式下无法设置背景色
对于大于等于 Android 5.0 版本的系统,在 Activity 的 onCreate 时,通过给 window 设置属性:
window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)
即可开启沉浸式系统 bar,效果如下:
Android 5.0 沉浸式状态栏
Android 5.0 沉浸式导航栏
但是设置沉浸式之后,原来通过 window.statusBarColor
和 window.statusBarColor
设置的颜色也不可用,也就是说不支持自定义半透明系统 bar 的颜色。
问题二:无法全透明导航栏
系统默认的状态栏和导航栏都有一个半透明的蒙层,虽然不支持设置颜色,但通过设置以下代码,可让状态栏变为全透明:
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE)
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
window.statusBarColor = Color.TRANSPARENT
效果如下:
Android 10.0 沉浸式全透明状态栏
通过类似的方式尝试将导航栏设置为全透明:
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION)
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
window.navigationBarColor = Color.TRANSPARENT
但发现导航栏半透明背景依然无法去掉:
问题三:亮色系统 bar 版本差异
对于大于等于 Android 6.0 版本的系统,如果背景是浅色的,可通过设置状态栏和导航栏文字颜色为深色,也就是导航栏和状态栏为浅色(只有 Android 8.0 及以上才支持导航栏文字颜色修改):
window.decorView.systemUiVisibility =
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
window.decorView.systemUiVisibility =
window.decorView.systemUiVisibility or if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR else 0
效果如下:
Android 8.0 亮色状态栏
Android 8.0 亮色导航栏
但是在亮色系统 bar 基础上开启沉浸式后,在 8.0 至 9.0 系统中,导航栏深色导航 icon 不生效,而 10.0 以上版本能显示深色导航 icon:
Android 8.0 亮色沉浸式亮色导航栏
Android 10.0 亮色沉浸式亮色导航栏
问题分析
问题一:沉浸式下无法设置背景色
查看源码发现设置状态栏和导航栏背景颜色时,是不能为沉浸式的:
问题二:无法全透明导航栏
当设置导航栏为透明色(Color.TRANSPARENT
)时,导航栏会变成半透明,当设置其他颜色,则是正常的,例如设置颜色为 0x700F7FFF,显示效果如下:
Android 10.0 沉浸式导航栏
为什么会出现这个情况呢,通过调试进入源码,发现 activity 的 onApplyThemeResource
方法中有一个逻辑:
// Get the primary color and update the TaskDescription for this activity
TypedArray a = theme.obtainStyledAttributes(
com.android.internal.R.styleable.ActivityTaskDescription);
if (mTaskDescription.getPrimaryColor() == 0) {
int colorPrimary = a.getColor(
com.android.internal.R.styleable.ActivityTaskDescription_colorPrimary, 0);
if (colorPrimary != 0 && Color.alpha(colorPrimary) == 0xFF) {
mTaskDescription.setPrimaryColor(colorPrimary);
}
}
也就是说如果设置的导航栏颜色为 0(纯透明)时,将会为其修改为内置的颜色:ActivityTaskDescription_colorPrimary
,因此就会出现灰色蒙层效果。
问题三:亮色系统 bar 版本差异
通过查看源码发现,与设置状态栏和导航栏背景颜色类似,设置导航栏 icon 颜色也是不能为沉浸式:
解决沉浸式兼容性问题
对于问题二无法全透明导航栏,由上述问题分析中的代码可以看出,当且仅当设置的导航栏颜色为纯透明时(0),才会置换为半透明的蒙层。那么,我们可以将纯透明这种情况修改颜色为 0x01000000,这样也能达到接近纯透明的效果:
对于问题一,难以通过常规方式进行沉浸式下的系统 bar 背景颜色设置。而对于问题三,通过常规方式需要分别对各个版本进行适配,对于国内手机来说,适配难度更大。
为了解决兼容性问题,以及更好的管理状态栏和导航栏,我们是否能自己实现状态栏和导航栏的背景 View 呢?
通过 Layout Inspector 可以看出,导航栏和状态栏本质上也是一个 view:
在 activity 创建的时候,会创建两个 view(navigationBarBackground 和 statusBarBackground),将其加到 decorView 中,从而可以控制状态栏的颜色。那么,是否能把系统的这两个 view 隐藏起来,替换成自定义的 view 呢?
因此,为了提高兼容性,以及更好的管理状态栏和导航栏,我们可以将系统的 navigationBarBackground 和 statusBarBackground 隐藏起来,替换成自定义的 view,而不再通过 FLAG_TRANSLUCENT_STATUS
和 FLAG_TRANSLUCENT_NAVIGATION
来设置。
实现沉浸式状态栏
添加自定义的状态栏。通过创建一个 view ,让其高度等于状态栏的高度,并将其添加到 decorView 中:
View(window.context).apply {
id = R.id.status_bar_view
val params = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, statusHeight)
params.gravity = Gravity.TOP
layoutParams = params
(window.decorView as ViewGroup).addView(this)
}
隐藏系统的状态栏。由于 activity 在
onCreate
时,并没有创建状态栏的 view(statusBarBackground),因此无法直接将其隐藏。这里可以通过对 decorView 添加OnHierarchyChangeListener
监听来捕获到 statusBarBackground:
(window.decorView as ViewGroup).setOnHierarchyChangeListener(object : ViewGroup.OnHierarchyChangeListener {
override fun onChildViewAdded(parent: View?, child: View?) {
if (child?.id == android.R.id.statusBarBackground) {
child.scaleX = 0f
}
}
override fun onChildViewRemoved(parent: View?, child: View?) {
}
})
注意:这里将 child 的 scaleX
设为 0 即可将其隐藏起来,那么为什么不能设置 visibility
为 GONE
呢?这是因为后续在应用主题时(onApplyThemeResource
),系统会将 visibility
又重新设置为 VISIBLE
。
隐藏之后,半透明的状态栏不显示,但是顶部会出现空白:
通过 Layout Inspector 发现,decorView 的第一个元素(内容 view )会存在一个 padding:
因此,可以通过设置 paddingTop 为 0 将其去除:
val view = (window.decorView as ViewGroup).getChildAt(0)
view.addOnLayoutChangeListener { v, _, _, _, _, _, _, _, _ ->
if (view.paddingTop > 0) {
view.setPadding(0, 0, 0, view.paddingBottom)
val content = findViewById<View>(android.R.id.content)
content.requestLayout()
}
}
注意:这里需要监听 view 的 layout 变化,否则只有一开始设置则后面又被修改了。
实现沉浸式导航栏
导航栏的自定义与状态栏类似,不过会存在一些差异。先创建一个自定义 view 将其添加到 decorView 中,然后把原来系统的 navigationBarBackground 隐藏:
window.decorView.findViewById(R.id.navigation_bar_view) ?: View(window.context).apply {
id = R.id.navigation_bar_view
val resourceId = resources.getIdentifier( navigation_bar_height , dimen , android )
val navigationBarHeight = if (resourceId > 0) resources.getDimensionPixelSize(resourceId) else 0
val params = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, navigationBarHeight)
params.gravity = Gravity.BOTTOM
layoutParams = params
(window.decorView as ViewGroup).addView(this)
(window.decorView as ViewGroup).setOnHierarchyChangeListener(object : ViewGroup.OnHierarchyChangeListener {
override fun onChildViewAdded(parent: View?, child: View?) {
if (child?.id == android.R.id.navigationBarBackground) {
child.scaleX = 0f
} else if (child?.id == android.R.id.statusBarBackground) {
child.scaleX = 0f
}
}
override fun onChildViewRemoved(parent: View?, child: View?) {
}
})
}
注意:这里 onChildViewAdded
方法中,因为只能设置一次 OnHierarchyChangeListener
,需要同时考虑状态栏和导航栏。
通过这个方式,能将导航栏替换为自定义的 view ,但是存在一个问题,由于 navigationBarHeight 是固定的,如果用户切换了导航栏的样式,再回到 app 时,导航栏的高度不会重新调整。为了让导航栏看的清楚,设置其颜色为 0x7F00FF7F:
从图中可以看出,导航栏切换之后高度没有发生变化。为了解决这个问题,需要通过对 navigationBarBackground 设置 OnLayoutChangeListener
来监听导航栏高度的变化,并通过 liveData 关联到 view 中,代码实现如下:
val heightLiveData = MutableLiveData<Int>()
heightLiveData.value = 0
window.decorView.setTag(R.id.navigation_height_live_data, heightLiveData)
val navigationBarView = window.decorView.findViewById(R.id.navigation_bar_view) ?: View(window.context).apply {
id = R.id.navigation_bar_view
val params = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, heightLiveData.value ?: 0)
params.gravity = Gravity.BOTTOM
layoutParams = params
(window.decorView as ViewGroup).addView(this)
if (this@immersiveNavigationBar is FragmentActivity) {
heightLiveData.observe(this@immersiveNavigationBar) {
val lp = layoutParams
lp.height = heightLiveData.value ?: 0
layoutParams = lp
}
}
(window.decorView as ViewGroup).setOnHierarchyChangeListener(object : ViewGroup.OnHierarchyChangeListener {
override fun onChildViewAdded(parent: View?, child: View?) {
if (child?.id == android.R.id.navigationBarBackground) {
child.scaleX = 0f
child.addOnLayoutChangeListener { _, _, top, _, bottom, _, _, _, _ ->
heightLiveData.value = bottom - top
}
} else if (child?.id == android.R.id.statusBarBackground) {
child.scaleX = 0f
}
}
override fun onChildViewRemoved(parent: View?, child: View?) {
}
})
}
通过上面方式,可以解决切换导航栏样式后自定义的导航栏高度问题:
作者:字节跳动技术团队
来源:juejin.cn/post/7075578574362640421
接 Android系统Bar沉浸式完美兼容方案(下)
收起阅读 »2022 经历裁员之后的感受
序
第一次经历裁员(已经提前知道的情况下,毕竟信息都共享),说实话 老大之前找我 1V1 聊天
老大:
“你最近感觉怎么样 ”
我:
“没什么意思,问题和改进意见和具体的调研都做完了,但是不给排期做,我已经放弃了,听老大的安排 ”
老大:
“那你愿意去带别的项目么 ”
“聊天到这,我其实早就 已经对公司不抱什么希望了,如果报希望 我也不能这么说了。。。说实话 我对老大已经失望了,所以早在3个月之前 我就开始准备了 ”
1 我的工作经历
1.1 鞍山工作
我非计算机专业,自学的java 第一份工作找了很多家,鞍山的公司 基本都是 外派鞍钢 (因为鞍山就是一个鞍钢)
当时进鞍山公司 还是因为听话 哈哈,毕竟什么都不会。刚开始碰到的第一个女领导,所有问题 只允许问一次,第二次问就不回答了,让自己领悟。 那是一段黑暗时光,我们总共3个人一起来的,因为他 我们3个都打算跑,本来就不会,还自己领悟。。。 我打算走的时候 一个男领导 (这里叫A) 来总公司找人去他项目组 干活,很幸运 挑中了我。
我当时不会抽烟,但是和A的交集 因为实际情况,只能找抽烟时间 多问问 多学学。跟着试着抽烟 哈哈 很无奈,但是跟 A 确实学了很多东西,算是我的第一个领路人
“那为什么去沈阳呢? ”
鞍山只有几个公司,都是外派鞍钢,我想出去闯闯,我应该去一线城市,但是本人母亲岁数大,所以因为实际情况 我只能选择 沈阳这个离鞍山 比较近 ,相对有发展的城市。
1.2 沈阳工作
我从17年 一直工作到2021年的城市 我经历了几家公司,其中印象最深的是 杭州珍林 (沈阳研发中心) ,我在这 认识了皓哥 帅哥 青橘 等人,一直到现在 我都感觉 氛围和工作 都很适合
“如果要说 为什么感觉杭州珍林 好的原因? ”
皓哥 能根据 每个人的性格 和能力 ,给你分配 适当的活,帮你指定你的 职业规划。这是你在别的公司没有的,而且皓哥 是 真正的 有管理能力,而不是依靠技术能力,这里第一次让我知道了 什么是 真正的管理能力 因为之前 我一直以为 程序员的发展路线
1.2.1 程序员发展路线
1 初级程序员 (依靠别人带着干活)
2 可以独立完成任务
3 可以独立完成任务 并且可以优化 bug少,业务理解良好
4 带少数几个人 完成小模块、小项目
5 这里根据 技术、业务、管理 走不同的分支 这里就不细说了
皓哥让我感觉到除了上面的发展路径,可以单纯依靠 管理能力 来带着团队往前走 ,印象很深
“既然你在 杭州珍林 这么好 为什么要去北京呢? ”
说到这,说实话 我是没办法,我在沈阳买了房子,随着父母岁数大,我的压力加大,我睡觉的时候 会想到父母以后的养老问题,我不想因为自己的 没能力 导致父母 没有钱治病 (我妈说过 如果以后病重了 就不治了。我当时心里 咯噔一下,我现在都记得当时的情景) 当时我就怪自己的 没能力
当时从沈阳到北京 我当时工资变成了 3倍,当然有运气的成分,但是我还是有些底气的。
1 我从 16年毕业开始 就从指定月计划 ,到周计划,再到 日计划 把每天的时间都安排满。强迫自己学、实践 东西。
2 从2020年 我就开始准备了,因为我不是计算机专业 ,数据结构和算法 是我比较费劲的,我花了半年的时候 学了3遍,写代码 练习。(说到这 很有意思的是 有一家北京公司 面试,出了一个力扣的 难度为困难的题当 面试题,很不巧,我还真写过,直接掏出手机 一顿写 哈哈,面试官给20分钟, 我5分钟交活,给他讲,说明准备还是有用的)
1.3 北京开课吧 裁员
当时在做offer 选择的时候 我有多个offer 从业务/技术(消息中间件) 做选择 ,当时因为 没去过人数过千的公司 ,选择了开课吧 做业务
提前2个月知道裁员消息,集体欠薪,不给交社保 这是我第一次面临裁员
因为我不想跟着那个老大(开课吧老大 这里叫B) ,本来就想借着这次机会跑,所以我 还挺高兴的,本来以为能多给点赔偿,哈哈 但是很尴尬 集体公司都不开工资,让大家 去仲裁 摆烂 员工,学员去找 都不给
1.4 新公司
我算是知道了 我还是老实干 基础组件,我不适合做完全业务的。从开课吧走 10天左右 就确定了offer,现在做基础组件,还是很适应,比纯业务 更适合自己。
总结
1 学习方法 要添加总结时间 自己说出来你 今天学的东西,你要把自己说明白,有不会的就立刻去学习去
2 根据你喜欢的工作内容 选择 你的公司/offer
3 老大是很关键的,面试 是一个双向选择的,你要看你和 你这个未来的老大 合不合的来。合不来 不要去
4 做事情要有计划,想到什么,立刻去做 保持冲劲。 男人至死是少年
5 希望大家都好 互勉
疑问
我有时候 迷茫 ,同时有很多事要做,但是我只能选择 其中有一到两件事做,到底做那件事呢?
我现在的答案是 做有复利的事情
希望 大家都能努力工作的同时,把父母/伴侣 都照顾好
作者:雨夜之寂
来源:juejin.cn/post/7126779834541277191
简易的Android网络图片加载器
在项目开发中,我们加载图片一般使用的是第三方库,比如Glide,在闲暇之余突发奇想,自己动手实现一个简单的Android网络图片加载器。
首先定义API,API的定义应该简单易用,比如
imageLoader.displayImage(imageView,imagePath);
其次应该支持缓存。缓存一般是指三级缓存,先定义一个入口类ImageLoader
public ImageLoader(Activity activity) {
this.activity = activity;
memoryCache = new MemoryCache();
diskCache = new DiskCache(activity);
netCache = new NetCache(activity,memoryCache,diskCache);
}
在初始化的时候就初始化内存缓存,磁盘缓存,网络缓存三个变量,然后定义加载方法:
public void displayImage(final ImageView imageView, String url,int placeholder){
imageView.setTag(url);
imageView.setImageResource(placeholder);
Bitmap bitmap;
bitmap = memoryCache.getBitmap(url);
if(bitmap != null){
imageView.setImageBitmap(bitmap);
Log.i(TAG, "从内存中获取图片");
return;
}
bitmap = diskCache.getBitmap(url);
if(bitmap != null){
imageView.setImageBitmap(bitmap);
memoryCache.setBitmap(url,bitmap);
Log.i(TAG, "从磁盘中获取图片");
return;
}
netCache.getBitmap(imageView,url);
}
首先将图片地址设置给ImageView的tag,防止因为ImageView复用导致图片错乱的问题。然后设置一个占位图防止图片加载过慢ImageVIew显示白板
三级缓存中从内存中加载缓存信息是最快的,所以第一步从内存缓存中查找,如果找到了就直接设置给ImageView,否则继续从磁盘缓存中查找,找到了就显示,最后实在找不到就从网络下载图片
内存缓存
public class MemoryCache {
private LruCache<String,Bitmap> lruCache;
public MemoryCache() {
long maxMemory = Runtime.getRuntime().maxMemory() / 8;
lruCache = new LruCache<String,Bitmap>((int) maxMemory){
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount();
}
};
}
public Bitmap getBitmap(String url) {
return lruCache.get(url);
}
public void setBitmap(String url,Bitmap bitmap) {
lruCache.put(url,bitmap);
}
}
内存缓存比较简单,只需要将加载过的图片放入内存,然后下次加载直接获取,由于内存大小有限制,所以这里使用了LruCache算法保证缓存不会无限制增长。
磁盘缓存
对于已经缓存在磁盘上的文件,就不需要在从网络下载了,直接从磁盘读取。
public Bitmap getBitmap(String url) {
FileInputStream is;
String cacheUrl = Md5Utils.md5(url);
File parentFile = new File(Values.PATH_CACHE);
File file = new File(parentFile,cacheUrl);
if(file.exists()){
try {
is = new FileInputStream(file);
Bitmap bitmap = decodeSampledBitmapFromFile(file.getAbsolutePath());
is.close();
return bitmap;
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
考虑到多图加载的时候,如果图片太大容易OOM,所以需要对加载的图片稍作处理
public Bitmap decodeSampledBitmapFromFile(String pathName){
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(pathName,options);
options.inSampleSize = calculateInSampleSize(options)*2;
options.inJustDecodeBounds = false;
options.inPreferredConfig = Bitmap.Config.RGB_565;
return BitmapFactory.decodeFile(pathName,options);
}
这里降低了图片的采样,对图片的质量进行了压缩
对于本地没有缓存的图片,需要从网络下载,当获取到图片流之后,保存在本地
public void saveBitmap(InputStream inputStream, String url) throws IOException {
String cacheUrl = Md5Utils.md5(url);
File parentFile = new File(Values.PATH_CACHE);
if(!parentFile.exists()){
parentFile.mkdirs();
}
FileOutputStream fos = new FileOutputStream(new File(parentFile,cacheUrl));
byte[] bytes = new byte[1024];
int index = 0;
while ((index = inputStream.read(bytes))!=-1){
fos.write(bytes,0,index);
fos.flush();
}
inputStream.close();
fos.close();
}
为了防止图片url带一些非法字符导致创建文件失败,所以对url进行了md5处理
网络缓存
这里比较简单,直接从服务器加载图片信息就可以了,访问网络使用了OkHttp
public void getBitmap(final ImageView imageView, final String url) {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder() .get().url(url).build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
imageView.setImageResource(R.mipmap.ic_error);
}
@Override
public void onResponse(Call call, Response response) throws IOException {
InputStream inputStream = response.body().byteStream();
diskCache.saveBitmap(inputStream, url);
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
if (url != null && url.equals(imageView.getTag())) {
Bitmap bitmap = diskCache.getBitmap(url);
memoryCache.setBitmap(url, bitmap);
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
} else {
imageView.setImageResource(R.mipmap.ic_error);
}
} else {
imageView.setImageResource(R.mipmap.ic_place);
}
}
});
}
});
}
当获取到图片后,分别放入磁盘和内存缓存起来
使用
最后直接在需要加载图片的地方调用
new ImageLoader(activity).displayImage(imageView,path)
作者:晚来天欲雪_
来源:juejin.cn/post/7088693420109070373
年薪最高21万?哈哈想去杜蕾斯公司应聘了
来源:xhs@🌵
【入门级】Java解决动态规划背包问题
前言
本文是最入门级别的动态规划背包问题的解题过程,帮助小白理解动态规划背包问题的解题思路和转化为代码的过程。
动态规划背包问题是什么?
一个背包只能装下5kg物品;
现有:
物品一:1kg价值6元,
物品二:2kg价值10元,
物品三:3kg价值15元,
物品四:4kg价值12元。
问:怎么装,价值最大化? (每样物品只有一件,且每个物品不可拆分)
动态规划解题转代码
动态规划的解题套路千千万,但都是离不开穷举+if装这个物品会怎样else不装会怎样,最终比较一下结果哪条路得到价值最大,就是哪条路。
我选个最好理解的。
总体思路是:背包总共5kg分成1kg1kg的作为最外层循环(穷举的根),每次都取最优。
第一步:拆包填表格
当背包只有1kg时 | 当背包只有2kg时 | 当背包只有3kg时 | 当背包只有4kg时 | 当背包只有5kg时 | |
加入物品一(1kg,¥6) | |||||
加入物品二(2kg,¥10) | |||||
加入物品三(3kg,¥15) | |||||
加入物品四(4kg,¥12) |
如何填写表格?把当前状态(背包为某kg)下,最多能装的价格填进去!
1)横着看第一行:当背包1kg时,加入物品1是多少¥,就填进去;当背包是2kg,加入物品一是多少¥,就填进去......以此类推
当背包只有1kg时 | 当背包只有2kg时 | 当背包只有3kg时 | 当背包只有4kg时 | 当背包只有5kg时 | |
加入物品一(1kg,¥6) | ¥6 | ¥6 | ¥6 | ¥6 | ¥6 |
加入物品二(2kg,¥10) | |||||
加入物品三(3kg,¥15) | |||||
加入物品四(4kg,¥12) |
2)横着看第二行:当背包1kg时,加不进去物品二,那当背包1k时候利益最大就是¥6;当背包2kg时候加物品二是¥10,比加物品一的¥6多,所以利益最大是放二物品 ;当背包是3kg时候,在原有物品一的基础上,还可以再加物品二,价值就变为¥6+¥10=¥16元,以此类推。
当背包只有1kg时 | 当背包只有2kg时 | 当背包只有3kg时 | 当背包只有4kg时 | 当背包只有5kg时 | |
加入物品一(1kg,¥6) | ¥6 | ¥6 | ¥6 | ¥6 | ¥6 |
加入物品二(2kg,¥10) | ¥6 | ¥10 | ¥16 | ¥16 | ¥16 |
加入物品三(3kg,¥15) | |||||
加入物品四(4kg,¥12) |
3)横着看第三行:1kg放不下;2kg也装不下,取之前最大利益10¥;3kg可以装下,但是3kg全装物品三价值为¥15,但是之前两物品可以得到¥16,那么还是之前的落下来¥16。4kg时候,装3kg物品三还剩1kg装下物品一后二者之和为¥21,所以最大值取物品三加物品一的。
当背包只有1kg时 | 当背包只有2kg时 | 当背包只有3kg时 | 当背包只有4kg时 | 当背包只有5kg时 | |
加入物品一(1kg,¥6) | ¥6 | ¥6 | ¥6 | ¥6 | ¥6 |
加入物品二(2kg,¥10) | ¥6 | ¥10 | ¥16 | ¥16 | ¥16 |
加入物品三(3kg,¥15) | ¥6 | ¥10 | ¥16 | ¥21 | ¥25 |
加入物品四(4kg,¥12) |
4)横着看第四行:同上道理
当背包只有1kg时 | 当背包只有2kg时 | 当背包只有3kg时 | 当背包只有4kg时 | 当背包只有5kg时 | |
加入物品一(1kg,¥6) | ¥6 | ¥6 | ¥6 | ¥6 | ¥6 |
加入物品二(2kg,¥10) | ¥6 | ¥10 | ¥16 | ¥16 | ¥16 |
加入物品三(3kg,¥15) | ¥6 | ¥10 | ¥16 | ¥21 | ¥25 |
加入物品四(4kg,¥12) | ¥6 | ¥10 | ¥16 | ¥21 | ¥25 |
以上,物品加完,价值最大在哪里?在表格最右下角!
这个思路,和穷举四个物品432*1=24种结果区别在哪里?这种方式相当于穷举每次都有最优解!
这就是状态转移方程: 就是拿装和 不装 每次都和上面的比较,大了就装,小了就不装!
第二步:转为代码
以上这拆包填表过程转为伪代码是什么?
一、首先看空表格:即初始化代码
1、最基础的准备:
// 物品价值
int value[] = { 6, 10, 15, 12 };
// 物品重量
int weight[] = { 1, 2, 3, 4 };
// 背包总容量
int bagWeight = 5;
// 物品总数量
int num = 4;
2、准备下面这个表格:二维数组
// 表格内容:第一个[]表示行(坐标) 第二个[]表示列 (坐标)
// [][] 两个坐标定位出哪个表格,dp[][]取出的就是最大价值金额
// 防止越界可以加个1,横是待装物品个数,竖是被拆分的背包重量
int dp[][] = new int[num + 1][bagWeight + 1];
当背包只有1kg时 | 当背包只有2kg时 | 当背包只有3kg时 | 当背包只有4kg时 | 当背包只有5kg时 | |
加入物品一(1kg,¥6) | |||||
加入物品二(2kg,¥10) | |||||
加入物品三(3kg,¥15) | 坐标是[2][3]dp[2][3] = ¥21 | ||||
加入物品四(4kg,¥12) | 这里是最大 |
二、看怎么循环填表格
1、按行循环
// 最外层循环即 表格横向有几行就循环几次
for (int i = 1; i <= num; i++) {
}
2、每行里按列循环
// 被拆分的背包 单行从左到右依次循环,有几列循环几次
for (int everyBagWeight = 1; everyBagWeight <= bagWeight; everyBagWeight++) {
}
3、表格里填入多少如何判断
1)能装下这个物品:
// if 物品重量 小于 当前拆分后背包的重量 就是能装
// weight[i是最外层的循环(有几个物品i就等于几,i-1下标的值就是第几个物品的重量值)]
if (weight[i - 1] <= everyBagWeight) {
}
1-1)能装下这个物品,装还是不装
// 能装就计算装之后和装之前 哪个是最大价值
dp[i][everyBagWeight] = Math.max(
// 装之后
value[i - 1] + dp[i - 1][everyBagWeight - weight[i - 1]],
// 装之前
dp[i - 1][everyBagWeight]
);
这里很晕举个例子说明,以红色格子为例子:
横坐标[1] | 横坐标[2] | 横坐标[3] | 横坐标[4] | 横坐标[5] | |
纵坐标[1] | ¥6 | ¥6 | ¥6 | ¥6 | ¥6 |
纵坐标[2] | ¥6 | ¥10 | ¥16 | ¥16 | ¥16 |
纵坐标[3] | ¥6 | ¥10 | ¥16 | ¥21 | ¥25 |
纵坐标[4] | ¥6 | ¥10 | ¥16 | ¥21 | ¥25 |
// 红色格子能装就计算装之后和装之前 哪个是最大价值
//给纵坐标4,横坐标5的格子赋值
dp[i=4 ][everyBagWeight = 5kg] =
Math.max(
// 装之后~~~~~~~~~~~~~~~~
//value[i-1=3]是第四个物品的价值 = 12¥
//dp[i-1=3]是纵坐标是[3],
//[5 - weight[3]]即(总重量)减掉(当前物品四的weight[3]=4kg )=1kg
//dp[3][1]是纵坐标是[3],横坐标为[1]即粉色格子值¥6
//所以装之后总价值为¥12+¥6=¥18
value[i-1] + dp[i - 1][everyBagWeight - weight[i - 1]],//=¥18
//-----------------------------------------------------
// 装之前~~~~~~~~~~~~~~~
//dp[i-1=3][everyBagWeight = 5]
//纵坐标是3 横坐标是5 即是绿色格子的值 ¥25
dp[i - 1][everyBagWeight]
);
//取最大:25>18所以是赋值25
2)装不下
// 装不下 就是绿色格子直接赋值上面的价值
} else {
dp[i][everyBagWeight] = dp[i - 1][everyBagWeight];
}
三、输出结果(最大价值)
//表格右下角就是结果
System.out.print(dp[num][bagWeight]);
第三步:完整代码
public class Bag {
public static void main(String[] args) {
// 物品价值
int value[] = { 6, 10, 15, 12 };
// 物品重量
int weight[] = { 1, 2, 3, 4 };
// 背包总容量
int bagWeight = 5;
// 物品总数量
int num = 4;
// 表格内容:第一个[]表示价值 第二个[]表示重量??
int dp[][] = new int[num + 1][bagWeight + 1];
// 每次加物品 最外层循环即表格横向有几行就循环几次
for (int i = 1; i <= num; i++) {
// 被拆分的背包 单行从左到右依次循环,有几列循环几次
for (int everyBagWeight = 1; everyBagWeight <= bagWeight; everyBagWeight++) {
// 尝试装物品
// if装 : 物品重量 小于 当前拆分后背包的重量
if (weight[i - 1] <= everyBagWeight) {
// 能装就计算装之后和装之前 哪个是最大价值
dp[i][everyBagWeight] = Math.max(
// 装之后
value[i - 1] + dp[i - 1][everyBagWeight - weight[i - 1]],
// 装之前
dp[i - 1][everyBagWeight]);
// 装不下 就是上面的价值
} else {
dp[i][everyBagWeight] = dp[i - 1][everyBagWeight];
}
}
}
//表格右下角就是结果
System.out.print(dp[num][bagWeight]);
}
}
动态规划写出路径
以上问题,我们只是计算出了最大价值是多少,那如果需要输出拿了哪个物品呢?
我们只需要把最右列倒着遍历,即背包重量最大时的容量都装了哪些物品,即可
当背包只有1kg时 | 当背包只有2kg时 | 当背包只有3kg时 | 当背包只有4kg时 | 当背包只有5kg时 | |
加入物品一(1kg,¥6) | ¥6 [1][5] | ||||
加入物品二(2kg,¥10) | ¥16 [2][5] | ||||
加入物品三(3kg,¥15) | ¥25 [3][5] | ||||
加入物品四(4kg,¥12) | ¥25 [4][5] |
拿这道题举例子,有如下这么几种情况:
1)如果加了物品四和没加物品四是一样的,代表物品四根本没有加入。即dp[4][5] ==dp[3][5]
2)如果加了物品三和没加物品三是不一样的,代表物品三是加入了的,需要输出!
3)因为我们表格横纵坐标都是从1开始的,所以遍历不到,最后补上就可以。
// 具体的物品输出,只需要遍历最后一列即可(从右下角表格向上走)
for (int j = num; j > 1; j--) {
if (dp[j][bagWeight] == dp[j - 1][bagWeight]) {
// 该物品加入,与没加入没有差别,意味着该物品没有加入,即不用输出
} else {
// 该物品被加入了,输出即可
System.out.println("加入物品" + j + ":重量=" + weight[j - 1] + ";价值=" + value[j - 1]);
bagWeight = bagWeight - weight[j - 1];
}
}
// 如果背包不等于0,就要把最后一个商品加进来
if (bagWeight != 0) {
System.out.println("最后加入物品" + 1 + ":重量=" + weight[0] + ";价值=" + value[0]);
}
以上就是入门全部过程~
作者:Java程序员调优
链接:https://juejin.cn/post/7151416114949324813
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Flutter 组件集录 | 日期范围组件 - DateRangePickerDialog
前言
今天随手翻翻源码,突然看到 showDateRangePicker
,心中狂喜。原来 Flutter
早已将 日期范围选择器
内置了,可能有些小伙伴已经知道,但应该还有一部分朋友不知道。想当年,为了日期范围选择可吃了不少坑。做为 Flutter 内置组件收集狂魔的我,自然要发篇文章来安利一下这个组件。另外,该组件已经收录入 FutterUnit
,可更新查看。
1 | 2 | 3 |
---|---|---|
1. 日期范围选择器的使用
如下所示,是最简单的日期选择器操作示意:点击选择按钮时,触发下面代码中的 _show
方法:
主界面 | 选择器界面 | 保存后界面 |
---|---|---|
showDateRangePicker
是 Flutter
内置的方法,用于弹出日期范围的对话框。其中必传的参数有三个:
参数 | 类型 | 描述 |
---|---|---|
context | BuildContext | 构建上下文 |
firstDate | DateTime | 可选择的最早日期 |
lastDate | DateTime | 可选择的最晚日期 |
该方法返回 DateTimeRange?
泛型的 Future
对象,如下代码所示:可以通过 async/await
来等待 showDateRangePicker
任务的完成,获取 DateTimeRange?
结果对象。
void _show() async {
DateTime firstDate = DateTime(2021, 1, 1);
DateTime lastDate = DateTime.now();
DateTimeRange? range = await showDateRangePicker(
context: context,
firstDate: firstDate,
lastDate: lastDate,
);
print(range);
}
2. 日期范围选择器的语言
默认情况下,你会发现选择器是 英文
的(左图),怎么能改成中文呢?
英文 | 中文 |
---|---|
默认情况下,应用是不支持多语言,对于日历这种内置组件的多语言,可以通过加入 flutter_localizations
依赖实现:
dependencies:
flutter_localizations:
sdk: flutter
在 MaterialApp
中指定 localizationsDelegates
和 supportedLocales
。如果应用本身没有多语言的需求,可以指定只支持中文:
如果需要多语言,可以通过 locale
参数指定语言。如果未指定的话,会使用当前项目中的当前语言。
简单瞄一眼 showDateRangePicker
源码,可以看出 locale
非空时,会通过 Localizations.override
来让子树使用指定的 locale
语言:
3. 日期范围选择器的其他参数
除了默认的必需参数外,还有一些参数用于指定相关文字。下面三张图中标注了相关文本对应的位置,如果需要修改相关文字,设置对应参数即可:
1 | 2 | 3 |
---|---|---|
另外,showDateRangePicker
方法中可以传入 initialDateRange
设置弹出时的默认时间范围; currentDate
可以设置当前日期,如下右图的 8 日
:
DateTimeRange? range = await showDateRangePicker(
context: context,
firstDate: firstDate,
lastDate: lastDate,
initialDateRange: DateTimeRange(
start: DateTime(2022, 10, 1),
end: DateTime(2022, 10, 6),
),
currentDate: DateTime(2022, 10, 8)
);
未设置默认情况 | 设置默认值 |
---|---|
4. 源码简看
showDateRangePicker
方法,本质上就是就是通过 showDialog
方法展示对话框:
其中的内容是 DateRangePickerDialog
组件,方法中的绝大多数参数都是为了创建 DateRangePickerDialog
对象而准备的。
DateRangePickerDialog
就是一个很普通的 StatefulWidget
的派生类:
依赖 _DateRangePickerDialogState
状态类进行组件构建。如果在开发中,DateRangePickerDialog
无法满足使用需求,可以将代码拷贝一份进行魔改。
@override
State<DateRangePickerDialog> createState() => _DateRangePickerDialogState();
如下所示,可以在月份条目下叠放月份信息,看起来更直观;或者修改选中时的激活端点的装饰:
月份背景 | 修改端点装饰 |
---|---|
如下稍微翻翻源码,可以找到每个月份是通过 _MonthItem
组件构建的,所以需要对条目进行魔改,就在这里处理:
在 _MonthItemState
中,有 _buildDayItem
方法,如下是两端激活处的 BoxDecoration
装饰对象。 Decoration
的自定义能力非常强, BoxDecoration
如果无法满足需求,可以通过自定义 Decoration
进行绘制。
抓住这些核心的构建处理场合,我们可以更灵活地根据具体需求来魔改。而不是让应用千篇一律,毕竟 Flutter
框架中封装的组件只能满足大多数的基本使用场景,并不能尽善尽美。
需求是无限的,变化也是无限的,能应对变化的只有变化本身,能操纵变化的是我们编程者。
希望通过本文可以让更多的朋友知道 DateRangePickerDialog
的存在,让你的日期选择需求变得简单。那本文就到这里,谢谢观看 ~
作者:张风捷特烈
链接:https://juejin.cn/post/7153054582162063390
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Kotlin对象的懒加载方式?by lazy 与 lateinit 的异同
前言
属性或对象的延时加载是我们相当常用的,一般我们都是使用 lateinit 和 by lazy 来实现。
他们两者都是延时初始化,那么在使用时那么他们两者有什么区别呢?
lateinit
见名知意,延时初始化的标记。lateinit var可以让我们声明一个变量并且不用马上初始化,在我们需要的时候进行手动初始化即可。
如果我们不初始化会怎样?
private lateinit var name: String
findViewById<Button>(R.id.btn_load).click {
YYLogUtils.w("name:$name age:$age")
}
会报错:
所以对应这一种情况我们会有一个是否初始化的判断
private lateinit var name: String
findViewById<Button>(R.id.btn_load).click {
if (this::name.isInitialized) {
YYLogUtils.w("name:$name age:$age")
}
}
lateinit var的作用相对较简单,其实就是让编译期在检查时不要因为属性变量未被初始化而报错。(注意一定要记得初始化哦!)
by lazy
by lazy 委托延时处理,分为委托和延时
其实如果我们不想延时初始化,我们直接使用委托by也可以实现。
private var age: Int by Delegates.observable(18) { property, oldValue, newValue ->
YYLogUtils.w("发生了回调 property:$property oldValue:$oldValue newValue:$newValue")
}
findViewById<Button>(R.id.btn_load).click {
age = 25
YYLogUtils.w("name:$name age:$age")
}
我们通过 by Delegates 的方式就可以指定委托对象,这里我用的 Delegates.obsevable 它的作用是修改 age 的值之后会有回调的处理。
运行的效果:
除了 Delegates.obsevable 它还有其他的用法。
public object Delegates {
public fun <T : Any> notNull(): ReadWriteProperty<Any?, T> = NotNullVar()
public inline fun <T> observable(initialValue: T, crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Unit):
ReadWriteProperty<Any?, T> =
object : ObservableProperty<T>(initialValue) {
override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) = onChange(property, oldValue, newValue)
}
public inline fun <T> vetoable(initialValue: T, crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Boolean):
ReadWriteProperty<Any?, T> =
object : ObservableProperty<T>(initialValue) {
override fun beforeChange(property: KProperty<*>, oldValue: T, newValue: T): Boolean = onChange(property, oldValue, newValue)
}
}
private class NotNullVar<T : Any>() : ReadWriteProperty<Any?, T> {
private var value: T? = null
public override fun getValue(thisRef: Any?, property: KProperty<*>): T {
return value ?: throw IllegalStateException("Property ${property.name} should be initialized before get.")
}
public override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
this.value = value
}
}
- notNull方法我们可以看到就是说这个对象不能为null,否则就会抛出异常。
- observable方法主要用于监控属性值发生变更,类似于一个观察者。当属性值被修改后会往外部抛出一个变更的回调。
- vetoable方法跟observable类似,都是用于监控属性值发生变更,当属性值被修改后会往外部抛出一个变更的回调。与observable不同的是这个回调会返回一个Boolean值,来决定此次属性值是否执行修改。
其实用不用委托没什么区别,就是看是否需要属性变化的回调监听,否则我们直接用变量即可
private var age: Int = 18
findViewById<Button>(R.id.btn_load).click {
age = 25
YYLogUtils.w("name:$name age:$age")
}
如果我们想实现延时初始化的关键就是 lazy 关键字,所以,lazy是如何工作的呢? 让我们一起在Kotlin标准库参考中总结lazy()方法,如下所示:
- lazy() 返回的是一个存储在lambda初始化器中的Lazy类型实例。
- getter的第一次调用执行传递给lazy()的lambda并存储其结果。
- 后面再调用的话,getter调用只返回存储中的值。
简单地说,lazy创建一个实例,在第一次访问属性值时执行初始化,存储结果并返回存储的值。
private val age: Int by lazy { 18 / 2 }
findViewById<Button>(R.id.btn_load).click {
age = 25
YYLogUtils.w("name:$name age:$age")
}
由于我们使用的是 by lazy ,归根到底还是一种委托,只是它是一种特殊的委托,它的过程是这样的:
我们的属性 age 需要 by lazy 时,它生成一个该属性的附加属性:age?delegate。
在构造器中,将使用 lazy(()->T) 创建的 Lazy 实例对象赋值给 age?delegate。
当该属性被调用,即其getter方法被调用时返回 age?delegate.getVaule(),而 age?delegate.getVaule()方法的返回结果是对象 age?delegate 内部的 _value 属性值,在getVaule()第一次被调用时会将_value进行初始化并储存起来,往后都是直接将_value的值返回,从而实现属性值的唯一一次的初始化,并无法再次修改。所以它是只读的。
当我们调用这个 age 这个属性的时候才会初始化,它属于一种懒加载,既然是懒加载,就必然涉及到线程安全的问题,我们看看lazy是怎么解决的。
public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)
public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
when (mode) {
LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
}
public actual fun <T> lazy(lock: Any?, initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer, lock)
我们需要考虑的是线程安全和非线程安全
SYNCHRONIZED通过加锁来确保只有一个线程可以初始化Lazy实例,是线程安全的
PUBLICATION表示不加锁,可以并发访问多次调用,但是我之接收第一个返回的值作为Lazy的实例,其他后面返回的是啥玩意儿我不管。这也是线程安全的
NONE不加锁,是线程不安全的
总结
总的来说其实 lateinit 是延迟初始化, by lazy 是懒加载即初始化方式已确定,只是在使用的时候执行。
虽然两者都可以推迟属性初始化的时间,但是 lateinit var 只是让编译期忽略对属性未初始化的检查,后续在哪里以及何时初始化还需要开发者自己决定。而by lazy真正做到了声明的同时也指定了延迟初始化时的行为,在属性被第一次被使用的时候能自动初始化。
并且 lateinit 是可读写的,by lazy 是只读的。
那我们什么时候该使用 lateinit,什么时候使用 by lazy ?
其实大部分情况下都可以通用,只是 by lazy 一般用于非空只读属性,需要延迟加载情况,而 lateinit 一般用于非空可变属性,需要延迟加载情况。
惯例,如有错漏还请指出,如果有更好的方案也欢迎留言区交流。
如果感觉本文对你有一点点的启发,还望你能点赞
支持一下,你的支持是我最大的动力。
Ok,这一期就此完结。
作者:newki
链接:https://juejin.cn/post/7152689103794864159
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
落地 Kotlin 代码规范,DeteKt 了解一下~
前言
各个团队多少都有一些自己的代码规范,但制定代码规范简单,困难的是如何落地。如果完全依赖人力Code Review
难免有所遗漏。
这个时候就需要通过静态代码检查工具在每次提交代码时自动检查,本文主要介绍如何使用DeteKt
落地Kotlin
代码规范,主要包括以下内容
- 为什么使用
DeteKt
? IDE
接入DeteKt
插件CLI
命令行方式接入DeteKt
Gradle
方式接入DeteKt
- 自定义
Detekt
检测规则 Github Action
集成Detekt
检测
为什么使用DeteKt
?
说起静态代码检查,大家首先想起来的可能是lint
,相比DeteKt
只支持Kotlin
代码,lint
不仅支持Kotlin
,Java
代码,也支持资源文件规范检查,那么我们为什么不使用Lint
呢?
在我看来,Lint
在使用上主要有两个问题:
- 与
IDE
集成不够好,自定义lint
规则的警告只有在运行./gradlew lint
后才会在IDE
上展示出来,在clean
之后又会消失 lint
检查速度较慢,尤其是大型项目,只对增量代码进行检查的逻辑需要自定义
而DeteKt
提供了IDE
插件,开启后可直接在IDE
中查看警告,这样可以在第一时间发现问题,避免后续检查发现问题后再修改流程过长的问题
同时Detekt
支持CLI
命令行方式接入与Gradle
方式接入,支持只检查新增代码,在检查速度上比起lint
也有一定的优势
IDE
接入DeteKt
插件
如果能在IDE
中提示代码中存在的问题,应该是最快发现问题的方式,DeteKt
也贴心的为我们准备了插件,如下所示:
主要可以配置以下内容:
DeteKt
开关- 格式化开关,
DeteKt
直接使用了ktlint
的规则 Configuration file
:规则配置文件,可以在其中配置各种规则的开关与参数,默认配置可见:default-detekt-config.ymlBaseline file
:基线文件,跳过旧代码问题,有了这个基线文件,下次扫描时,就会绕过文件中列出的基线问题,而只提示新增问题。Plugin jar
: 自定义规则jar
包,在自定义规则后打出jar
包,在扫描时就可以使用自定义规则了
DeteKt IDE
插件可以实时提示问题(包括自定义规则),如下图所示,我们添加了自定义禁止使用kae
的规则:
对于一些支持自动修复的格式问题,DeteKt
插件支持自动格式化,同时也可以配置快捷键,一键自动格式化,如下所示:
CLI
命令行方式接入DeteKt
DeteKt
支持通过CLI
命令行方式接入,支持只检测几个文件,比如本次commit
提交的文件
我们可以通过如下方式,下载DeteKt
的jar
然后使用
curl -sSLO https://github.com/detekt/detekt/releases/download/v1.22.0-RC1/detekt-cli-1.22.0-RC1.zip
unzip detekt-cli-1.22.0-RC1.zip
./detekt-cli-1.22.0-RC1/bin/detekt-cli --help
DeteKt CLI
支持很多参数,下面列出一些常用的,其他可以参见:Run detekt using Command Line Interface
Usage: detekt [options]
Options:
--auto-correct, -ac
支持自动格式化的规则自动格式化,默认为false
Default: false
--baseline, -b
如果传入了baseline文件,只有不在baseline文件中的问题才会掘出来
--classpath, -cp
实验特性:传入依赖的class路径和jar的路径,用于类型解析
--config, -c
规则配置文件,可以配置规则开关及参数
--create-baseline, -cb
创建baseline,默认false,如果开启会创建出一个baseline文件,供后续使用
--input, -i
输入文件路径,多个路径之间用逗号连接
--jvm-target
EXPERIMENTAL: Target version of the generated JVM bytecode that was
generated during compilation and is now being used for type resolution
(1.6, 1.8, 9, 10, 11, 12, 13, 14, 15, 16 or 17)
Default: 1.8
--language-version
为支持类型解析,需要传入java版本
--plugins, -p
自定义规则jar路径,多个路径之间用,或者;连接
在命令行可以直接通过如下方式检查
java -jar /path/to/detekt-cli-1.21.0-all.jar # detekt-cli-1.21.0-all.jar所在路径
-c /path/to/detekt_1.21.0_format.yml # 规则配置文件所在路径
--plugins /path/to/detekt-formatting-1.21.0.jar # 格式化规则jar,主要基于ktlint封装
-ac # 开启自动格式化
-i $FilePath$ # 需要扫描的源文件,多个路径之间用,或者;连接
通过如上方式进行代码检查速度是非常快的,根据经验来说一般就是几秒之内可以完成,因此我们完成可以将DeteKt
与git hook
结合起来,在每次提交commit
的时候进行检测,而如果是一些比较耗时的工具比如lint
,应该是做不到这一点的
类型解析
上面我们提到了,DeteKt
的--classpth
参数与--language-version
参数,这些是用于类型解析的。
类型解析是DeteKt
的一项功能,它允许 Detekt
对您的 Kotlin
源代码执行更高级的静态分析。
通常,Detekt
在编译期间无法访问编译器语义分析的结果,我们只能获取Kotlin
源代码的抽象语法树,却无法知道语法树上符号的语义,这限制了我们的检查能力,比如我们无法判断符号的类型,两个符号究竟是不是同一个对象等
通过启用类型解析,Detekt
可以获取Kotlin
编译器语义分析的结果,这让我们可以自定义一些更高级的检查。
而要获取类型与语义,当然要传入依赖的class
,也就是classpath
,比如android
项目中常常需要传入android.jar
与kotlin-stdlib.jar
Gradle
方式接入DeteKt
CLI
方式检测虽然快,但是需要手动传入classpath
,比较麻烦,尤其是有时候自定义规则需要解析我们自己的类而不是kotlin-stdlib.jar
中的类时,那么就需要将项目中的代码的编译结果传入作为classpath
了,这样就更麻烦了
DeteKt
同样支持Gradle
插件方式接入,这种方式不需要我们另外再配置classpath
,我们可以将CLI
命令行方式与Gradle
方式结合起来,在本地通过CLI
方式快速检测,在CI
上通过Gradle
插件进行完整的检测
接入步骤
// 1. 引入插件
plugins {
id("io.gitlab.arturbosch.detekt").version("[version]")
}
repositories {
mavenCentral()
}
// 2. 配置插件
detekt {
config = files("$projectDir/config/detekt.yml") // 规则配置
baseline = file("$projectDir/config/baseline.xml") // baseline配置
parallel = true
}
// 3. 自定义规则
dependencies {
detektPlugins "io.gitlab.arturbosch.detekt:detekt-formatting:1.21.0"
detektPlugins project(":customRules")
}
// 4. 配置 jvmTarget
tasks.withType(Detekt).configureEach {
jvmTarget = "1.8"
}
// DeteKt Task用于检测,DetektCreateBaselineTask用于创建Baseline
tasks.withType(DetektCreateBaselineTask).configureEach {
jvmTarget = "1.8"
}
// 5. 只分析指定文件
tasks.withType<io.gitlab.arturbosch.detekt.Detekt>().configureEach {
// include("**/special/package/**") // 只分析 src/main/kotlin 下面的指定目录文件
exclude("**/special/package/internal/**") // 过滤指定目录
}
如上所示,接入主要需要做这么几件事:
- 引入插件
- 配置插件,主要是配置
config
与baseline
,即规则开关与老代码过滤 - 引入
detekt-formatting
与自定义规则的依赖 - 配置
JvmTarget
,用于类型解析,但不用再配置classpath
了。 - 除了
baseline
之外,也可以通过include
与exclude
的方式指定只扫描指定文件的方式来实现增量检测
通过以上方式就接入成功了,运行./gradlew detektDebug
就可以开始检测了,扫描结果可在终端直接查看,并可以直接定位到问题代码处,也可以在build/reprots/
路径下查看输出的报告文件:
自定义Detekt
检测规则
要落地自己制定的代码规范,不可避免的需要自定义规则,当然我们首先要看下DeteKt
自带的规则,是否已经有我们需要的,只需把开关打开即可.
DeteKt
自带规则
DeteKt
自带的规则都可以通过开关配置,如果没有在 Detekt
闭包中指定 config
属性,detekt
会使用默认的规则。这些规则采用 yaml
文件描述,运行 ./gradlew detektGenerateConfig
会生成 config/detekt/detekt.yml
文件,我们可以在这个文件的基础上制定代码规范准则。
detekt.yml
中的每条规则形如:
complexity: # 大类
active: true
ComplexCondition: # 规则名
active: true # 是否启用
threshold: 4 # 有些规则,可以设定一个阈值
# ...
更多关于配置文件的修改方式,请参考官方文档-配置文件
Detekt
的规则集划分为 9 个大类,每个大类下有具体的规则:
规则大类 | 说明 |
---|---|
comments | 与注释、文档有关的规范检查 |
complexity | 检查代码复杂度,复杂度过高的代码不利于维护 |
coroutines | 与协程有关的规范检查 |
empty-blocks | 空代码块检查,空代码应该尽量避免 |
exceptions | 与异常抛出和捕获有关的规范检查 |
formatting | 格式化问题,detekt直接引用的 ktlint 的格式化规则集 |
naming | 类名、变量命名相关的规范检查 |
performance | 检查潜在的性能问题 |
potentail-bugs | 检查潜在的BUG |
style | 统一团队的代码风格,也包括一些由 Detekt 定义的格式化问题 |
更细节的规则说明,请参考:官方文档-规则集说明
自定义规则
接下来我们自定义一个检测KAE
使用的规则,如下所示:
// 入口
class CustomRuleSetProvider : RuleSetProvider {
override val ruleSetId: String = "detekt-custom-rules"
override fun instance(config: Config): RuleSet = RuleSet(
ruleSetId,
listOf(
NoSyntheticImportRule(),
)
)
}
// 自定义规则
class NoSyntheticImportRule : Rule() {
override val issue = Issue(
"NoSyntheticImport",
Severity.Maintainability,
"Don’t import Kotlin Synthetics as it is already deprecated.",
Debt.TWENTY_MINS
)
override fun visitImportDirective(importDirective: KtImportDirective) {
val import = importDirective.importPath?.pathStr
if (import?.contains("kotlinx.android.synthetic") == true) {
report(
CodeSmell(
issue,
Entity.from(importDirective),
"'$import' 不要使用kae,推荐使用viewbinding"
)
)
}
}
}
代码其实并不复杂,主要做了这么几件事:
- 添加
CustomRuleSetProvider
作为自定义规则的入口,并将NoSyntheticImportRule
添加进去 - 实现
NoSyntheticImportRule
类,主要包括issue
与各种visitXXX
方法 issue
属性用于定义在控制台或任何其他输出格式上打印的ID
、严重性和提示信息visitImportDirective
即通过访问者模式访问语法树的回调,当访问到import
时会回调,我们在这里检测有没有添加kotlinx.android.synthetic
,发现存在则报告异常
支持类型解析的自定义规则
上面的规则没有用到类型解析,也就是说不传入classpath
也能使用,我们现在来看一个需要使用类型解析的自定义规则
比如我们需要在项目中禁止直接使用android.widget.Toast.show
,而是使用我们统一封装的工具类,那么我们可以自定义如下规则:
class AvoidToUseToastRule : Rule() {
override val issue = Issue(
"AvoidUseToastRule",
Severity.Maintainability,
"Don’t use android.widget.Toast.show",
Debt.TWENTY_MINS
)
override fun visitReferenceExpression(expression: KtReferenceExpression) {
super.visitReferenceExpression(expression)
if (expression.text == "makeText") {
// 通过bindingContext获取语义
val referenceDescriptor = bindingContext.get(BindingContext.REFERENCE_TARGET, expression)
val packageName = referenceDescriptor?.containingPackage()?.asString()
val className = referenceDescriptor?.containingDeclaration?.name?.asString()
if (packageName == "android.widget" && className == "Toast") {
report(
CodeSmell(
issue, Entity.from(expression), "禁止直接使用Toast,建议使用xxxUtils"
)
)
}
}
}
}
可以看出,我们在visitReferenceExpression
回调中检测表达式,我们不仅需要判断是否存在Toast.makeTest
表达式,因为可能存在同名类,更需要判断Toast
类的具体类型,而这就需要获取语义信息
我们这里通过bindingContext
来获取表达式的语义,这里的bindingContext
其实就是Kotlin
编译器存储语义信息的表,详细的可以参阅:K2 编译器是什么?世界第二高峰又是哪座?
当我们获取了语义信息之后,就可以获取Toast
的具体类型,就可以判断出这个Toast
是不是android.widget.Toast
,也就可以完成检测了
Github Action
集成Detekt
检测
在完成了DeteKt
接入与自定义规则之后,接下来就是每次提交代码时在CI
上进行检测了
一些大的开源项目每次提交PR
都会进行一系列的检测,我们也用Github Action
来实现一个
我们在.github/workflows
目录添加如下代码
name: Android CI
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
detekt-code-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
cache: gradle
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: DeteKt Code Check
run: ./gradlew detektDebug
这样在每次提交PR
的时候,就都会自动调用该workflow
进行检测了,检测不通过则不允许合并,如下所示:
点进去也可以看到详细的报错,具体是哪一行代码检测不通过,如图所示:
总结
本文主要介绍了DeteKt
的接入与如何自定义规则,通过IDE
集成,CLI
命令行方式与Gradle
插件方式接入,以及CI
自动检测,可以保证代码规范,IDE
提示,CI
检测三者的统一,方便提前暴露问题,提高代码质量。
如果本文对你有所帮助,欢迎点赞~
示例代码
本文所有代码可见:github.com/RicardoJian…
参考资料
作者:程序员江同学
链接:https://juejin.cn/post/7152886037746827277
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »
Android修改弹窗样式的几种方式
一、载入布局修改样式
这种方式大家都比较熟悉,直接在xml 上设计布局的内容,然后创建弹窗时加载这个布局,这个方式可以让我们更好的自定义样式,比较考验个人的审美和写UI 的能力,如果你很强的话,那么你可以设计各种花里胡哨的的弹窗,下面我简单的介绍一下这个方式的使用。
先定义一个edit_name.xml 的文件,在这个文件中写入下面的代码。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_height="wrap_content">
<TextView
android:layout_marginTop="10dp"
android:padding="10dp"
android:layout_width="match_parent"
android:text="@string/please_input_name"
android:textSize="20sp"
android:textAlignment="center"
android:layout_height="wrap_content"
android:gravity="center_horizontal">
</TextView>
<EditText
android:id="@+id/name_edit"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</EditText>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_marginTop="10dp"
android:padding="15dp"
android:layout_height="wrap_content">
<TextView
android:id="@+id/info_n"
app:layout_constraintTop_toTopOf="parent"
android:text="@string/cancel"
android:textSize="18sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintHorizontal_bias="0.3"
app:layout_constraintRight_toRightOf="parent">
</TextView>
<TextView
android:id="@+id/info_y"
app:layout_constraintTop_toTopOf="parent"
android:text="@string/sure"
android:textSize="18sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintHorizontal_bias="0.7"
app:layout_constraintRight_toRightOf="parent">
</TextView>
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
上面的布局文件出来的效果是这样的 。
xml 文件写好了,那么我们看看代码是如何载入这个布局的。先创建一个 AlertDialog(dialog) 和 View ( dialogView) 对象 , 然后 dialogView 载入上面写好的布局文件, 通过 dialog.setView(dialogView) 设置 dialog 的布局。
private void showDialog1() {
// 创建一个 dialogView 弹窗
AlertDialog.Builder builder = new
AlertDialog.Builder(MainActivity.this);
final AlertDialog dialog = builder.create();
View dialogView = null;
//设置对话框布局
dialogView = View.inflate(MainActivity.this,
R.layout.edit_name, null);
dialog.setView(dialogView);
dialog.show();
// 获取布局控件
editName =(EditText) dialogView.findViewById(R.id.name_edit);
editN= (TextView) dialogView.findViewById(R.id.info_n);
editY = (TextView) dialogView.findViewById(R.id.info_y);
editN.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dialog.dismiss();
}
});
editY.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this,"姓名为:"+editName.getText().toString(),Toast.LENGTH_SHORT).show();
dialog.dismiss();
}
});
}
这种方式的载入布局,后面如果你有需求要改动,或者改变样式,那么你直接修改 xml 文件 , 或者在java 代码中重新设置一个新的布局。
二、载入style样式
载入style 样式呢,这个方法适用于所有Android 布局控件,所有控件都可以通过这个方式去修改样式,当然前提是你得会写 style 样式。当然,我也对这个东西了解不是很深,在这就先班门弄斧、关公面前舞大刀一下,浅浅的介绍一下这个东西。
首先在values目录下创建一个 styles.xml 文件
在文件中创建一个自定义的样式,如下所示,这个样式特别简单,就是一些基本的定义。这里的 name="myDialogStyle" 很重要,下面我们载入这个样式时,就是根据这个 name 找到这个样式的。
<!--重写系统弹出Dialog -->
<style name="myDialogStyle" parent="android:Theme.Dialog">
<item name="android:windowFrame">@null</item>
<item name="android:windowIsFloating">true</item>
<item name="android:windowIsTranslucent">false</item>
<item name="android:windowNoTitle">true</item><!--除去title-->
<item name="android:windowContentOverlay">@null</item>
<item name="android:backgroundDimEnabled">false</item>
<item name="android:windowBackground">@null</item><!--除去背景色-->
</style>
在 java 代码中创建弹窗时载入这个样式。
private void showDialog3() {
AlertDialog mDialog = new AlertDialog.Builder(MainActivity.this, R.style.myDialogStyle)
.setTitle("标题")
.setMessage("这个是什么呢?")
.setPositiveButton(R.string.sure,null)
.setNegativeButton(R.string.cancel, null)
.create();
mDialog.show();
}
额,好吧,我承认有点丑,毕竟我不是做UI的,似乎这是个很好的借口。。。。。
人都是爱美的,看到这么丑总觉得怪怪的,重新扣了下面的这段样式
<style name="myDialogStyleAlert" parent="@android:style/Theme.Holo.Light.Dialog">
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowMinWidthMajor">@android:dimen/dialog_min_width_major</item>
<item name="android:windowMinWidthMinor">@android:dimen/dialog_min_width_minor</item>
</style>
在代码中引入这个样式后,效果如下所示。似乎好看点了。
当然这篇文章的主要目的并不是让你弄成一个好看的弹窗,这个我也不会,还是回归主题,我们如何修改弹窗的样式,用这种方法呢,也能争对性的修改弹窗的样式,只要你知道样式的内容代表什么,那么都能进行简单的修改。
三、通过反射机制修改弹窗样式
我们直接看代码,大家可能会好奇,哎,这个东西是怎么来的,为什么这么写呢?说起这个,那我们不得不先看看源码了。
private void showDialog2() {
AlertDialog mDialog = new AlertDialog.Builder(MainActivity.this)
.setTitle("标题")
.setMessage("这个是什么呢?")
.setPositiveButton(R.string.sure,null)
.setNegativeButton(R.string.cancel, null)
.show();
// 修改弹窗的背景颜色
mDialog.getWindow().setBackgroundDrawableResource(R.color.purple_200);
// 修改 确定取消 按钮的字体大小
mDialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextSize(20);
mDialog.getButton(DialogInterface.BUTTON_NEGATIVE).setTextSize(20);
try {
//获取mAlert对象
Field mAlert = AlertDialog.class.getDeclaredField("mAlert");
mAlert.setAccessible(true);
Object mAlertController = mAlert.get(mDialog);
//获取mTitleView并设置大小颜色
Field mTitle = mAlertController.getClass().getDeclaredField("mTitleView");
mTitle.setAccessible(true);
TextView mTitleView = (TextView) mTitle.get(mAlertController);
mTitleView.setTextSize(40);
mTitleView.setTextColor(Color.WHITE);
//获取mMessageView并设置大小颜色
Field mMessage = mAlertController.getClass().getDeclaredField("mMessageView");
mMessage.setAccessible(true);
TextView mMessageView = (TextView) mMessage.get(mAlertController);
mMessageView.setTextColor(Color.RED);
mMessageView.setTextSize(30);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
在 android studio 中 使用 ctrl + shift + n 的快捷键, 然后搜索 AlertDialog 就可以看到源码的文件,我们打开这个文件
在 AlertDialog 这个文件中,在开头的位置,很容易就看到 mAlert 这个对象的声明
下面这段代码就是通过放射机制获取 mAlert 这个对象。
//获取mAlert对象
Field mAlert = AlertDialog.class.getDeclaredField("mAlert");
mAlert.setAccessible(true);
Object mAlertController = mAlert.get(mDialog);
通过同样的方法 查看 AlertController.java 这个文件的代码,查看这个代码可以发现这里声明了一些变量,这些变量就是弹窗的组成,通过变量名能够大概知道它代表着什么东西。
下面这两段就是设置弹窗标题和消息的样式的代码。
//获取mTitleView并设置大小颜色
Field mTitle = mAlertController.getClass().getDeclaredField("mTitleView");
mTitle.setAccessible(true);
TextView mTitleView = (TextView) mTitle.get(mAlertController);
mTitleView.setTextSize(40);
mTitleView.setTextColor(Color.WHITE);
//获取mMessageView并设置大小颜色
Field mMessage = mAlertController.getClass().getDeclaredField("mMessageView");
mMessage.setAccessible(true);
TextView mMessageView = (TextView) mMessage.get(mAlertController);
mMessageView.setTextColor(Color.RED);
mMessageView.setTextSize(30);
效果是这样的。细心的人可能会发现,上面设置的内容好像 跟下面显示的不一样吧,我读书少,你别骗我啊!
确实,上面通过反射的方式并没有让我的弹窗样式修改成功。
我查看了log ,发现有报错,大概就是因为无法通过反射机制找到对于的对象,所以并没有修改样式成功,那是不是说这个方法不可行呢,并不是,我简单查找了一下原因,怀疑是本地的环境有冲突,存在多个AlertDialog.java 这个文件的源码,无法精准的找到对应的变量,导致冲突报错了。
提示: 上面的方式提供一个思想,如果你在实际应用中没有找到别的方法解决,这个方式可以提供参考,当然,可能你得先解决这个报错的问题。
四、设置App style样式
上面讲了如何设置弹窗的 style样式,这里再讲讲从 App的层面来修改样式,也就是说设置App 的主题风格来设置弹窗的样式。
先在 styles.xml 文件中声明一个 App 样式,我设置的如下所示。
<style name="myAppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
<!--<item name="android:windowFullscreen">true</item>-->
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowBackground">@android:color/white</item>
<!-- item name="android:windowIsTranslucent">true</item -->
<item name="android:windowTranslucentNavigation">true</item>
<item name="android:selectableItemBackground">@null</item>
<item name="android:selectableItemBackgroundBorderless">@null</item>
<item name="android:windowEnableSplitTouch">false</item>
<item name="android:splitMotionEvents">false</item>
<item name="android:textColorPrimary">@color/teal_700</item>
<item name="android:colorControlNormal">@android:color/white</item>
<item name="android:textColorAlertDialogListItem">@android:color/white</item>
</style>
然后在 AndroidManifest.xml 使用这个theme 。
下面我们看看Java 代码
CharSequence[] stringList = new CharSequence[]{"苹果","香蕉","梨"};
private void showDialog4() {
int index = 1;
AlertDialog mDialog = new AlertDialog.Builder(MainActivity.this)
.setTitle("标题")
.setIcon(null)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(null, null)
.setSingleChoiceItems(stringList, index,null)
.create();
mDialog.show();
// 修改弹窗的背景颜色
mDialog.getWindow().setBackgroundDrawableResource(R.color.dialog_background_color);
}
效果如下所示。
不知道大家有没有发现,上面的弹窗跟前面几个不一样的,它是动态加载的,里面的内容可以根据需求动态增加,这种动态变化的,如果我要修改苹果、香蕉这些文字的颜色是白色时,前面的几种方式中,第一种是很难进行修改的,这个是动态变化的也不是直接在xml 上写死能解决的。第二种也是可以的,就是载入style样式后,弹窗并没有官方的那么美观,如果你能写成一模一样,那当我没说。通过放射机制修改,也是可以,就是我试了一下,没找到怎么改(好吧,是我太菜!)。
好了,说了这么多,主要的需求就是,怎么把上面的苹果、香蕉这些文字的颜色改成白色。其实我已经给了解决方案 , 上面的 style 样式中的最后一行代码, 没错就是下面这行代码。 为什么是这行代码呢,不能别的吗? 额 ,还真不能! 下面听我娓娓道来。
<item name="android:textColorAlertDialogListItem">@android:color/white</item>
搜索源码 values.xml 文件,搜索 AlertDialog, 查找到下面的的位置。
其中下面红色的框框是我们要找的东西,这里进入这个布局文件
在这个布局文件中,我们可以发现下面设置 textColor , 这个就是设置选择框文字的颜色,我们再点进去查看这个设置的资源
点击上面的资源会跳转到下面的位置 ,这里可以看到一个name 为 textColorAlertDialogListItem 的资源,在这个文件中,查找这个name ,
就可以看到在这里设置颜色,所以这个 android:textColorAlertDialogListItem 就是我们要的东西。
在定义的布局文件中,重新定义这个 android:textColorAlertDialogListItem 的变量的颜色。也就是上面我写的这行代码。
<item name="android:textColorAlertDialogListItem">@android:color/white</item>
上面已经讲了一下修改弹窗样式的方式的思维方式,我写的样式很丑并不重要,重要的是这个思维,这种思维方式并不仅仅适用于弹窗的样式,其他安卓控件也是适用。毕竟编程的思维是相通的。
下面我找到的一些常用的样式 仅供参考,具体效果还望实际操作后看效果。
<style name="AppThemeDemo" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- 应用的主要色调,actionBar默认使用该颜色,Toolbar导航栏的底色 -->
<item name="colorPrimary">@color/white</item>
<!-- 应用的主要暗色调,statusBarColor 默认使用该颜色 -->
<item name="colorPrimaryDark">@color/white</item>
<!-- 一般控件的选中效果默认采用该颜色,如 CheckBox,RadioButton,SwitchCompat,ProcessBar等-->
<item name="colorAccent">@color/colorAccent</item>
<!-- 状态栏、顶部导航栏 相关-->
<!-- status bar color -->
<item name="android:statusBarColor">#00000000</item>
<!-- activity 是否能在status bar 底部绘制 -->
<item name="android:windowOverscan">true</item>
<!-- 让status bar透明,相当于statusBarColor=transparent + windowOverscan=true -->
<item name="android:windowTranslucentStatus">true</item>
<!-- 改变status bar 文字颜色, true黑色, false白色,API23可用-->
<item name="android:windowLightStatusBar">true</item>
<!-- 全屏显示,隐藏状态栏、导航栏、底部导航栏 -->
<item name="android:windowFullscreen">true</item>
<!-- hide title bar -->
<item name="windowNoTitle">true</item>
<!-- 底部虚拟导航栏颜色 -->
<item name="android:navigationBarColor">#E91E63</item>
<!-- 让底部导航栏变半透明灰色,覆盖在Activity之上(默认false,activity会居于底部导航栏顶部),如果设为true,navigationBarColor 失效 -->
<item name="android:windowTranslucentNavigation">true</item>
<!-- WindowBackground,可以设置@drawable,颜色引用(@color),不能设置颜色值(#fffffff),
Window区域说明:Window涵盖整个屏幕显示区域,包括StatusBar的区域。当windowOverscan=false时,window的区域比Activity多出StatusBar,当windowOverscan=true时,window区域与Activity相同-->
<item name="android:windowBackground">@drawable/ic_launcher_background</item>
<!--<item name="android:windowBackground">@color/light_purple</item>-->
<!-- 控件相关 -->
<!-- button 文字是否全部大写(系统默认开)-->
<item name="android:textAllCaps">false</item>
<!-- 默认 Button,TextView的文字颜色 -->
<item name="android:textColor">#B0C4DE</item>
<!-- 默认 EditView 输入框字体的颜色 -->
<item name="android:editTextColor">#E6E6FA</item>
<!-- RadioButton checkbox等控件的文字 -->
<item name="android:textColorPrimaryDisableOnly">#1C71A9</item>
<!-- 应用的主要文字颜色,actionBar的标题文字默认使用该颜色 -->
<item name="android:textColorPrimary">#FFFFFF</item>
<!-- 辅助的文字颜色,一般比textColorPrimary的颜色弱一点,用于一些弱化的表示 -->
<item name="android:textColorSecondary">#C1C1C1</item>
<!-- 控件选中时的颜色,默认使用colorAccent -->
<item name="android:colorControlActivated">#FF7F50</item>
<!-- 控件按压时的色调-->
<item name="android:colorControlHighlight">#FF00FF</item>
<!-- CheckBox,RadioButton,SwitchCompat等默认状态的颜色 -->
<item name="android:colorControlNormal">#FFD700</item>
<!-- 默认按钮的背景颜色 -->
<item name="android:colorButtonNormal">#1C71A9</item>
<!-- 【无效】 在theme中设置Activity的属性无效, 请到AndroidManifest中Activity标签下设置 -->
<item name="android:launchMode">singleTop</item>
<item name="android:screenOrientation">landscape</item>
</style>
代码已上传至 gitee :zpeien/AndroidProject - 码云 - 开源中国 (gitee.com)
作者:zpeien
链接:https://juejin.cn/post/7149415708626485284
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »
不使用第三方库怎么实现【前端引导页】功能?
前言
随着应用功能越来越多,繁多而详细的功能使用和说明文档,已经不能满足时代追求 快速 的需求,而 引导页(或分步引导) 本质就是 化繁为简,将核心功能以更简单、简短、明了的文字指引用户去使用对应的功能,特别是 ToB
的项目,各种新功能需求迭代非常快,免不了需要 引导页 的功能来快速帮助用户引导。
下面我们通过两个方面来围绕着【前端引导页】进行展开:
哪些第三方库可以直接使用快速实现功能?
如何自己实现前端引导页的功能?
第三方库的选择
如果你不知道如何做技术选型,可以看看 山月大佬 的这一篇文章 在前端中,如何更好地做技术选型?,下面就简单列举几个相关的库进行简单介绍,具体需求具体分析选择,其他和 API
使用、具体实现效果可以通过官方文档或对应的 README.md
进行查看。
vue-tour
vue-tour
是一个轻量级、简单且可自定义的 Tour
插件,配置也算比较简单清晰,但只适用于 Vue2
的项目,具体效果可以直接参考对应的前面链接对应的内容。
driver.js
driver.js
是一个强大而轻量级的普通 JavaScript
引擎,可在整个页面上驱动用户的注意力,只有 4kb
左右的体积,并且没有外部依赖,不仅高度可定制,还可以支持所有主流浏览器。
shepherd.js
shepherd.js
包含的 API
众多,大多场景都可以通过其对应的配置得到,缺点就是整体的包体积较大,并且配置也比较复杂,配置复杂的内容一般都需要进行二次封装,将可变和不可变的配置项进行抽离,具体效果可见其 官方文档。
intro.js
intro.js
是是一个开源的 vanilla Javascript/CSS
库,用于添加分步介绍或提示,大小在 10kB
左右,属于轻量级的且无外部依赖,详情可见 官方文档。
实现引导页功能
引导页核心功能其实就两点:
一是 高亮部分
二是 引导部分
而这两点其实真的不难实现,无非就是 引导部分 跟着 高亮部分 移动,并且添加一些简单的动画或过渡效果即可,也分为 蒙层引导 和 无蒙层引导,这里介绍相对比较复杂的 蒙层引导,下面就简单介绍两种简单的实现方案。
cloneNode + position + transition
核心实现:
高亮部分
通过
el.cloneNode(true)
复制对应目标元素节点,并将克隆节点添加到蒙层上
通过
margin
(或tranlate
、position
等)实现克隆节点的位置与目标节点重合
引导部分 通过
position: fixed
实现定位效果,并通过动态修改left、top
属性实现引导弹窗跟随目标移动过渡动画 通过
transition
实现位置的平滑移动页面 位置/内容 发生变化时(如:
resize、scroll
事件),需要重新计算位置信息
缺点:
目标节点需要被深度复制
不能实现边引导边操作
效果演示:
核心代码:
// 核心配置参数
const selectors = [
{
selector: "#btn1",
message: "点此【新增】数据!",
},
{
selector: "#btn2",
message: "小心【删除】数据!",
},
{
selector: "#btn3",
message: "可通过此按钮【修改】数据!",
},
{
selector: "#btn4",
message: "一键【完成】所有操作!",
},
];
// Guide.vue
<script setup>
import { computed, onMounted, ref } from "vue";
const props = defineProps({
selectors: Array,
});
const guideModalRef = ref(null);
const guideBoxRef = ref(null);
const index = ref(0);
const show = ref(true);
let cloneNode = null;
let currNode = null;
let message = computed(() => {
return props.selectors[index.value]?.message;
});
const genGuide = (hasChange = true) => {
// 前置操作
cloneNode && guideModalRef.value?.removeChild(cloneNode);
// 所有指引完毕
if (index.value > props.selectors.length - 1) {
show.value = false;
return;
}
// 获取目标节点信息
currNode =
currNode || document.querySelector(props.selectors[index.value].selector);
const { x, y, width, height } = currNode.getBoundingClientRect();
// 克隆节点
cloneNode = hasChange ? currNode.cloneNode(true) : cloneNode;
cloneNode.id = currNode.id + "_clone";
cloneNode.style = `
margin-left: ${x}px;
margin-top: ${y}px;
`;
// 指引相关
if (guideBoxRef.value) {
const halfClientHeight = guideBoxRef.value.clientHeight / 2;
guideBoxRef.value.style = `
left:${x + width + 10}px;
top:${y <= halfClientHeight ? y : y - halfClientHeight + height / 2}px;
`;
guideModalRef.value?.appendChild(cloneNode);
}
};
// 页面内容发生变化时,重新计算位置
window.addEventListener("resize", () => genGuide(false));
window.addEventListener("scroll", () => genGuide(false));
// 上一步/下一步
const changeStep = (isPre) => {
isPre ? index.value-- : index.value++;
currNode = null;
genGuide();
};
onMounted(() => {
genGuide();
});
</script>
<template>
<teleport to="body">
<div v-if="show" ref="guideModalRef" class="guide-modal">
<div ref="guideBoxRef" class="guide-box">
<div>{{ message }}</div>
<button class="btn" :disabled="index === 0" @click="changeStep(true)">
上一步
</button>
<button class="btn" @click="changeStep(false)">下一步</button>
</div>
</div>
</teleport>
</template>
<style scoped>
.guide-modal {
position: fixed;
z-index: 999;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.3);
}
.guide-box {
width: 150px;
min-height: 10px;
border-radius: 5px;
background-color: #fff;
position: absolute;
transition: 0.5s;
padding: 10px;
text-align: center;
}
.btn {
margin: 20px 5px 5px 5px;
}
</style>
z-index + position + transition
核心实现:
高亮部分 通过控制
z-index
的值,让目标元素展示在蒙层之上引导部分 通过
position: fixed
实现定位效果,并通过动态修改left、top
属性实现引导弹窗跟随目标移动过渡动画 通过
transition
实现位置的平滑移动页面 位置/内容 发生变化时(如:
resize、scroll
事件),需要重新计算位置信息
缺点:
当目标元素的父元素
position: fixed | absolute | sticky
时,目标元素的z-index
无法超过蒙版层(可参考shepherd.js
的svg
解决方案)
效果演示:
核心代码:
<script setup>
import { computed, onMounted, ref } from "vue";
const props = defineProps({
selectors: Array,
});
const guideModalRef = ref(null);
const guideBoxRef = ref(null);
const index = ref(0);
const show = ref(true);
let preNode = null;
let message = computed(() => {
return props.selectors[index.value]?.message;
});
const genGuide = (hasChange = true) => {
// 所有指引完毕
if (index.value > props.selectors.length - 1) {
show.value = false;
return;
}
// 修改上一个节点的 z-index
if (preNode) preNode.style = `z-index: 0;`;
// 获取目标节点信息
const target =
preNode = document.querySelector(props.selectors[index.value].selector);
target.style = `
position: relative;
z-index: 1000;
`;
const { x, y, width, height } = target.getBoundingClientRect();
// 指引相关
if (guideBoxRef.value) {
const halfClientHeight = guideBoxRef.value.clientHeight / 2;
guideBoxRef.value.style = `
left:${x + width + 10}px;
top:${y <= halfClientHeight ? y : y - halfClientHeight + height / 2}px;
`;
}
};
// 页面内容发生变化时,重新计算位置
window.addEventListener("resize", () => genGuide(false));
window.addEventListener("scroll", () => genGuide(false));
const changeStep = (isPre) => {
isPre ? index.value-- : index.value++;
genGuide();
};
onMounted(() => {
genGuide();
});
</script>
<template>
<teleport to="body">
<div v-if="show" ref="guideModalRef" class="guide-modal">
<div ref="guideBoxRef" class="guide-box">
<div>{{ message }}</div>
<button class="btn" :disabled="index === 0" @click="changeStep(true)">
上一步
</button>
<button class="btn" @click="changeStep(false)">下一步</button>
</div>
</div>
</teleport>
</template>
<style scoped>
.guide-modal {
position: fixed;
z-index: 999;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.3);
}
.guide-box {
width: 150px;
min-height: 10px;
border-radius: 5px;
background-color: #fff;
position: absolute;
transition: 0.5s;
padding: 10px;
text-align: center;
}
.btn {
margin: 20px 5px 5px 5px;
}
</style>
【扩展】SVG
如何完美解决 z-index
失效的问题?
这里以 shepherd.js
来举例说明,先来看起官方文档展示的 demo
效果:
在上述展示的效果中进行了一些验证:
正常点击
NEXT
进入下一步指引,仔细观察SVG
相关数据发生了变化等到指引部分指向代码块的内容区时,复制了此时
SVG
中和path
相关的参数返回到第一步很明显此时的高亮部分高度较小,将上一步复制的参数直接替换当前
SVG
中和path
相关的参数,此时发现整体SVG
高亮内容宽高发生了变化
核心结论:通过 SVG
可编码的特点,利用 SVG
来实现蒙版效果,并且在绘制蒙版时,预留出目标元素的高亮区间(即 SVG
不需要绘制这一部分),这样就解决了使用 z-index
可能会失效的问题。
最后
以上就是一些简单实现,但还有很多细节需要考虑,比如:边引导边操作的实现、定位原因导致的图层展示问题等仍需要优化。
相信大部分人第一直觉是:直接使用第三方库实现功能就好了呀,自己实现功能不全、也未必好用,属实没有必要。
对于这一点其实在早前看到的一句话说的挺好:了解底层实现原理比使用库本身更有意义,当然每个人的想法不同,不过如果你想开始了解原理又不能立马挑战一些高深的内容,为什么不先从自己感兴趣的又不是那么复杂的功能开始呢?
作者:熊的猫
来源:juejin.cn/post/7142633594882621454