记录一下在高中三年和在大学一年的 Coding 经历

只是在这里记录一下过去的自己,文笔不好,请见谅

文中讲解技术的部分会比较多,看起来像流水账一样又臭又长

我第一次正经的接触编程,应该是初中毕业后的暑假, 在这之前,我只写过易安卓,用中文来编程,写起来挺像汇编的。

我当时因为接触过易安卓,后来开始写正经的软件延续了客户端这个方向,下载了 Android Studio。

当时 Kotlin 还不算很流行,所以我的首选语言是 Java。与其说是写,倒不如说是到处 CV 别人的代码,复制进来,点绿三角,能跑起来就是成功。

内存泄漏、软件架构、设计模式什么的,也不懂。当时也没有 AI,所以只能到处翻博客,翻搜索引擎,找别人的教程然后把代码复制进来,修修改改,最后能跑就行。当时连读写文件都不懂,就上网搜索“android txt 文件读写”这种关键词,然后点进去复制代码。

我当时的学习路线是,先写了,能跑,然后再去看书,结合书里面的讲解,然后豁然开朗,这个词用的没错。

当时看的应该是明日科技的 Java 入门到精通,其实这本书也就一个语法+入门的水平,不过我当时还是研究了这本书很久。

当时看到反射和 Lambda 函数,真的是一点都看不懂,也不理解为什么要有反射这种东西,直接调用不就好了吗?还有 Lambda,包括什么匿名内部类啦,当时实在是没搞懂它的定义是什么。

后来,在写项目的时候,写了一句 v.setOnClickListener(new View.OnClickListener() { /** 这里省略一部分 **/ }),但是 ide 在这段代码下面画了一条黄色波浪线,我把鼠标凑过去,然后点了几下按钮,一大串代码变成了 v.setOnClickListene((v) -> xxxx),才发现 Lambda 就是 () -> {} ,可以替代一段很长写法。然后后面才了解到原先的写法是匿名内部类。

反射也是,后来学到 Gson 这种依赖反射的序列化工具,才知道反射有什么用和怎么用。

在看了明日科技的那本 Java 书之后,我又看了欧阳燊先生的《Android Studio开发实战:从零基础到App上线》

书是针对 Android 10 来讲的,不过当时好像已经都有了 Android 11 了。

书里的讲解的技术栈其实我已经记不太清了,因为看的次数太少了。

我是那种,有需求才去学东西的人,如果单纯为了学而学,我不太能记得住。

初中毕业的那个暑假,我用蹩脚的 Java,写出了我第一个安卓应用,然后上传到酷安(一个数码交流平台&应用商店),当时上了平台的热门,然后也吸引了很多人去用这个工具,然后下载量估计有2w左右,当时成就感还是满满的。

后来用户有需求,需要新的功能,然后我就去研究。不懂原理,然后就去看别人的博客,研究对应的实现,研究要用到的工具,然后就往软件里面塞。

后来有些实现只有某个开源项目有,然后我就去翻源码。那项目真的很庞大,架构也很厉害,是我现在也学不来的,我第一次的开源贡献就是给了这个开源项目。

后来有 B 站的一些 up 主自发的帮我做宣传,我记得最高的一个视频的播放量有 30w,真的很感谢这些朋友帮我做宣传(还不收钱)。

但实际上这个软件以我现在的眼光来看就是一坨构思,能跑真的是个奇迹,例如我当时还不知道线程池是啥,所以静态定义了一堆 Thread,如果想要跑一个新的 Thread 就使用 new Thread(oldThread).start(),然后再运行。

后来想过重构,但是写到一半就放弃了,感觉自己的能力不足以写去完善。

上高一的时候,我选择了一个最为常见的练手项目--一个音乐播放器

我花了 5 天的时间,学了一些新的技术,Service(是的,我当时还不会写 Service,现学的)、MediaSession、Jaudiotagger、MediaPlayer、 RecyclerView、 Sqlite,然后把软件的原型给搓了出来。

很简陋的一个壳子,能播放音乐,能看歌词,能看播放列表,还能用蓝牙耳机切歌。

然后我就把这个 demo 录制视频发到网上了,寥寥几千播放。

后来我一直在维护这个项目,慢慢给软件加新的功能:歌单,歌手,专辑的分类;使用 MediaStore 加快扫描速度;定时播放...

在这个过程中也吸引了不少的用户,也认识了不少朋友。

不过到项目后来堆成屎山了:

  • 糟糕的 MVC 架构设计
  • 糟糕的手写 sql
  • 糟糕的代码风格
  • 糟糕的实现
  • 代码高耦合度

总之一切都很糟糕,写出来的代码真的没眼看。

后来我学到了 MVVM 这种概念,不过这个概念似乎很早就提出了,不过我一直都不知道。

后来我用新的技术栈重构了整个项目,不过这些都是后话了,因为这个时候我开始写另外一个自己的开源项目。

高二的时候,我阅读了郭霖的《Android 第一行代码 第三版》,里面较为详细的讲解了 Kotlin 的语法,我就是从这里开始了解 Kotlin 的。

书上讲的东西我个人认为非常不错,因为它教 Kotlin 不是一次性教完,而是每个大章节的最后会穿插一部分 Kotlin 语法。

我的 Kotlin 之旅就是从这本书开始的,当时还记得讲了 Context 和 Activity,章节结尾的时候,教了如何用 Kotlin 的 reified 关键字去封装一个高阶函数,函数签名是这样的:

inline fun <reified T> Context.start()

书里还讲了 Kotlin 协程,一次性可以开启多个 Coroutine 却不会 OOM,这让我初步了解协程这个概念。

协程真是个好东西吧,一开始写项目的时候,还是用 Thread + Handler 来做多线程异步编程,或者使用 Okhttp 自带的线程池加回调接口。

用协程的话,就可以使用 suspend fun 就可以告别回调地狱,即使你只提供回调接口,也同样可以使用 suspendCancellableCoroutine 把回调结果直接转换成协程的返回值;

还能使用 withContext 切换协程的上下文,指定代码在主线程还是在IO线程去执行,而在没有协程的时候通常是使用 Activity.runOnUiThread 或者是 View.post 去简单的切换到主线程。

不过我对 Coroutine 的内部实现就不是很了解了,仅限于能用的水平。

当时我 Java 转 Kotlin 的时候,会写错非常多东西,例如在 Java 中定义数组的方式是 int[],而在 Kotlin 是 IntArray,两种的写法是不同的;

这里可以讲讲 Kotlin 的数组设计。Java 里定义数组很简单,就是类型后面加 [],像 int[]。到了 Kotlin 就变成了两种:Array<Int> 和 IntArray。

一开始我认为这俩写法差不多,后来才发现,Array<Int> 对应的是 Java 的 Integer[](包装类型),而 IntArray 才对应真正的 int[](基础类型)。

Kotlin 编译器并不会自作主张地替你优化这个选择:你写 Array<Int>,它就是对象数组;你写 IntArray,它才是基本类型数组。尽管在局部运算时,Kotlin 的 Int 变量在字节码层面会被编译为 int,但数组容器本身以什么形式存在,完全由开发者决定。

用 Array<Int>,每次读写都伴随装箱和拆箱;用 IntArray,则直接操作 int[],避免了这些开销。

除了数组以外,还有 lambda 的写法也是不同的,我第一次的写的时候还是用 (v) -> {} 这样去写,但 Kotlin 里面明显没有那么麻烦,而是 { v -> } 就解决了

然后也学到了一些框架 Jetpack Room,Retrofit的使用。

Room 中用 @Entity 定义数据表,@Database 声明数据库并关联实体,@Dao 则通过接口来注册数据访问对象。编写 Dao 时同样采用注解加接口的方式,通过 @Query 标记 SQL 语句,方法可以直接定义为 suspend 函数,返回值也能自由选择,比如用 Flow<R> 来观察数据变化。

Retrofit 也是用注解驱动,通过 @GET@POST 等标注接口方法,结合 interface 来声明网络 API。它同样支持 suspend 函数,返回值可以直接写为数据类,也支持 Flow 等响应式类型。

虽然 Room 和 Retrofit 都是通过注解来驱动整个框架的运转,不过它们的实现思路不太一样:Room 依赖编译时的符号处理器(如 KSP)生成模版代码,而 Retrofit 则是在运行时通过动态代理来解析注解。这种框架的注解的方式让接口定义极简,省去了自己封装的过程。

书上还写了如何实现一个 MVVM 架构,用到了 LiveData, DataBinding,ViewModel。

不过这个部分其实看起来特别晦涩难懂,一开始真的不理解为啥要使用 LiveData 和 ViewModel,好像时候只提高了代码的复杂度,直接赋值给对应的 View 或者使用一个变量去保存数据不行吗?

但是随着慢慢理解,开始了解为什么要这样设计了:MVVM 在设计上的核心目的就是为了将 View 和 Model 解耦,避免 View 层的职责过重,将业务逻辑部分从 View 中抽出来到 ViewModel 中保存,View 只负责显示 ViewModel 中的数据,本身不负责对数据的修改。

另外,因为 Android 的设计,在旋转屏幕或内存过低时会触发 Activity 的重建,如果数据直接存在 Activity 里,页面状态就会完全丢失。如果使用了 ViewModel,它在 Activity 重建期间依然存活,因为它的生命周期是覆盖整个 Activity 的,直到 Activity 真正销毁。这样一来,就可以通过 ViewModel 中的 LiveData 快速恢复页面状态。这正是 ViewModel 的职责所在——负责页面状态和逻辑封装。

而 DataBinding 则是一种增强版本的 ViewBinding。

一开始的开发是 setContentView + findViewById,然后将获取到的 View 放在变量里面,这就是最基础的实现。

后来出现了 ViewBinding 和 Kotlin Android Extensions(KAE),但由于 KAE 的 bug 很多,而且在 RecyclerView 中使用时会导致内存泄漏,这里就不展开介绍了。

ViewBinding 提供了一种类型安全的方式来访问对应的子 View。它的内部封装其实仍然是 XML 布局 + findViewById 这一套,只不过编译器把这个过程包装了起来,帮你减少了模板代码量。

而 DataBinding 在 ViewBinding 的基础上,支持在 XML 里直接编写部分表达式逻辑,让布局文件可以引用数据对象。

这就很灵活了,你可以在布局中用 <variable> 声明变量,然后在 View 属性里通过 @{} 表达式直接绑定,比如 android:text="@{user.name}",这样你不需要手动获取 View,再给它赋值。

编译器生成的 Binding 类不仅提供了 setVariable 接口,还有类型安全的 setter,让你可以像 binding.user = viewModel.user 这样直接给 XML 赋值。

而且配合 LiveData,数据变化时界面会自动更新,免去手动 setText。它还支持双向绑定 @={},比如 EditText 的输入能实时回写到 ViewModel 的字段,大幅简化交互代码。

同时,你还可以用 Lambda 表达式(如 () -> xxx())直接在 XML 层完成 OnClickListener 的绑定。

在使用 DataBinding 的过程中,你完全不需要去编写 setXxx 这类方法给 View 设置状态,也不用关心 View 的获取。只需要专注于 ViewModel 中的逻辑编写,然后在 Binding 里通过 XML 绑定即可。

而且还有 @BindingAdapter 可以实现自己的属性定义,在这里就不多讲解了。

在网上对 DataBinding 的看法分为两派,一派是认为在 xml 中写逻辑很蠢,一派是认为 DataBinding 大量简化了代码;我在这里不过多评判。

可惜的是现在,Google 不再维护 DataBinding 了,现在只支持 Kapt 而不支持 Ksp。

后面又学到了 Flow 这种概念,一开始我是不理解为什么要用 Flow 这种东西的,因为我觉得 LiveData 本身就够用了。

我了解到 LiveData 存在所谓“数据倒灌”的问题。但在我看来,LiveData 的设计初衷本身就是用来持有页面状态的。Activity 在因配置变更重建后,会重新从 LiveData 获取数据,这恰恰是它的核心能力。如果设计意图不是这样,那要 LiveData 做什么呢?所谓的“数据倒灌”,其实就是 LiveData 在新的观察者注册时,会立刻把当前持有的数据发送给该观察者——这本质上正是它状态持有特性的一种体现,而不是缺陷。

在后来要设计一些复杂的架构的时候,我发现 LiveData 的作用十分有限,因为它的核心能力是“状态持有 + 重建恢复”,但也正因为与 Lifecycle 强绑定,一旦遇到稍微复杂的需求,就很容易掉进“数据倒灌”的坑里。

LiveData 很难承载事件,因为它天生带有粘性、会保留状态;而 Flow 中直接选用 SharedFlow(replay=0) 或者 Channel 来实现事件队列。

同时在复杂架构中,数据往往需要脱离 UI 生命周期独立运作;Flow 只依赖 Kotlin 协程,不和 Android 平台强绑定,因此可以自由地放到 Repository 甚至更底层。

并且 LiveData 的可操作性太差了,只有最基本的 map、switchMap;Flow 则提供了丰富的操作符,通常可以像搭管道一样把复杂的异步逻辑组装得简洁清晰,这在 LiveData 里要么做不到,要么要绕很大弯子。

在现在的 Android 开发中, 用 Flow 完全替代 LiveData 不但在技术上是可行的,在实际项目里也已经是很成熟的实践了。 StateFlow 既保留了“状态持有 + 重建恢复”的核心能力,也同时被 DataBinding 支持,用法和 LiveData 接近。SharedFlow 则可以灵活配置重放次数,天然胜任一次性事件的传递,从根源上解决了“数据倒灌”问题。

从 LiveData 过渡到 Flow,其实就是从“刚好够用的专用工具”切换到“可以长期复用的通用语言”。

后来面试的时候还问到 LiveData 和 Flow 有什么根本上的区别,一时之间还答不上来。。。

。。。

高三的时候,大部分时间都在学习课堂上的知识,没什么时间去开发项目,不过我一直有关注客户端的新开发框架和软件架构。

当时我会刷稀土掘金,然后了解到了 MVI 架构。MVI 强调单一数据流,和之前 MVVM 的双向绑定数据流不同。

在一个 ViewModel 中,只用一个 State 和一个 Event 来承载所有数据和事件:通常用 data class 封装状态,用户的意图则用 sealed class 封装成 Intent。

因为 sealed class 会在编译时强制检查 when 分支是否穷举了所有子类,这样就不会遗漏对任何 Intent 的处理。这种写法替代了原有的接口式写法,感觉会更直观一些吧。

不过我并没有过多的实践这个架构,因为我还在以 DataBinding + MVVM 作为我的主要架构来使用,所以只是稍微了解一些 MVI 的理念。

当时还买了本李文塔的《Go语言核心编程》,偶尔会看看 Go 的语***感觉 Go 的语法对于 Kotlin 来说异常的简洁了,没有那么多的语法糖。

但由于只是纸上谈兵,所以一直没有真正学习这门语言,只觉得里面的概念会很难学,例如 Duck 类型, Goroutine,扇入扇出。

不过后来一直没什么机会写 Golang,因为那时候写后端先用 Node.js + Express 框架来凑合,到后来懒得改这部分逻辑了,就一直搁置这个计划。

高考完之后,就开始重构过去写的软件。

我主要在重构一开始的音乐播放器。我大概是整个暑假都在写代码,只有中途去了一趟珠海找朋友玩,然后去澳门逛了一圈,当时流行的是 Cursor 吧,我在看我朋友用 Cursor 写 Golang 后端,这算是我第一次了解 Vibe Coding。不过我穷,玩不起 Cursor,依旧手搓。

当时又接触到一个新概念:依赖注入。

说实话,其实这个概念我在 《Android 第一行代码》的时候就看过了,但是当时对这个概念还是摸不着头脑,感觉太抽象了。后来认识了一位很有经验的客户端前辈,向他请教了这方面的问题。

所谓依赖注入的核心,就是把一个类所需要的依赖(比如某个对象)从外部传进来,而不是在类的内部自己创建。把“创建对象”的控制权交给外部,这就是所谓的控制反转。这样做的好处是能让类之间解耦,方便替换实现,也更利于做单元测试。

前辈还告诉我,其实 Android 中就有这种实现,比如 Activity 本身就是一种控制反转的体现。我思索一下,还真是。

理解了这一点之后,我很快就掌握了一种新的开发方式:先定义 API Interface,再编写对应的 Impl 实现类,最后通过 Koin 或 Hilt 这类框架来注入。

这种写法真的是非常爽,以至于后面去实习的时候写到的项目是没有依赖注入的就难受的很。

后来我把整个播放器项目以较为现代化的架构重构了一遍:DataBinding + Coroutine + Flow + Jetpack Room + ExoPlayer + MediaPlayer + Koin

后面写成的项目其实已经很庞大了,现在的情况是整个 Gradle 项目有大概 10 个左右的 module。

当时需要做多播放器内核实现,于是复用了之前所说的 API - IMPL 的设计思路,先将 ExoPlayer 和 MediaPlayer 的对应的接口整理出来,取其共同点,然后编写对应的 Interface,最后写出两个 Impl 实现。

。。。这里不知道写什么好了,感觉写不了什么,先空着,以后有机会再写

在掘金上面偶尔会零零碎碎的刷几个 Android 的八股,偶尔看看。

其实我当时对 Android 的招聘市场还不太了解,也没有主动的去看过。

除了做这个播放器项目,我还在 Github 上面维护了自己的一个开源项目,到现在也有了 300+ star 了

整个项目也是比较庞大的。在这个项目中我也学了不少的东西。

例如在 Maven Central 发布自己的类库;学习了如何编写自己的 Gradle Plugin;学会了使用 KSP + Kotlin Poet 来实现根据注解来动态生成代码的实现;学会了 Git 的使用。。。

每一次重构项目的时候,都可以学到很多新的框架,新的开发思路:

一开始我使用的数据持久化方案是 Jetpack Room + SharedPreferences。后来了解到用 SharedPreferences 存储账户数据不太安全,于是接触到了 EncryptedSharedPreferences,但这个库已经被废弃了。没办法,我开始寻找其他方案。

之后我了解到,AndroidKeyStore 配合 Jetpack DataStore 可以实现比 EncryptedSharedPreferences 更好的效果。

关于 AndroidKeyStore,顾名思义,看起来像是 Android 提供的一个密钥仓库,需要的时候就从里面取出密钥来加解密数据。但实际上,它更像一个依托安全硬件的加解密服务:它从不暴露任何 Key 的明文,而是通过 alias 来区分不同的密钥,密钥本身只存储在设备的安全硬件中。说人话就是:AndroidKeyStore 负责在安全环境里生成密钥并提供加解密服务,但你永远拿不到密钥的明文。

另外,SharedPreferences 本身就像一个键值对注册表,使用起来非常难受,这意味着如果你想把数据映射成 Preference item,必须手动写一套转换逻辑。而 DataStore 就灵活得多,它提供了另一种方案:DataStore 本身并不负责序列化和反序列化,而是把这一过程完全交给开发者控制。你可以自由选择 Protobuf、JSON、YAML 甚至 XML 等任意序列化方案。正因如此,在读写数据流时你完全可以插入一层加解密处理,配合 AndroidKeyStore 就能实现安全的本地持久化。

后来我后续的持久化方案都换成 DataStore + Room 了。

在开发这个项目的过程中,遇到了一个 Ktor 的 Bug,我随后提交了一个 PR 修复了该问题,算是第一次给这种大型的开源项目做贡献。

Kotlin 多平台实践,这部分可能没什么好讲的,主要是 Kotlin Multiplatform 的使用,仅限于会用。

进入大学后,我阴差阳错的加入了学校的一个实验室。实验室里面的师兄大部分是做前端和后端开发的,专门写客户端的只有我一个人。

在实验室里面,我遇到了很多很厉害的师兄,他们同时也在牛客很活跃,其中一个你们应该听说过。师兄给我分享了不少经验,他叫我要学好数据结构、计算机系统和计算机网络;也给我讲了投递简历的技巧,可以通过 Boss 直聘或者企业官网投递简历。

于是我便开始刷 Hot100,其实刷的还挺痛苦的,因为之前根本没有写算法的经验,我的数据结构基础非常非常的烂,我当时连 Array 和 List 的区别是啥都不知道。

不过好在写代码的经验不少,我很快地把大部分的数据结构过了一遍:数组,链表,哈希表,树,队列之类的最基础的数据结构过了一遍。然后 Hot100 也能做一半了。

当时应该是 11 月份左右,学校那边组织参加 CCF 举办的第二届 CACC 区域赛。当时距离比赛就只有十几天,实验室老师临时组了一个队伍去参赛。当时也没啥时间去准备,就直接毫无准备的去参加比赛了。激烈的比赛过后,我意外的进总决赛了。不过很可惜的是没有同行的同学和朋友一起参加总决赛。

我记得当时还打算参加字节跳动那边举办的一个工程训练营,选的客户端方向。不过真的很可惜,AC 两道算法题目也没能入选,估计是学校咖位太低了。

然后就是一些实验室自习日常,这段没啥好写的,略过。

放寒假了,实验室的师兄过来催我,写一份简历,然后投递一下简历练手。

我当天晚上把简历的初版给敲了出来(其实是之前工程训练营的那份,报名的时候也要交一份简历),把自己的学会的技术栈啊什么的简单的写了一份出来,顺手包了一份实习经历。将写出来的简历给师兄简单的阅览了一下,第二天就开始上 Boss 直聘投递了。

一开始投的几个,要么就是已读不回,要么就是拿完简历拍拍屁股走人,一个约面的都没有。后来运气好点,终于有一家杭州中厂 HR 看完我的简历后,要了我的微信,她在微信上和我约定了面试时间,是周一的下午。

我开始筹备这次第一次面试。其实也没啥好筹备的,因为是第一次面试,最多就是看看别人的面筋啥的。

后来经过半个小时的拷打,技术面算是通过了。第二面约在第二天,其实这个时候差不多就敲定了,第二面主要就是介绍自己的一些项目经验啥的,10分钟就结束了。几天后就可以发 offer 了。

本身这家公司我是没打算去的,因为我人其实在广东,去杭州那边实习是不方便的。公司也考虑到了这一点,给我改成了远程实习,问我接受不接受远程实习,这是好事啊

然后就开始进入远程打工状态。

关于面试和实习的一些事情可以看一下我之前的牛客动态。

今年 4 月份,启程去浙江宁波参加第二届 CACC 的总决赛。主要是主办方提供一千块钱的交通补贴,不然的话其实根本去不起。单单机票就花费了 1300 左右。

比赛是分 3 天举行,第一天就是纯算法。还是那句话,我数据结构学的太烂了,700 分的算法题为估计只拿了不到 100 分,算法能力太差了。

不过第二天是两个赛道:安全赛道和 AI 赛道。AI 赛道主要是用 pytorch 来训练模型吧,我没怎么细看,我的 python 学的很烂。

安全赛道则是和区块链相关的技术有关,题目为记不太清了,但是最后 500 分我拿了 470,算是个比较好的结果。

不过比赛的结果自然是没能拿奖,还是那句话,我算法太烂了。

现在的话,我一边远程实习混点金币,另一边正在筹备自己的一个商业项目。

目前阶段,把软著 + 软件ICP备案 + 网站ICP备案整好了。在这里感谢一下我的一位朋友,是他支持了我的项目开发。

目前,除了继续学 Kotlin 与客户端开发,还重新学了 Go 语言的语法,其主流框架和相关中间件,写我的项目的后端服务。

后面除了继续写自己的项目,可能还会去别的公司实习?先观望一会。

最后的最后,如果 AI 真的发展到了,人不用学代码的地步,那么去跑跑外卖也不是不行,做酱香饼也可以,看一步走一步。

不过在那之前,还是继续学习和 Coding 吧。

上一张 Github Stats

#大一新生学习方向##个人学术成果##实习##Kotlin##学习笔记##过去的故事##经历体会##成长记录#

全部评论

相关推荐

05-20 18:38
北京大学 Java
_小趴菜_:北京大学加大加粗就够了
点赞 评论 收藏
分享
评论
6
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务