0%

背景

64位的应用性能更好,也能运行在未来仅支持 64 位架构的设备上。目前各个应用市场也对 64 适配提出了要求。

Google Play 市场要求:

自 2019 年 8 月 1 日起,在 Google Play 上发布的应用必须支持 64 位架构。

国内应用市场:

小米应用商店与OPPO应用商店、vivo应用商店等已经发出通知

  • 2021年12月底:现有和新发布的应用/游戏,需上传包含64位包体的APK包(支持双包在架,和64位兼容32位的两个形式,不再接收仅支持32位的APK包)
  • 2022年8月底:硬件支持64位的系统,将仅接收含64位版本的APK包
  • 2023年底:硬件将仅支持64位APK,32位应用无法在终端上运行

32 和 64 位区别

这里需要先说一下 CPU 类型,每种 CPU 类型对应了一种 ABI(Application Binary Interface),常见的 abi 有 armeabi、armeabi-v7a、arm64-v8a、x86、x86_64 等。

  • armeabi: 第5代、第6代的ARM处理器,早期的手机用的比较多,基本可以淘汰了
  • armeabiv-v7a: 第7代及以上的 ARM 处理器。2011年15月以后的生产的大部分Android设备都使用它
  • arm64-v8a: 第8代、64位ARM处理器
  • x86: 平板、模拟器用得比较多
  • x86_64: 64位的平板
Read more »

Algorithm

反转从位置 m 到 n 的链表

限制一趟扫描完成反转。扫描得时候记录下 m 前面和 n 后面的节点,m 到 n 反转完成之后再分别接上。需要注意 m=1 的情况,不能直接返回 head。

Review

lawsofux
这个网站列了20条UX(用户体验)相关的定律,不得不说针对网站主题这个域名是真的好。网站上放的第一条定律是

Users often perceive aesthetically pleasing design as design that’s more usable.
用户通常认为美观的设计是更实用的设计。

这个网站做得还挺美观的,难怪这个要放第一条了。HackNews上的评论里提到里两本书,感觉可以进一步了解,豆瓣也可以搜索到翻译版本:

Tip

shell脚本中让 echo 显示不同颜色的字符
可以使用 ANSI 转义码:

Read more »

简介

WorkManager 官方介绍是说用于在应用退出或者设备重启后还可以被系统调度来执行后台任务的组件。
主要功能直接摘抄官方文档

  • 最高向后兼容到 API 14
    • 在运行 API 23 及以上级别的设备上使用 JobScheduler
    • 在运行 API 14-22 的设备上结合使用 BroadcastReceiver 和 AlarmManager
  • 添加网络可用性或充电状态等工作约束
  • 调度一次性或周期性异步任务
  • 监控和管理计划任务
  • 将任务链接起来
  • 确保任务执行,即使应用或设备重启也同样执行任务
  • 遵循低电耗模式等省电功能

简单使用

添加依赖

1
2
3
4
5
def work_version = "2.3.4"

// Kotlin + coroutines
implementation "androidx.work:work-runtime-ktx:$work_version"

WorkManager 对 lifecycle-service 和 sqlite-framework 有依赖,如果之前有引入需要注意版本。
三个步骤即可完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 1)创建后台任务
class UploadWorker(appContext: Context, workerParams: WorkerParameters)
: Worker(appContext, workerParams) {

override fun doWork(): Result {
// Do the work here--in this case, upload the images.
uploadImages()
// Indicate whether the task finished successfully with the Result
return Result.success()
}
}

// 2)配置运行任务的方式和时间
val uploadWorkRequest = OneTimeWorkRequestBuilder<UploadWorker>()
.build()

// 3)将任务提交给系统
WorkManager.getInstance(myContext).enqueue(uploadWorkRequest)

配置任务分为一次性和周期性工作。周期性最短间隔是 15 分钟,和 JobScheduler 相同。

工作约束

Read more »

Algorithm

井字游戏

有点类似五子棋,但是比五子棋简单,因为限定了和棋盘边长相等的个数。简单点就直接逐行、逐列、正反对角线判断一下是否全为某个玩家的棋子即可。也可以利用字符值来求和判断是否全部为某一种字符。

Review

Flutter vs React Native vs Native: Deep Performance Comparison

Flutter、RN 应该算目前比较主流的跨平台方案了,这篇文章从 FPS、CPU占用、内存占用 和 GPU 表现等多个方面进行了两者与原生平台的性能对比。
文中用了下面三个场景进行了测试:

  • 列表视图
  • 复杂动画
  • 更复杂的包含旋转,缩放和淡出效果的动画

从结果看原生性能肯定是最好的,动画比较复杂的时候 Flutter 和 RN 的 FPS 都下降得比较多。占用得内存也会比较多,不过 RN 在 CPU 占用上是三者中最高的。

结论就是从性能看的话,一般业务型的 APP 用三者区别不大,动画比较多的话还是原生性能好,CPU 占用高的情况下不推荐使用 RN。

Tip

Read more »

最近需要实现一个需求,让团队所有人在提交代码之前,能够自动检查提交中图片文件的大小,如果超过限制则需要提示并不能提交成功。这个需求听起来比较适合用 Git Hooks(中文叫钩子)来做,不过使用钩子的话需要每个人都在本地设置一次,有更新的话也比较麻烦。我们平时提交到 Gerrit 是使用的仓库中的一个脚本,所以采用了在 shell 脚本中实现这个功能。

获取修改文件列表

要实现这个功能首先就需要找到所有未入库的提交。可以使用 git cherry -v 命令获得。

1
2
3
4
$ git cherry -v 
+ e861d61861a8ce0307cdb2f985a42492a70dcee2 ADD: test1
+ a70592c6e47d4d314efdd53a430b433cee4f3cb8 ADD: test2
+ df5a0bfa22ab009ee7d84feb196f7e01bd464295 MOD: del file

那如何从中获得所有修改的文件列表呢?可以使用 git diff 命令,这里可以对比相对当前 HEAD 的前 N 个提交的文件修改记录,前面为 D 的表示删除,我们不需要关注。利用上面得到的提交个数,我们就可以得到未入库的所有提交的文件修改列表。

1
2
3
4
5
6
$ git diff --name-status HEAD~3 HEAD 
M app/src/main/java/.../abc.kt
M app/src/main/java/.../def.kt
A app/src/main/res/drawable-xxhdpi/xyz.png
A app/src/main/res/drawable-xxhdpi/wallpaper01.jpg
D app/src/main/res/drawable/aaa.png

这里获取修改的提交数目有个坑,直接用 wc 输出行数的话,输出结果前有空格,导致后面用的时候出错,最后是用 sed 处理了下:

1
2
3
4
5
6
7
HEAD~ 3 
fatal: ambiguous argument '3': unknown revision or path not in the working tree.

$ git cherry -v | wc -l
3
$ git cherry -v | wc -l | sed 's/ //g'
3

处理文件列表

怎么从上面列表中获取到非删除的图片文件列表呢, grep 大法好,使用 -v 参数即可输出不包含匹配文本的所有行。再使用后缀过滤,即可找出所有添加或者修改的图片文件。

Read more »

Algorithm

68. 文本左右对齐

这个是下面 Review 中提到地一个实用算法地例子,题目不算太难,但是需要处理地细节比较多。循环列表进行判断,如果是最后一个单词,或者加上下一个词就超过长度了,即可凑成一行,进行额外空格的相关处理。

Review

Data Structures & Algorithms I Actually Used Working at Tech Companies

这篇文章先介绍了作者工作中用到的数据结构和算法,例如图、哈希结构、栈和队列,加密以及决策树等。

作者觉得应该掌握基础算法和哈希表、队列和栈等数据结构。其他特定的高级算法和红黑树等则不需要靠记忆,需要的时候能想到使用这些就行。尽可能地实用,有相应地使用场景。

数据结构和算法应该被看作是一个工具集,应该熟练掌握并在构建软件地时候有信心地使用。

Tip

客户端获取机器型号一般都是从 Build.MODEL 中获取,但是这里的型号一般都并不是市场上流行的名称,例如华为 P30 Pro 获取到的 model 可能是 HW-02L。

Read more »

简介

Lifecycles 是 Google 推出得架构组件中的一个,架构组件又是 Android Jetpack 的一部分。Jetpack 组件是库的集合,可帮助开发者更轻松地编写优质应用。这些组件可帮助您遵循最法、让您摆脱编写样板代码的工作并简化复杂任务,以便您将精力集中放在所需的代码上。

Lifecycles 的作用是什么?以传统的 MVP 模式为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MyPresenter() {
fun create() //do something
fun destroy() //do something
}

class MyActivity: AppCompatActivity {
private val presenter = MyPresenter()

fun onCreate(...) {
super.onCreate(...)
presenter.create()
}

fun onDestroy() {
super.onDestroy()
presenter.destory()
}
}

我们需要在 Activity 创建和销毁的时候做一些处理,所以需要在 Activity 对应的生命周期里去手动调用 Presenter 对应的方法。如果这时候我们想在 Activity 里使用 EventBus,则需要加上类似如下的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MyActivity: AppCompatActivity() {    
private val presenter = MyPresenter()

fun onCreate(...) {
super.onCreate(...)
presenter.create()
}

fun onStart() {
super.onStart()
EventBus.getDefault().register(this)
}

fun onDestroy() {
super.onDestroy()
EventBus.getDefault().unregister(this)
presenter.destory()
}
}

如此每个和 Acitivty 生命周期相关的逻辑都集中到了 Activity 中,变得不好维护。有没有办法让外部的代码主动感知生命周期的变化呢? Lifecycle 的诞生就是为了实现这个目的。

使用

早期是在 android.arch.lifecycle 包名下,后面跟随迁移到 androidx.lifecycle 包名下了。support 库低于 26.1 得话需要单独引库,高于的已经包含在 support 包中了。对于 AndroidX 项目,需要按下面的方式引入,一般会搭配 ViewModel 和 LiveData 使用。使用和查看文档的时候需要看下版本说明,例如在 2.2.0 版本就弃用了之前提供的 lifecycle-extensions 库,被新的几个面向特定功能的库替代了。

1
2
3
4
5
6
7
8
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
// ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
// LiveData
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
// 其他可选库
...

Read more »

Algorithm

42. 接雨水

这道题是困难难度。官方提供了暴力、动态规划、栈和双指针四种解法。暴力法是对于数组中的每个元素,我们找出下雨后水能达到的最高位置,等于两边最大高度的较小值减去当前高度的值。后面三种是在暴力法的基础上进行的优化。

Review

How To Understand Things

  1. 作者在青少年时期发现一些最聪明的人在证明一个理论或者解决一个问题后会回头继续看这个问题并尝试去找不同的证明,而大多数人则会在“获得答案”后停止思考。聪明人的“硬件”并不相同,有的人思考、计算或者阅读很快,而有的聪明人却并不快。而聪明人的“软件”则有相同之处,并且可以通过努力学习获得。一个人的智力并不是固定不变的。

  2. 上面提到的“不满足现有的答案”需要付出很多努力去思考,需要耗费精力。这样做需要真正的动力,诺贝尔奖得主 William Shockley 谈过 “the will to think”,这是他从物理学家费米那学到的一个终生难忘的短语。除非坚信某些事情值得努力去完成,否则很难有 “the will to think”。

    [Fermi] distilled the essence of a very significant insight: A competent thinker will be reluctant to commit himself to the effort that tedious and precise thinking demands – he will lack ‘the will to think’ – unless he has the conviction that something worthwhile will be done with the results of his efforts.

    有动力花费这么多精力,意味着不理解某些事情或者在思考中存在 bug 会让你很困扰,这个时候就会有 “the will to know”。与之相关的特质就是诚实。费曼曾说过做科学的第一条规则就是不要糊弄自己,因为自己是最好糊弄的那个人。

    (这就是为什么写作很重要。当你脑子里满是杂乱和困惑的时候,坐下来写东西就很难糊弄自己说自己已经弄懂了。写作能够强迫你把这些弄清楚。)

  3. 深入理解事情通常是和我们的物理直觉深刻联系在一起的。法拉第不相信他不能通过实验验证的东西。同时期的法国科学家(例如安培)能够在更高的抽象层面理解事情。相比之下法拉第部分因为数学不太好,因此主要通过实验来理解事情,但是法拉第的物理直觉使他获得了科学界的一些最重要的发现。即使不是真正地做实验,也能产生具体地案例地能力也非常重要。通过视觉化一些抽象的过程能让人理解更加深刻。
    另外一个聪明人的特质是不怕看起来很蠢。看起来蠢需要勇气,作者举例说有时候提出一些基本问题的时候,会有拖慢整个团队的负罪感,结果却是发现没有人知道这些问题的答案,而且有人私下说很高兴他问了这些问题。这是个比较容易掌握的习惯,会让你变得更聪明。

  4. 作者举例上学学习微积分的时候被不充分的证明激怒了,但是时间所迫只能学习下公式和运算方法,完成考试然后开始下一部分的学习。感叹学校是在杀死人们心中的“will to understanding” 。作者的建议是“慢下来”,慢慢地读、慢慢地思考,一周或者一个月后会非常惊讶已经走得有多远。

    如此你将会形成一个思想上的框架,阅读学到得知识可以挂在上面,会更有可能理解和记住这些知识。以比尔·盖茨得阅读周为例,他围绕一些重要问题例如水危机为大纲分解出很多碎片问题,例如“世界上有多少水?”、“现有的饮用水来自哪里?”、“如何把海水变成饮用水?”等等,然后才开始阅读去解决这些问题。

  5. 作者推荐了一本电子书:Sequences,特别推荐了 Noticing Confusion 这个章节。

  6. 两个故事

    • 关于阿加西让学生描述一条鱼的一个寓言
    • 《禅与摩托车维修艺术》中的一个学生想不出可表达的东西的故事,这里包含了这个故事

    两个故事说明的道理:没有什么比直接经验更重。即使不能直接经历,尝试找一些包含细节和事实的高信息密集的来源,然后从这些事实中进行推理。被誉为“最伟大的战地摄影师罗伯特·卡帕对新人摄影师的建议是:“如果你的照片拍的不够好,那是因为你靠的不够近”。(这对写小说也是一样)

    这对理解事情也是一样的好建议,当有疑问的时候,靠得更近一些。

Tip

属性动画中平移是相对于控件最初最初位置的一个距离。下面这篇文章讲解得比较好:

【Android】使用属性动画碰到的困惑及讲解

Share

Read more »

ProGuard

ProGuard 是一个免费的 Java 字节码的压缩,优化,混淆器。它删除没有用的类,字段,方法与属性。使字节码最大程度地优化,使用简短且无意义的名字来重命名类、字段和方法。AndroidStudio 在 3.4.0 版本之前默认使用 ProGuard 来优化字节码。开启方法大家应该也比较熟悉,在 build.gradle 中添加 minifyEnabled true 即可,一个新建项目的配置文件看起来是这样的:

1
2
3
4
5
6
7
8
9
10
11
android {
...
buildTypes {
release {
shrinkResources true
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-rules.pro'
}
}
}

SDK 里有'proguard-android.txtproguard-android-optimize.txt这两个默认的配置文件,区别是后者内有优化相关配置。proguard-rules.pro 则是项目中的一个空文件,可以配置自己的混淆和优化规则。

D8

相比 ProGuard,D8 可能很多人都不太熟悉,因为不需要直接和它打交道。了解 D8 要先从 Dalvik 虚拟机说起,早期的移动设备电池容量、内存的大小有限,存储空间也很有限,所以 Google 没有使用标准的 Java 字节码和虚拟机,而是采用了 Dex 格式和 Dalvik 虚拟机。Java 字节码是以栈为基础的(所有的变量都存储在栈中),而 Dex 格式的字节码是是寄存器为基础的(所有的变量都存储在寄存器中)。后者更加高效并且需要更少的空间。运行 Dex 字节码的 Android 虚拟机被称为 Dalvik。后来 Google 又推出了 ART 虚拟机。ART 与 Dalvik 的主要区别是,它不是在运行时进行解释和 JIT 编译,而是直接运行的提前编译好的 .oat 文件,因此获得了更好更快的运行速度,ART 虚拟机依旧运行 Dex 代码。

Dex 字节码的方案看起来不错,但是像硬币都有两面,Dex 方案的问题在于基本只能完整地支持Java6 SE,Java 后续新版本引入的语言特性并不能直接就能用在 Android 开发中。为了让我们能使用上 Java 8 的特性,Google 使用了 Transformation 来增加了一步编译过程叫 desugaring(脱糖),就是将我们代码里使用的 Java 8 新特性翻译为 Dalvik/ART 能够识别的 Java 6 字节码,但是这也导致一个新的问题:编译花费的时间变得更长。

desugaring

D8 是一种命令行工具,Android Studio 和 Android Gradle 插件使用该工具来将项目的 Java 字节码编译为在 Android 设备上运行的 DEX 字节码,该工具支持您在应用的代码中使用 Java 8 语言功能。

D8 还作为独立工具纳入了 Android 构建工具 28.0.1 及更高版本中:android_sdk/build-tools/version/

Read more »

Algorithm

17. 电话号码的字母组合(中等)

初看还是比较简单,就是排列组合,可以先用 for 循环实现两个数组之间的组合结果,然后拿前面合并的结果和下一个进行组合,如此即可。有没有更巧妙地办法呢,看了一个用迭代做地题解,感觉还比较简洁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public List<String> letterCombinations(String digits) {
LinkedList<String> ans = new LinkedList<String>();
if(digits.isEmpty()) return ans;
String[] mapping = new String[] {"0", "1", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
ans.add("");
for(int i =0; i<digits.length();i++){
int x = Character.getNumericValue(digits.charAt(i));
while(ans.peek().length()==i){ //查看队首元素
String t = ans.remove(); //队首元素出队
for(char s : mapping[x].toCharArray())
ans.add(t+s);
}
}
return ans;
}

Review

The forgotten art of construction

这篇文章主要讲了构造函数的一些最佳实践。开头调侃了下现代程序员写的代码,说二十年前的程序员来看现在程序员写的代码可能会被震惊到。

对于如何优化构造函数参数,提出了几点:

  • 单一职责

    如果构造函数需要十个参数,那有很高的几率你的类做了不止一件事情。那多少个参数合适呢,作者引经据典说明少于4个是合适的。另外通过建造者模式来减少构造函数参数数量并不能真正地减少类的复杂性。

  • 提取共同的依赖关系

    如果有两个同类的参数,比如两个不同的 scheduler 可以合并为一个:

    1
    2
    3
    4
    5
    6
    7
    8
    @MainThreadScheduler private val mainScheduler: Scheduler,
    @IOScheduler private val ioScheduler: Scheduler,

    interface RxSchedulers {
    val io: Scheduler
    val computation: Scheduler
    val main: Scheduler
    }
  • 外观模式

    有些情况简单地把多个参数包装到一个类里并不适用,可以把相关的参数放到一个外观类中,只暴露出需要的相关方法。

  • 通过接口

    一个类可以继承多个接口,可以通过接口来实现外观模式。

  • 用例模式

    像 Presenters 或者 ViewModels,会比较复杂,包含多个 repositories。直接把多个 repositories 合并成一个不太合适,可以通过用例模式组合成一个类。这里其实也就是组合优于继承的一个体现。

    1
    2
    3
    4
    5
    class ProfileUseCase(
    private val userRepository: UserRepository,
    private val tracksRepository: TracksRepository,
    private val playlistRepository: PlaylistRepository,
    )
  • 拆分

    有时候构造函数参数直接没什么关系,这时候应该回过头来看这个类的职责并开始拆分。基于同一个原因改动的代码应该放在一起,把基于不同原因改动的代码分开。

Tip

Read more »