从零开始Android商业项目Vibe coding完全指南(十一)

20261015

读者应具备的知识基础

Kotlin、 Compose、 基础Android开发(谷歌官网课程优先)、网络编程基础、文件解析基础(可参阅《Java编程的逻辑》相关章节)

可以预见的AI能力边界

上节末尾的结语可能触动了"拜AI神教"的信徒,有朋友发消息给我表示我实在太猖狂了,AI分分钟把我秒到渣都不剩!

如果真有这么一天,那么我将毫不意外,十分可惜的是不是今天,甚至不会是今年、明年、后年。

既然如此,我们还是做一点科普吧。

我在之前章节早就已经写过,AI不过是件工具,工具在某些方面超越人类并非稀罕事,这就好比计算器发明之后,人类心算再快也无法受到周围人的景仰了,仅此而已。

现在的怪现象在于,有许多没有理工思维的人,莫名其妙对着AI直接下跪唱征服了,实际上在人类的科技史上,不弱于AI的"奇迹"简直数不胜数,而在此之前他们却对之视而不见。

我坚信科学战胜宗教是必然事件,只要教育平均水平足够高,"拜AI神教"的覆灭也只是时间问题。

同时,我们也十分容易预言AI会在哪里碰壁。

目前所谓AI技术的根本原理,说白了跟机器翻译差不太多,是一种基于统计学的"最优概率输出"。牵扯到概率的话,有个广为流传的说法是:给猴子一部打字机,再给它超长时间比如十亿年,让它随机敲键盘,它也能打出莎士比亚的著作。而牵扯到统计的话,有个极度乐观的说法是:只要给AI足够多的上下文信息,它就可以得出一切问题的答案。

对于第一个说法,那仅仅只是理论上的浪漫,以此套到AI身上认为它总可以力大砖飞也是浪漫。在科学领域有数之不尽这种过于理想的假设了,但在现实中它根本不会发生 。对于第二个说法,同样也是科学史上的老黄历了,也就是所谓的"科学决定论",如果你对此有所了解的话,应当知道这也早就破灭了,既因为某些信息根本无法收集,也因为类似量子物理的存在,指明了我们的现实世界始终存在无法预测之物,总会存在某些事物,人类与AI均不可理解、不可观测、不可触碰。

也就是说,无论如何,AI始终存在犯错的可能性,与人类拉不开本质差别。

当然,AI这件工具仍旧十分强大,在当前时代,就像不再需要心算一样,常规场景下我们或许已经不再需要手写算法。

算法成为逃课项目

之前章节提到过,AI十分擅长写UI,但比起来算法它可能会更为擅长。

算法的发展并不轻松,如果你读过《编程珠玑》,应该记得仅仅一个二分法,人类足足耗费了27年才找到真正完美的写法。

不知道你是否有参加过ACM竞赛?我参加过,也曾在算法八股文里挣扎,成绩不咋地。并非吃不到葡萄就说葡萄酸,我认为算法竞赛与奥数竞赛性质雷同:不过是题海的堆砌罢了。就像奥数冠军往往离真正的数学家相去甚远,算法竞赛冠军也跟真正的软件工程师不是一回事。

只不过,在AI出现之前,在某些场景下我们要把软件性能做得足够高效,就必须苦苦钻研算法工具。

算法的本质,或者说软件优化的本质,就两条路:以空间换时间,或反过来,以时间换空间。

你只需要学习一点经典算法,就不难领会了。我推荐你学一点,因为我见过太多人不懂算法对软件优化完全就抓不到点子上。你也需要懂那么一点皮毛来判定AI的方向是否正确。

撇开设计算法不谈,一般情况下,我们使用算法,不过是使用一些既定套路,适用场景、使用方法,全都非常死板,没有创新之处,因此相当适合AI发挥。

接下来的功能,涉及算法的部分我们就让Gemini来写。

支持网络在线浏览的Pdf解析器

如上一节所述,我们在做Smb在线阅读漫画功能,之前举了Mobi、Zip的例子。

Pdf的情况稍有不同,这个Adobe 1993年发布的文档格式太老了,理念与技术的时代限制令其一出生就存在很大问题,而经过过于漫长的历史,上千页的文档就更像是屎山的具象化了。没有其他大公司强推新格式将其全面替换也实在是很遗憾的事。

Pdf在设计之初没有考虑过网络在线阅读的场景,之后想要补救的时候,引入了"线性化PDF",遗憾的是覆盖范围太小,即使是现在新生成的Pdf,也绝少是线性化的。

而且,现成的开源库都是基于本地阅读的,根本无法适配在线阅读的需求。

所以,我们应当就像处理Mobi、Epub那样,做一个简易的Pdf解析器,反正对于漫画文档来说,只需要处理图片就好。

本文重点不是Pdf解析细节,如有需要推荐参阅书籍《PDF Explained》《Developing with PDF》。

Png图片流解码

PDF漫画储存图片主要采用Jpg,Png这两种格式。对于Jpg,它将是原本的二进制数据,也就是说取得流对象中的ByteArray后就可以直接解析为图片。而对于Png,流对象中的数据有经过压缩,所以第一步需要进行zlib解压,之后,如果Predictor参数大于10,还说明采用了png过滤算法压缩数据,也就需要进行还原,最后,Pdf中存储的为Rgb色彩空间数据,而Android采用Argb,所以还需要一步转换。

让AI来写,这十分简单,注意到这不需要上下文,我们让网页版Gemini来做,模型版本更新且不会消耗Android studio中的用量。只不过它不懂Pdf规范,在经我提示后,得到了可用代码:

kotlin 复制代码
/**
 * 独立的 PDF 图片流解码方法
 * @param compressedBytes 压缩的图片字节数据
 * @param width 图片宽度 (/Width)
 * @param height 图片高度 (/Height)
 * @param predictor 预测器类型(无预测传 1,PNG 预测通常传 12 或 15)
 * @param colors 颜色通道数(RGB为3,灰度为1)
 * @param bitsPerComponent 每分量位数(通常为8)
 * @param columns 外部解析出的 /Columns 参数(可空。若有则用之,若无或<=0则自动回退到 width)
 */
private fun decodeImageBytesFlateDecode2(
    compressedBytes: ByteArray,
    width: Int,
    height: Int,
    predictor: Int = 1,
    bitsPerComponent: Int = 8,
    colors: Int = 3,
    columns: Int? = null
): ByteArray {
    val decompressedBytes = decompressZlib(compressedBytes, 0, compressedBytes.size)

    if (predictor >= 10) {
        val finalColumns = if (columns != null && columns > 0) columns else width
        val rowBytes = (finalColumns * colors * bitsPerComponent + 7) / 8
        val bpp = (colors * bitsPerComponent + 7) / 8
        if (decompressedBytes.size % (rowBytes + 1) == 0) {
            return applyPngPredictor(decompressedBytes, rowBytes, bpp)
        }
    }

    return decompressedBytes
}


private fun decompressZlib(compressedBytes: ByteArray, offset: Int, length: Int): ByteArray {
    ByteArrayInputStream(compressedBytes, offset, length).use { bis ->
        InflaterInputStream(bis).use { iis ->
            ByteArrayOutputStream().use { bos ->
                val buffer = ByteArray(1024)
                var len: Int
                while (iis.read(buffer).also { len = it } != -1) {
                    bos.write(buffer, 0, len)
                }
                return bos.toByteArray()
            }
        }
    }
}



private fun applyPngPredictor(rawData: ByteArray, columns: Int, bpp: Int = 1): ByteArray {
    val rowLength = columns + 1
    if (columns <= 0 || rawData.size % rowLength != 0) {
        return rawData
    }
    val rows = rawData.size / rowLength
    val outData = ByteArray(rows * columns)

    for (r in 0 until rows) {
        val filterType = rawData[r * rowLength].toInt() and 0xFF
        val rowStart = r * rowLength + 1
        val outRowStart = r * columns

        for (c in 0 until columns) {
            val rawVal = rawData[rowStart + c].toInt() and 0xFF
            val priorRowVal = if (r > 0) outData[(r - 1) * columns + c].toInt() and 0xFF else 0
            val leftVal = if (c >= bpp) outData[outRowStart + c - bpp].toInt() and 0xFF else 0
            val topLeftVal =
                if (r > 0 && c >= bpp) outData[(r - 1) * columns + c - bpp].toInt() and 0xFF else 0

            val predicted = when (filterType) {
                0 -> rawVal // None
                1 -> (rawVal + leftVal) and 0xFF // Sub
                2 -> (rawVal + priorRowVal) and 0xFF // Up
                3 -> (rawVal + (leftVal + priorRowVal) / 2) and 0xFF // Average
                4 -> (rawVal + paethPredictor(
                    leftVal,
                    priorRowVal,
                    topLeftVal
                )) and 0xFF // Paeth
                else -> rawVal
            }
            outData[outRowStart + c] = predicted.toByte()
        }
    }
    return outData
}

private fun paethPredictor(a: Int, b: Int, c: Int): Int {
    val p = a + b - c
    val pa = kotlin.math.abs(p - a)
    val pb = kotlin.math.abs(p - b)
    val pc = kotlin.math.abs(p - c)
    return if (pa <= pb && pa <= pc) a else if (pb <= pc) b else c
}

可以见到,applyPngPredictor属于固定算法应用,十分死板也没什么好说的,让AI代劳实在非常美好,这大概帮我们节省了一下午的生命。:)

以上,就是整个环节里Vibe coding最为顺利的部分了。接下来,无论怎么试,即使我有引入PdfBox库供Gemini参考,它也无法按要求写出解析器来。:( 没办法,只能自己动手了。

设计思维

即便假设AI已经完成超前发展,能够达到顶级人类工程师的水平,有几个地方仍然可能会是绕不开的弱项:

  1. 技术选项
  2. 工程架构
  3. 软件退化

万丈高楼平地起,一个项目适用怎样的技术选型是个难以回答的问题,如果你没有即有项目开发经验的话,基本上不可能第一次就找到正确答案,而不合适的选项产生的影响将会十分深远,甚至最终导致被竞争对手打败。

工程架构,是在打好地基之后构建建筑物的骨架。与之类似,它也不是抽象的堆砌设计模式就能解决的问题,不合理的架构会在某些时候导致某些功能无法实现,如果硬要实现则会引入一次性的胶水代码,这可能会非常危险。

软件不会磨损,只会退化。优质的项目,依靠优秀的人类工程师进行严苛的代码审查来抵御这种退化。AI的高速代码生成特性,注定了无法沿用这一机制,终究只可能是AI写、AI审。Kotlin官方的数据统计报告指出,同样的软件在使用Kotlin重构之后,代码更少,Bug更少,性能更强。我的实际体验认同这个观点,如同前面的章节所述,AI写的代码既缺乏整体性,也很可能在局部上并不最优。累赘、次优、隐患的累积,在经过了海量代码的协同之后,可能会造成难以承受的退化效应。而由于不属于Bug,这种退化的因子,想象不到任何现有技术可以检测。

接下来,以我们现在Pdf解析器的设计为例,讲述一点AI做不好所以需要我们自己动手的部分。

完整的Pdf解析无疑是个浩大的工程,如果你研读过PdfBox或者Pdfium,应该会明白虽然似乎难度不大,但是相当的劳心费力。

好在,我们现在仅仅需要完成一个小小的子集。

Pdf是个文本文件与二进制文件的结合。其中的文本部分采用ASC编码。二进制部分可能有经过(多次)压缩也可能没有。它姑且采用了面向对象的方法,让文档中的内容分对象存储。它的文档结构是一棵树,没有限制所以每一层的节点可以数量任意,也就是说要达到最终的页面叶子节点,并取得正确的顺序,需要采用深度优先搜索(DFS)------如果你听不懂我在说什么,那么你需要如前所述去学习一点基础算法。

PDF中的所有基本素材的寻址方法,也就是前述被抽象为对象的东西,被汇集在一张(或多张)表中。旧版本中是交叉引用表,新版本中是交叉引用流。

交叉引用表样例:

shell 复制代码
xref
0 6
0000000000 65535 f 
0000000015 00000 n 
0000000074 00000 n 
0000000120 00000 n 
0000000179 00000 n 
0000000322 00000 n 
trailer
<<
  /Size 6
  /Root 1 0 R
>>
startxref
408
%%EOF

交叉引用流样例:

css 复制代码
15 0 obj
<<
  /Type /XRef
  /Size 6
  /W [ 1 4 2 ]
  /Index [ 0 6 ]
  /Root 1 0 R
  /Info 2 0 R
  /Length 42
>>
stream
\x00\x00\x00\x00\x00\x00\x00
\x01\x00\x00\x00\x0f\x00\x00
\x01\x00\x00\x00\x4a\x00\x00
\x01\x00\x00\x00\x7c\x00\x00
\x02\x00\x00\x00\x0b\x00\x01
\x02\x00\x00\x00\x0b\x00\x02
endstream
endobj

十分恶心的是,如果一个文档有经过增量更新(Pdf的更新始终的增量更新,显然,全量更新才是更符合现代技术的选择),那么这个文档将有可能具有多段的交叉引用表、交叉引用流,甚至可能同时并存 交叉引用表、交叉引用流,最终我们需要根据他们来构建出可以查找到所有对象的查找表。如前所述,构造方法是采用DFS。

交叉引用表,与交叉引用流中存储的虽然都是对象的寻址方法,但是差别却很大。

对交叉引用表:

erlang 复制代码
xref
0 6                   % 第一个数字代表起始对象编号;第二个数字代表存了几个对象
0000000000 65535 f    % 第一个数字为对象起始地址,整个文件中的字节偏移量;第二个数字为代数,说明该对象更新了几次;第三个字母, f表示未使用, n表示使用中
0000000015 00000 n    % 第1号对象,起始地址在字节偏移量15的位置,没有经过更新,使用中的对象
............

对交叉引用流:

css 复制代码
15 0 obj                         % 对象的寻址方法被储存在经过zlib压缩的流对象中,也就是被下方stream endstream包住的字节。
<<
  /Type /XRef
  /Size 6
  /W [ 1 4 2 ]                   % 指明寻址方法中,三个字段分别占据的字节宽度。
  /Index [ 0 6 ]
  /Root 1 0 R
  /Info 2 0 R
  /Length 42
>>
stream
\x00\x00\x00\x00\x00\x00\x00
\x01\x00\x00\x00\x0f\x00\x00
............
endstream


//        根据 /W [w0 w1 w2] 的定义,解压后的 stream 是一个连续的字节数组。每条记录由 Field1 + Field2 + Field3 组成,Field1(类型字段 $Type$)决定了后续两个字段的含义
//        Type (Field 1)含义                       Field 2 含义                           Field 3 含义
//        0  未使用的空闲对象 (Free Object)         下一个空闲对象的编号                    代数 (Generation Number)
//        1  普通未压缩对象 (Uncompressed Object)   对象在文件中的字节偏移量 (Offset)        代数 (Generation Number, 通常为 0)
//        2  对象流中的对象 (Compressed Object)     包含该对象的对象流 (Object Stream)编号   该对象在对象流中的索引下标 (Index)

你可以看到,交叉引用流中的寻址方法,比交叉引用表要复杂不少。简单地,我们可以针对二者设计数据类型XrefItem与CRSItem:

kotlin 复制代码
/**
 * 交叉引用表中的条目。
 * 固定为20字节
 * 没有管是否有效条目(也就是第18个字节为'n'或'f')。反正总要扫描,减少一重判定,多耗点空间完全值得
 * @param objNumber 对象号,新加的使容易处理
 * @param offset 偏移量,相对于文件头的字节偏移量
 * @param generationNumber 似乎没用?
 * */
data class XrefItem(
    val objNumber: Int,
    val offset: Long,
    val generationNumber: Int,
) 

enum class CRSItemField1Type(val value: Int) {
    FREE(0), UNCOMPRESSED(1), COMPRESSED(2);

    companion object {
        fun fromValue(v: Int): CRSItemField1Type {
            return entries.find { it.value == v }
                ?: throw IllegalArgumentException("Unknown value: $v")
        }
    }
}

/**
 * Cross-reference Stream item
 * */
data class CRSItem(
    val objNumber: Int,
    val type: CRSItemField1Type,
    val field2: Long,
    val field3: Int,
) {
    fun isUncompressedType() = CRSItemField1Type.UNCOMPRESSED == type
    fun isCompressedType() = CRSItemField1Type.COMPRESSED == type
}

水平的分水岭在于接下来处理:

  • 平庸的工程师,保持两种Item不做任何处理,if else写到海枯石烂
  • 普通的工程师,加一个中间抽象层将Item类型减少为1
  • 优秀的工程师,虽然没有看到任何文字提示,但能够识别出Pdf的设计者在堆砌这坨屎山的时候,至少还有保持向前兼容性,因此XrefItem天然可以转化为CRSItem:
kotlin 复制代码
data class XrefItem(
    val objNumber: Int,
    val offset: Long,
    val generationNumber: Int,
) {
    fun toCRSItem(): CRSItem {
        return CRSItem(objNumber, CRSItemField1Type.UNCOMPRESSED, offset, generationNumber)
    }
}

这只是一个极端浅显的设计例子,但你应该能明白我在说些什么,以及为什么这种能力AI无法取代。

本节到这里就进入尾声了,感兴趣的读者可以试着去实现一下可以适应网络在线阅读、限定为解析图片的PDF解析器。AI做不到/好,所以我全程手写了,代码量实际上并没有想象中多,基于之前章节提过的接口IRandomAccessFile,在去除上述的代码之后,带注释我总共只写了2000行不到。

相关推荐
疯狂的魔鬼2 小时前
写给 AI 的约束,为什么写了 40 条也没用
ai编程·vibecoding
健了个平_2414 小时前
【iOS】关于传统竖屏 UIKit 项目如何简单适配 iPhone Duo 的第一版
ios·app·swift
feiyu_gao20 小时前
Spec→实现→结晶:一种让 AI 项目不失控的纪律框架
架构·aigc·vibecoding
Autumn_ing21 小时前
AI生成应用能做到哪一步?在线问诊APP从挂号到支付的完整案例
小程序·app·健康医疗·ai应用·问诊app·ai生成应用·墨刀ai
Mr_Mao1 天前
8 月我曾说过要让 DSH 像 Codex 一样好用,现在怎么样了?
app·deepseek
河北清兮网络科技1 天前
在河北开发直播 APP,怎么挑选靠谱的软件公司?
小程序·app·短剧app·广告联盟
AI砖家2 天前
安全进入 Agent Loop:2027 年 Coding Agent 的决胜点
数据库·安全·codex·vibecoding
程序员龙一2 天前
VsCode+ClaudeCode+Deepseek的配置
vscode·claude·deepseek·vibecoding