开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

肘子的 Swift 周报

发表于 2024-04-23

肘子的话

欢迎访问 weekly.fatbobman.com 订阅本周报的中英文电子邮件版本。你也可以直接访问我的博客 肘子的 Swift 记事本 查看更多的周报内容。

苹果公司最近更新了应用商店审核指南,现在允许模拟器应用进入 App Store。就在几天前,名为 Delta 的模拟器应用登陆了应用商店。Delta 是一款为 iOS 设计的综合模拟器,支持多种任天堂掌机游戏。这款应用其实很早以前就已经开发完成,但之前由于政策限制,只能以非商店形式提供给用户。

我在第一时间下载使用了这款应用,当那些熟悉的游戏背景音乐响起,仿佛又带我回到了多年前的游戏时光。虽然这些经典游戏在画面和声音上无法与现代游戏媲美,但它们出色的游戏性至今仍让人沉迷。在玩这些精致而简洁的像素游戏时,我不禁思考,现在的游戏或应用是否变得过于庞大了。

新的硬件平台让我们可以在不担心效率的情况下尽情使用资源,开发者也逐渐习惯了对资源的大量消耗。但在硬件和开发工具不断进步的同时,我们创建的应用真的有相应的提升吗?

这个周日,我收到了来自 MadMachine 的 SwiftIO Playground Kit,一款配备 600MHz MCU、32MB RAM 和 16MB Flash 的开发板。其最大的特点是支持使用 Swift 进行开发,非常适合像我这样只熟悉 Swift 的开发者。我计划在这块硬件上实现一些小项目,试图克制自己对资源日益增长的需求和消耗的习惯。

IMG_3601-zipic

前一期内容|全部周报列表

原创

Core Data 改革:实现 SwiftData 般的优雅并发操作

Fatbobman( 东坡肘子 )

SwiftData,作为 Core Data 的后继者,引入了众多创新和现代化的设计思想。尽管它已经推出一段时间,但许多开发者还未在他们的项目中采用。这种状况部分是因为 SwiftData 对操作系统版本的要求较高,另一方面,由于 SwiftData 在某些功能方面还不够成熟,即便操作系统版本符合要求,开发者也可能因为功能限制而选择继续使用 Core Data。我们是否能将 SwiftData 中的一些卓越设计理念和巧妙实现,融合到 Core Data 的实际使用中呢?本文旨在探讨如何在 Core Data 中引入类似 SwiftData 的优雅和安全的并发操作,以实现一个 @ModelActor 的 Core Data 版本。

近期推荐

Is Dynamic Isolation Bad?

Matt Massicotte

在 Swift 编程中,nonisolated、isolated 和 actor 等关键字构成了所谓的静态隔离。这种方法允许开发者通过类型系统明确地向编译器表达隔离需求,大大减少潜在的错误风险。此外,Swift 还提供了动态隔离的工具,以帮助开发者优雅地处理现有的非并发代码。在这篇文章中,Matt Massicotte 深入探讨了使用静态隔离与动态隔离的利弊。最初,他对使用 MainActor.run 进行动态隔离持反对态度。但在接受了 Rob Napier 的详尽反馈后,他对此话题有了更全面的理解和思考。

How to learn TCA

Andrew Harrison

The Composable Architecture(TCA)是由 Point-Free 推出的一个开源框架,它整合了状态管理、组件组装、副作用处理及测试等工具,旨在帮助开发者构建应用程序。对 TCA 的评价褒贬不一:一些开发者认为它使问题复杂化,而另一些人则视其为与 SwiftUI 搭配使用时的理想选择。不论观点如何,了解该框架无疑能在状态管理、副作用处理、以及测试规划方面为开发者带来显著的益处。在本文中,Andrew Harrison 提供了一个关于如何开始学习 TCA 的指导路径。文章的主旨不在于直接教授 TCA 的使用方法,而是提供一个学习该框架的好的起点。

在上周中,Antoine Lee 也在 Is TCA the new MVVM? 的推文中讨论了与 TCA 相关的话题,感兴趣的朋友可以参与其中。

探索打造以用户为中心的独立应用

Junping

在三月底的北京之行中,我与 Junping 交流了他正在开发中的应用—— 语境词典 (Context)。这款应用以其与众不同的交互方式给我留下了深刻印象。《语境词典》是一个快速而精确的多语言词典,通过大型语言模型根据词语的具体上下文来解释新词汇。在这篇文章中,Junping 分享了他在开发该应用时的经验。他强调了以用户为中心设计的重要性,并详细介绍了从灵感来源到产品发布的整个开发流程。尤其珍贵的是,Junping 还分享了他在构思和设计过程中使用的所有相关文件,包括设计规划和 Figma 文档,旨在帮助其他开发者更好地理解和应用以用户为中心的设计理念。

How to migrate an iOS app to Bazel

Brentley Jones

Bazel 是一个开源的多语言构建系统,广泛应用于大型项目和代码库中。它通过利用构建缓存和并行执行任务来提高构建速度、重复使用、可扩展性和多样性。在这篇详尽的指南中,Brentley Jones 介绍了如何将 iOS 应用从 Xcode 迁移到 Bazel。这个过程包括基础的 Bazelisk 安装、定义构建目标、转换 Xcode 项目、以及集成 Xcode。文章以 Mastodon iOS 项目作为实例,涵盖了从使用 swift_library 和 ios_application 等规则到利用远程缓存和执行优化构建性能的多个方面。这篇文章是希望提高其项目构建效率和管理复杂性的 iOS 开发者的宝贵资源。

Common Mistakes to Avoid When Using Enums in Swift

Leonardo Pugliese

枚举是 Swift 语言中的一个强大工具,具有许多其他编程语言所不具备的特性。Leonardo Pugliese 的这篇文章探讨了 iOS 开发者在使用 Swift 中的枚举时常见的几个错误,包括映射值时缺乏凝聚力、懒惰的 Switch 和默认子句的误用,以及错误地假设自动原始值递增。文章强调了深思熟虑的编码方式、类型安全的价值,以及避免损害代码质量的捷径的重要性。作者提醒读者,良好的编码实践和对细节的关注可以显著提高软件的可靠性和可维护性。

往期内容

  • 肘子的 Swift 周报 #027| 苹果助你成为一日“百万富翁”
  • 肘子的 Swift 周报 #026 | Swift,超越苹果生态!
  • 肘子的 Swift 周报 #025|Let’s VisionOS, Let’s rock and roll!
  • 肘子的 Swift 周报 #024 | 秀出你的 WWDC 24 愿望单

THANK YOU

如果你觉得这份周报或者我的文章对你有所帮助,欢迎 点赞 并将其 转发 给更多的朋友。

欢迎访问 weekly.fatbobman.com 订阅本周报的中英文电子邮件版本。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

作者把他写了3年的软件,给开源了!

发表于 2024-04-23

不久前,GitHub上又一款热度很高的开源项目冲进了开发者们的视野。

项目开源不久,现如今就已经收获了15000+的Star标星。

这就是最近非常火热的“顶流” Web OS,名为Puter!

github.com/HeyPuter/puter

作者直言,这个项目开发了3年,用户数量也突破了100万,现如今正式开源了!

Puter,作为一个基于Web的桌面操作系统,运行于浏览器中,不仅拥有丰富的功能、极快的运行速度,而且具有高度的可扩展性,可以为用户提供了一个全新的Web端桌面体验环境。

这里也给几张图,感受一下这款Internet OS的交互界面:

项目特性与优势:

  • 丰富的功能:Puter项目提供了完整的基于Web的桌面操作系统,涵盖了文件管理、应用程序运行、组织工作环境等功能。用户可以在浏览器中完成各种任务,无需安装额外的软件或插件。
  • 极快的运行速度:Puter项目采用原生JavaScript和jQuery技术,直接与DOM互动,保证了系统运行速度的高效与稳定。无论是打开文件、运行程序还是进行多任务处理,Puter都能迅速响应,为用户带来流畅的使用体验。
  • 高度的可扩展性:用户可以根据自己的需求和偏好对系统进行定制和扩展。这意味着用户可以根据个人喜好调整桌面布局、添加或删除应用程序、自定义快捷方式等,打造出独一无二的桌面环境。
  • 轻松入门与跨平台兼容:Puter项目提供了详细的说明和指南,帮助用户轻松入门。同时,由于Puter是基于Web技术构建的,它具有良好的跨平台兼容性,可以在不同操作系统和设备上运行,为用户提供了更广泛的应用范围和灵活性。

这里必须要提到的一点是,Puter这个项目并未使用诸如像 React, Angular, Vue 等现如今非常流行的前端框架,而是采用的原生 JavaScript 和 jQuery 来编写,直接与DOM交互。

至于为什么这么做,作者也给出了解释,主要还是处于性能考虑,同时希望避免复杂的抽象,以保持整个项目的可控。

项目官方给到了Puter项目的几个典型应用场景,包括像:

  • 远程桌面环境:Puter可作为构建远程桌面环境的选择,一个典型的场景就是用于用于服务器和工作站的远程桌面环境。
  • 常见系统的平台界面:Puter项目也可以作为云存储服务、远程服务器以及Web托管平台的界面,为用户提供便捷的交互以及相关的操作和管理界面。

除此之外,其安装部署也非常方便。

用户如果想在本地体验或者进行学习,其支持直接性的本地部署:

1
2
3
4
bash复制代码git clone https://github.com/HeyPuter/puter
cd puter
npm install
npm start

同时也支持Docker化安装部署:

1
2
3
bash复制代码git clone https://github.com/HeyPuter/puter
cd puter
docker compose up

大家感兴趣的话也可以自己动手部署尝试一下。除此之外,大家有兴趣也可以研究一下对应的代码,这也是一个很好的项目学习材料。

注:本文在GitHub开源仓库「编程之路」 github.com/rd2coding/R… 中已经收录,里面有我整理的6大编程方向(岗位)的自学路线+知识点大梳理、面试考点、我的简历、几本硬核pdf笔记,以及程序员生活和感悟,欢迎star。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

接单必备神器FastAdmin系列一,环境搭建

发表于 2024-04-23

做独立开发接私活时,效率尤其重要。然而,很多后端开发语言几乎没有完善的一套系统让你快速开发,但 PHP 却是一个例外。在 Web 开发领域,PHP 一直占据着重要地位。FastAdmin 就是其中一个高效开发、节省时间的开源后端框架,采用了 PHP+Bootstrap 技术栈。最近,我利用它完成了一个包含 10 多个页面的全栈项目,尽管之前我从未真正使用过 PHP 进行项目开发。接下来我将这个过程进行一些总结,分享如何使用 FastAdmin 进行项目开发,包括数据库操作、API 开发以及插件应用等内容。我相信,即使没有 PHP 基础,阅读之后,也能够轻松上手,开始自己的项目开发。第一步,肯定是安装配置环境了。

phpStudy 安装

安装地址:www.xp.cn/,安装完成一键启动即可。

默认启动的是 Nginx 和 MySQL,如果喜欢使用 Apache,可以进行切换。

然后,切换到软件管理,如果 PHP 默认版本低于 7.4,安装一个大于 7.4 版本的,因为 FastAdmin 要求 PHP 版本高于 7.4。

创建网站

  1. 首先到 FastAdmin 官网下载 FastAdmin 完整包,并解压到 phpStudy 安装目录的 WWW 目录下,如我的目录 D:/phpstudy_pro/WWW/fastadmin.test。下载地址:www.fastadmin.net/download.ht…


2. 新建站点

  • 域名随便填写,域名会自动配置到 hosts 文件;
  • 根目录选择解压文件夹下的 public 目录;
  • PHP 选择 7.4 以上版本;

  • 切换到伪静态,添加配置;

1
2
3
4
php复制代码  if (!-e $request_filename) {
rewrite ^(.*)$ /index.php?s=/$1 last;
break;
}
  • 最后点击确认新建。
  1. 站点配置
  • 浏览器打开上面填写的域名,可以看到如下界面;

  • 按要求填写信息,数据库用户名和密码可以返回 phpStudy 面板查看;

  • 点击安装,完成就可以啦!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Android音视频开发框架(下) 前言 音视频读取 音视频

发表于 2024-04-22

前言

前文讲到Android音视频开发框架中的上半段:音视频的创建,编码,保存,这个属于音视频资源生产端的过程。在消费端,还需要经历读取,解码,播放这三个节点。

音视频读取

在前文中,我们可以打通从摄像头+麦克风-编码数据-保存文件这个过程,假如一切顺利,那么可以在磁盘中保存一个MP4文件。但是想要消费这段影片,首先要做的就是提取文件里的编码过的音频和视频信息。这个工作主要依赖于MediaExtractor类。

MediaExtractor的主要方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
scss复制代码// 设置数据源
mediaExtractor.setDataSource()
// 获取轨道数(音频轨道,视频轨道,字幕轨道等)
mediaExtractor.getTrackCount()
// 获取该轨道的格式类型(是音频还是视频)
mediaExtractor.getTrackFormat()
// 选择轨道(确定读取哪个轨道的数据)
mediaExtractor.selectTrack()
// 读取采样数据到数组
mediaExtractor.readSampleData()
// 进入下一个采样,readSampleData之后需要调用advance推动指针往前挪动
mediaExtractor.advance()
// 返回当前轨道索引
mediaExtractor.getSampleTrackIndex()
// 返回当前采样的显示时间
mediaExtractor.getSampleTime()
// seek到对应时间
mediaExtractor.seekTo()
// 释放资源
mediaExtractor.release()

我们可以把MediaExtrtactor看作是MediaMuxer的逆过程,后者是把音频视频封装写入文件,前者是读取文件,解封装获取独立的音频和视频。

音频和视频分别是独立线程编解码的,那么读取自然在分在两个线程中分别读取互不干扰。而且由于操作的相似性,我们可以对它的操作进行一定的封装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
kotlin复制代码class MExtractor(filePath:String) {
companion object{
val EXTRACTOR_TAG = "extractor_tag"
}
private var audioTrackIndex = -1
private var videoTrackIndex = -1
private val mediaExtractor:MediaExtractor by lazy {
MediaExtractor()
}

init {
try {
mediaExtractor.setDataSource(filePath)
}catch (e:IOException){
e.printStackTrace()
Log.e(EXTRACTOR_TAG,"${e.message}")
}
}
// 选择音频轨道
fun selectAudioTrack(){
val index = getAudioTrack()
if (index == -1) return
mediaExtractor.selectTrack(index)
}
// 选择视频轨道
fun selectVideoTrack(){
val index = getVideoTrack()
if (index == -1) return
mediaExtractor.selectTrack(index)
}
// 读取(对应轨道的)数据
fun readSampleData(byteBuf: ByteBuffer, offset:Int):Pair<Int,Long>{
//读取一块数据
val readSize = mediaExtractor.readSampleData(byteBuf, offset)
// 获取这块数据对应的时间错
val sampleTimeValue = mediaExtractor.sampleTime
//指针往前移动
mediaExtractor.advance()
return Pair(readSize,sampleTimeValue)
}
...
...
fun getAudioTrack():Int{
if (audioTrackIndex != -1){
return audioTrackIndex
}
for (i in 0..mediaExtractor.trackCount) {
val format = mediaExtractor.getTrackFormat(i)
if (format.getString(MediaFormat.KEY_MIME)?.startsWith("audio/") == true){
Log.i(EXTRACTOR_TAG,"selected format: $format track: $i")
audioTrackIndex = i
return i
}
}
return -1;
}

fun getVideoTrack():Int{
if (mediaExtractor.trackCount == 0){
return -1
}
if (videoTrackIndex != -1){
return videoTrackIndex
}
for (i in 0..mediaExtractor.trackCount) {
val format = mediaExtractor.getTrackFormat(i)
Log.i(EXTRACTOR_TAG,"video index: $i format: $format")

if (format.getString(MediaFormat.KEY_MIME)?.startsWith("video/") == true){
Log.i(EXTRACTOR_TAG,"format: $format")
videoTrackIndex = i
return i
}
}
return -1
}
}

以上基本上就是MediaExtractor的全部了,他往往需要配合其他的组件使用。

音视频解码

有了MediaExtractor的帮助,我们已经可以 从文件中获取数据源,接着我们还是使用异步模式来开启解码过程

视频

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
kotlin复制代码private val videoHandlerThread: HandlerThread = HandlerThread("video-thread").apply { start() }
private val videoHandler = Handler(videoHandlerThread.looper)

private val mediaExtractor: MExtractor by lazy {
MExtractor(fileData.filePath)
}

// 异步模式的回调
private val videoCallback = object : CodecCallback() {
override fun onInputBufferAvailableWrapper(codec: MediaCodec, index: Int) {
if (isSignalEOF || mediaExtractor.getSampleTrackIndex() == -1) {
return
}
pauseIfNeed()

val inputBuffer = codec.getInputBuffer(index) ?: return
inputBuffer.clear()
// 选择视频轨道
mediaExtractor.selectVideoTrack()
//读取数据
// sampleTime 视频的PTS
var (readSize, sampleTime) = mediaExtractor.readSampleData(inputBuffer, 0)
if (readSize < 0) {
inputBuffer.limit(0)
codec.queueInputBuffer(index, 0, 0, 0, 0)
isSignalEOF = true
} else {
codec.queueInputBuffer(index, 0, readSize, sampleTime, 0)
}


}

override fun onOutputBufferAvailableWrapper(
codec: MediaCodec, index: Int, info: MediaCodec.BufferInfo
) {
if (isOutputEOF) {
return
}
isOutputEOF = (info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0
trySleep(info.presentationTimeUs)
// index 是解码后的数据缓存空间下标
// 第二个参数表示是否渲染(如果提前设置了输出端的Surface的话,填true)
codec.releaseOutputBuffer(index, true)

}
...
...
}


...
// configure
mediaExtractor.getVideoFormat()?.let {
val mime = it.getString(MediaFormat.KEY_MIME)
mime?.let {m->
videoDecoder = MediaCodec.createDecoderByType(m)
// 这个surface来自于播放器(SurfaceView或者TextureView)
videoDecoder?.configure(it, surface, null, 0)
videoDecoder?.setCallback(videoCallback, videoHandler)
}

}
// 开始解码
videoDecoder?.start()
...
...
// release
videoDecoder?.stop()
videoDecoder?.release()

对于视频的解码过程,输出端我们仍然可以使用Surface来简化我们的输出操作,MediaCodec提供了直接输出数据到Surface的过程,因此我们把播放端的SurfaceView或者TextureView中的surface传入进来,那么数据就可以直接打通了。

音频

音频的解码过程和视频解码大差不差

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
kotlin复制代码private val audioHandlerThread: HandlerThread = HandlerThread("audio-thread").apply { start() }
private val audioHandler = Handler(audioHandlerThread.looper)

private val mediaExtractor: MExtractor by lazy {
MExtractor(fileData.filePath)
}

// 解码异步模式回调
private val audioCallback = object : CodecCallback() {
override fun onInputBufferAvailableWrapper(codec: MediaCodec, index: Int) {
if (isEOF || mediaExtractor.getSampleTrackIndex() == -1) {
return
}
pauseIfNeed()
val inputBuffer = codec.getInputBuffer(index) ?: return
inputBuffer.clear()
mediaExtractor.selectAudioTrack()
// 读取采样数据到buffer,获取采样时间,同时指针向前推进
// sampleTimeValue就是当前数据的PTS,这个直接从mediaExtractor中获取,从0开始
val (readSize, sampleTimeValue) = mediaExtractor.readSampleData(inputBuffer, 0)
if (readSize < 0) {
codec.queueInputBuffer(index, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM)
isEOF = true
} else {
codec.queueInputBuffer(index, 0, readSize, sampleTimeValue, 0)
}

}

override fun onOutputBufferAvailableWrapper(
codec: MediaCodec, index: Int, info: MediaCodec.BufferInfo
) {
val outputBuffer = codec.getOutputBuffer(index)
outputBuffer?.let {
it.position(info.offset)
it.limit(info.offset + info.size)
...
// 向音频播放设备写入数据
...
}

trySleep(info.presentationTimeUs)
codec.releaseOutputBuffer(index, false) // 重要
}
...
...
}


// configure
mediaExtractor.getAudioFormat()?.let {
val mime = it.getString(MediaFormat.KEY_MIME) ?: ""
audioDecoder = MediaCodec.createDecoderByType(mime)
audioDecoder?.configure(it, null, null, 0)
audioDecoder?.setCallback(audioCallback, audioHandler)
Log.i(TAG, "audio inputbuffer mime: $mime")

}
// start
audioDecoder?.start()
...
...
// release
audioDecoder?.stop()
audioDecoder?.release()

音视频播放

音视频播放其实是完全不同的路径,视频播放依赖TextureView等的view展示,而音频播放则是依赖音频设备。

对于视频而言,我们需要在UI中插入TextureView(SurfaceView也一样),然后在TextureView中设置SurfaceTextureListener,等待SUrface的创建成功,接着把SUrface传入解码器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
kotlin复制代码dataBinding.textureview.surfaceTextureListener = object :SurfaceTextureListener{
override fun onSurfaceTextureAvailable(
surfaceTexture: SurfaceTexture,
width: Int,
height: Int
) {
Log.i(TAG,"onSurfaceTextureAvailable $width $height $surfaceTexture")
val surface = Surface(surfaceTexture)
startDecodeVideo(surface) // 传入解码模块
startDecodeAudio() // 一般也可以在此时触发音频的解码

}

override fun onSurfaceTextureSizeChanged(
surface: SurfaceTexture,
width: Int,
height: Int
) {
Log.i(TAG,"onSurfaceTextureSizeChanged $width $height $surface")

}

override fun onSurfaceTextureDestroyed(surfaceTexture: SurfaceTexture): Boolean {
curSurface?.release()
Log.i(TAG,"onSurfaceTextureDestroyed $surfaceTexture")
return true
}

override fun onSurfaceTextureUpdated(surfaceTexture: SurfaceTexture) {
Log.i(TAG,"onSurfaceTextureUpdated $surfaceTexture")

}
}

这样,解码的视频帧就可以显示在textureView上了。

但是音频的播放过程则完全在后台进行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
scss复制代码//  创建音频播放设备
mediaExtractor.getAudioFormat()?.let {
// 初始化配置
val audioAttr = AudioAttributes.Builder()
.setContentType(CONTENT_TYPE_MOVIE)
.setLegacyStreamType(AudioManager.STREAM_MUSIC)
.setUsage(USAGE_MEDIA)
.build()
val sampleRate = it.getInteger(MediaFormat.KEY_SAMPLE_RATE)
var channelMask = if (it.containsKey(MediaFormat.KEY_CHANNEL_MASK)) {
it.getInteger(MediaFormat.KEY_CHANNEL_MASK)
} else {
null
}
var channelCount = 1
if (it.containsKey(MediaFormat.KEY_CHANNEL_COUNT)) {
channelCount = it.getInteger(MediaFormat.KEY_CHANNEL_COUNT)

}
val channelConfig =
if (channelCount == 1) AudioFormat.CHANNEL_OUT_MONO else AudioFormat.CHANNEL_OUT_STEREO
if (channelMask == null) {
channelMask = channelConfig
}
val formatInt = if (it.containsKey(MediaFormat.KEY_PCM_ENCODING)) {
it.getInteger(MediaFormat.KEY_PCM_ENCODING)
} else {
AudioFormat.ENCODING_PCM_16BIT
}
val audioFormat = AudioFormat.Builder()
.setChannelMask(channelMask)
.setEncoding(formatInt)
.setSampleRate(sampleRate)
.build()

bufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, formatInt)
// 创建音频播放设备
audioTrack = AudioTrack(
audioAttr,
audioFormat,
bufferSize,
AudioTrack.MODE_STREAM,
audioManager.generateAudioSessionId()
)
}


//开始播放,和audioDecode.start同时调用即可
audioTrack?.play()

// 在合适的时机写入音频数据(一般就放在解码完成输出之后写入即可)
audioTrack?.write(...)


// 释放资源
audioTrack?.stop()
audioTrack?.release()

以上就是音频播放设备的使用方式。

你以为这样就结束了么?天真。

如果按照正常操作视频的解码速度会很快,你会发现视频像走马灯一样播放完了,音频还在播放,因此我们需要对音视频进行同步。

音视频同步

由于每一帧音频或者视频数据都有PTS,也就是说已经设定好了这一帧数据应该播放的时间点,而音视频同步要做的就是,当解码出来的帧的时间戳还没到播放的时间节点时,我们需要等待,一直等到播放的时间节点到来。

音视频同步的方法不止一种,我选择大家比较容易理解的一种来描述:选择一条独立的时间轴,每次音频或者视频解码出来之后的时间戳与独立时间轴的当前时间戳进行比较,如果大于当前时间戳,表示该帧数据还没有到展示的时候,需要等待,否则就直接展示。

如何实现呢?比较简单,在开始解码时的时间设为独立时间轴的起点startPresentationTimeUs,后续的解码回调中和这个时间起点进行比较即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
kotlin复制代码// 开始解码时调用,并记录一下时间起点
@CallSuper
override fun start() {
if (startPresentationTimeUs == -1L){
startPresentationTimeUs = getMicroSecondTime()
}
}

protected fun getMicroSecondTime():Long{
return System.nanoTime()/1000L
}

// 每次准备播放音频或者视频时调用一次,
protected fun trySleep(sampleTime:Long){
val standAlonePassTime = getMicroSecondTime()-startPresentationTimeUs
if (sampleTime>standAlonePassTime){
try {
val sleepTime = (sampleTime-standAlonePassTime)/1000
Log.i(TAG,"sleep time $sampleTime ${sleepTime}ms $this")
// 如果时间不够,就休眠
Thread.sleep(sleepTime)
}catch (e:InterruptedException){
e.printStackTrace()
}
}
}

这就实现了一个简单的音视频同步的逻辑了,我相信理解起来没有太大的难度。当然,如果系统有支持的方法我们自然不必亲自实现同步逻辑,在Android体系中,有MediaSync可以帮助我们实现音视频播放同步的逻辑,使用起来不算太复杂,不过它也同样深度嵌套到音视频的解码过程中去了,这个留给大家去熟悉吧。

除了音视频同步这个重要内容外,其实还有播放/暂停,这个过程也会影响到音视频同步的逻辑,因为播放暂停时,每帧数据的显示时间戳PTS不会变,但是我们建立的独立时间轴的时间会继续流逝,等恢复之后,在比较时间戳就完全错误了,因此我们需要在暂停和恢复时记录一下暂停的时长,然后在比较时减去这段时间,又或者直接把独立时间轴的起点时间往后挪动暂停时长即可。

此外,播放过程中获取预览图,播放进度条等内容也是基本内容,我认为它们并没有比音视频同步更难以理解,因此不一一说明了。

Android当然有支持较好的播放器可以同时播放音频和视频,而且还能自动帮助我们解码数据,这些我相信大家是更了解的。

总结

到此,Android的音视频开发框架基本描述完整了,它涵盖了音视频的创建,编码,保存,提取,解码,播放的全过程,当然每个部分只是囫囵吞枣的介绍,代码也不是完整,其实这里里面很多内容都可以单列一章来讲,细节颇多,不过我认为作为一个简介性质的文章深度是够了的,主要侧重于介绍概念和使用方法。后续深入研究还靠自己,本身的水平也有限。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

做了一个图片批量压缩小工具 tiny_gradio

发表于 2024-04-22

一、背景

之前很早的时候搞了一个博客,但是一直没去打理它,最近放了一些文章上去,但是在配图体积方面有点问题,因为平常写公众号的时候会自动对图片做压缩,但是自己的博客没有。如果图片没做压缩,势必会影响访问速度,造成体验不佳。

我一般使用 tinyjpg进行压缩,有时候为了体积进一步减小,还会转成 webp 的格式。

loading

但是图片数量一多手动上传就比较麻烦,所以就想着开发一个小工具提高下效率。

二、技术选型

在压缩方面,本来是想在Github上找找压缩库,后面发现其实 tinyjpg 开放了免费的API,每个月有500次,想一想也够自己使用了,所以选了这个。

申请地址: https://tinypng.com/developers

Tiny API的官方库支持Ruby ,PHP, Node.js, Python, Java 和 .NET 编程语言,Python我比较熟,所以最终选择它。

我希望这个工具有UI,操作起来比较方便,而Python的GUI库中Gradio相对而言比较好看,所以最终的技术栈就是:Python + tinify + Gradio

三、项目实战

整个项目开发下来,比较难的在于使用 Gradio 绘制画面,因为之前并没接触过 Gradio ,需要一定的上手成本

我一开始设计的第一版是这样,但是这样设计有点问题,点击压缩按钮后,虽然成功压缩了图片,但界面上却没有地方展示输出的结果,导致不知道什么时候图片已经压缩成功。

loading

随后第二版设计成这样,右边的输入框就是用来输出结果

loading

这是界面的核心代码,主要是利用 Interface 实现了左边输入+右边输出的整体效果。

loading

这里需要注意的是,inputs传入多少个组件,对应的执行函数 fn 就得有多少个参数

loading

另外就是只用 Image组件只能实现单张图片上传的效果,所以为了实现选择多张图片的效果,采用的是 File+UploadButton 的组合形式

随后就是 tinify 的压缩代码

loading

还有 webp 的转换代码,这里使用的是Python的 Pillow 库

loading

四、注意事项

1、不同系统需要选择不同的requirements.txt

由于 tinify 依赖 requests库,而 requests 依赖 urllib3 库,在Mac上,如果 urllib3 的版本是 v2,极有可能会出现下面的问题,这时候使用 requirements_mac.txt 安装依赖即可

loading

2、批量上传只能把图片都放在同一个文件夹下

由于Gradio的文件多选,并不支持在多个目录下批量选择文件,所以目前使用的话只能把图片都放在同一个文件夹下,这里才能实现批量上传,而且注意批量上传需要点击 Upload Multi Image 按钮

3、源码获取

关于源代码的获取,在AI技术巫公众号回复“图片压缩”就可以得到全部源码!

最后如果这个工具能帮到你的话,还请多多点赞,你的点赞是我不断更新的动力!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

只写后台管理的前端要怎么提升自己

发表于 2024-04-22

本人写了五年的后台管理。每次面试前就会头疼,因为写的页面除了表单就是表格。抱怨过苦恼过也后悔过(虽然我现在已经心安理得的摆烂),但是站在现在的时间点回想以前,发现有很多事情可以做的更好,于是有了这篇文章。

写优雅的代码

一道面试题

大概两年以前,面试美团的时候,面试官让我写一道代码题,时间单位转换。具体的题目我忘记了。

原题目我没做过,但是我写的业务代码代码里有类似的单位转换,后端返回一个数字,单位是kb,而我要展示成 KB,MB 等形式。大概写一个工具函数(具体怎么写的忘记了,不过功能比这个复杂点):

1
2
3
4
5
6
7
8
9
10
11
js复制代码function formatSizeUnits(kb) {
let units = ['KB', 'MB', 'GB', 'TB', 'PB'];
let unitIndex = 0;

while (kb >= 1024 && unitIndex < units.length) {
kb /= 1024;
unitIndex++;
}

return `${kb.toFixed(2)} ${units[unitIndex]}`;
}

而在此之前,是一个后端同学写的代码(说起来,后台管理系统前端人力不够后端帮忙写好像是常规操作,后端总觉得写前端简单,而我只能说你们那代码写的,一言难尽……),如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
js复制代码function formatSizeUnits(kb) {
var result = '';

if (kb < 1024) {
result = kb + ' KB';
} else if (kb < 1024 * 1024) {
result = (kb / 1024).toFixed(2) + ' MB';
} else if (kb < 1024 * 1024 * 1024) {
result = (kb / 1024 / 1024).toFixed(2) + ' GB';
} else {
result = (kb / 1024 / 1024 / 1024).toFixed(2) + ' TB';
}

return result;
}

虽然功能都一样,但是我写在面试的时候写了上面的代码得到了面试官的赞扬,也因此非常顺利的通过了面试。而如果我写成了下面的样子,估计不会有这种效果。

如何提升代码质量

想提升代码质量,最理想的是靠 code review ,但实际上这玩意在大多数公司根本就不存在。其次就只能多看看大佬们写的代码,但是一来后台管理本来就没太大难度很少有大佬,二来很多时候一个后台管理系统就一两个人负责。

还好现在有了各种 AI 工具,可以非常方便的优化自己的代码了。

还是上面的问题,看看 GPT 给的答案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
ts复制代码// 提问:写一个工具函数,对文件大小的单位进行转换,加注释
// 回答:下面是一个JavaScript工具函数,它接受文件大小、初始单位、目标单位和小数位数,然后返回目标单位下的文件大小。

/**
* 将文件大小从一个单位转换为另一个单位。
*
* @param {number} size 文件大小。
* @param {string} fromUnit 初始单位('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB')。
* @param {string} toUnit 目标单位('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB')。
* @param {number} [decimalPoint=2] 结果保留的小数位数,默认为2。
* @return {string} 转换后的文件大小,带单位。
*/
function convertFileSize(size, fromUnit, toUnit, decimalPoint = 2) {
// 定义单位与字节之间的转换关系
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
// 获取初始单位和目标单位的索引
const fromIndex = units.indexOf(fromUnit);
const toIndex = units.indexOf(toUnit);

// 如果单位不在列表中,抛出错误
if (fromIndex === -1 || toIndex === -1) {
throw new Error('Invalid units');
}

// 计算初始单位与目标单位之间的转换系数
const exponent = toIndex - fromIndex;
// 计算结果大小
const resultSize = size / Math.pow(1024, exponent);

// 返回格式化后的结果
return parseFloat(resultSize.toFixed(decimalPoint)) + ' ' + toUnit;
}

// 示例使用
console.log(convertFileSize(1, 'GB', 'MB')); // 输出: 1024.00 MB
console.log(convertFileSize(1, 'MB', 'KB')); // 输出: 1024.00 KB
console.log(convertFileSize(1, 'KB', 'B')); // 输出: 1024.00 B
console.log(convertFileSize(1, 'MB', 'GB', 5)); // 输出: 0.00098 GB

还能说什么,我只能说 NB!(经评论区提示,此段代码可能有问题,建议大家在使用AI工具时,重点是借鉴思路,直接使用代码需要谨慎~)

我现在每次写大段逻辑之后都习惯拿给 AI 看看,有什么更好的实现方式,或者用什么设计模式。AI 是非常低成本且高效提升代码质量的工具。

学会封装

一个功能用到了好多次,为什么不封装成组件?一个组件用到了好几个项目,为什么不单独写个npm包?差不多的项目创建了好几个,为什么不封装成脚手架?

你说,没时间,没必要,复制粘贴反而更快。

那你就完全没理解,这么做不一定是为了让工作更快完成,而是可以让你在年年终述职时更有话说(你就算写了一百个表单表格没有写一个脚手架更值得炫耀),如果不会写可以问问 AI。

而当你真正开始封装组件,开始写工具库了,你会发现你需要思考的确实比之前多了。

关注业务

对于前端业务重要吗?

相比于后端来说,前端一般不会太关注业务。就算出了问题大部分也是后端的问题。

但是就我找工作的经验,业务非常重要!

如果你做的工作很有技术含量,比如你在做低代码,你可以面试时讲一个小时的技术难点。但是你只是一个破写后台管理,你什么都没有的说。这个时候,了解业务就成为了你的亮点。

一场面试

还是拿真实的面试场景举例,当时前同事推我字节,也是我面试过N次的梦中情厂了,刚好那个组做的业务和我之前呆的组做的一模一样。

  • 同事:“做的东西和咱们之前都是一样的,你随便走个过场就能过,我在前端组长面前都夸过你了!”
  • 我:“好嘞!”

等到面试的时候:

  • 前端ld:“你知道xxx吗?(业务名词)”
  • 我:“我……”
  • 前端ld:“那xxxx呢?(业务名词)”
  • 我:“不……”
  • 前端ld:“那xxxxx呢??(业务名词)”
  • 我:“造……”

然后我就挂了………………

如何了解业务

  1. 每次接需求的时候,都要了解需求背景,并主动去理解

我们写一个表格简简单单,把数据展示出来就好,但是表格中的数据是什么意思呢?比如我之前写一个 kafka 管理平台,里面有表格表单,涉及什么 cluster controller topic broker partition…… 我真的完全不了解,很后悔我几年时间也没有耐下心来去了解。
2. 每次做完一个需求,都需要了解结果

有些时候,后台管理的团队可能根本没有PM,那你也要和业务方了解,这个功能做了之后,多少人使用,效率提高了吗?数据是怎样的?
3. 理解需求,并主动去优化

产品要展示一千条数据,你要考虑要不要分页,不分页会不会卡,要不要上虚拟表格?

产品要做一个可拖拽表单,你要考虑是否需要拖动,是否需要配置。

其实很多时候,产品的思维可能会被局限在竞品的实现方式,而前端可以给TA更多选项。在和产品沟通的时候,你不仅是沟通页面的实现,也更能理解业务。

关注源码

说到源码, Vue,React 这些基本是每次必问,也有太多大佬们的总结我就不多说了。

除此之外,我想大家每次面试应该都会被问,你写了什么亮点的项目,困难的项目,你会说什么?哦,都是表单表格,都用 Element UI 实现的,没什么难度。那估计你面试也就到这里了。

那说什么,后台管理就这些啊?!

如果你用了 Element UI 你可以说写了什么自定义 Element UI 表单组件,Element UI 表单源码是什么原理,怎么通信的?用了 Axios 可以说 Axios 的原理,它的拦截器怎么实现的?用了编辑器 diff 那你知道 diff 算法是什么吗?用了 ECharts 那你知道他底层是用什么绘制的吗?用了 husky ?husky的原理是什么?用了Vite,它为什么快?是怎么打包的?写过插件吗?

可能有人觉得我离谱,事实上上面每一个问题都是我在面试中真实被问到的。当然,如果全部去了解,肯定不现实,每个人的精力有限,不可能每个用到的每个工具每个库都去了解原理,不过只要有一两个你去了解过,就可以在面试中引导面试官来问,要学会表现出你擅长的点。

至于怎么读源码?以前我不知道,现在嘛…… 为什么不问问神奇的 ChatGPT 呢?

讲一下 Axios 源码中,拦截器是怎么实现的?

Axios 的拦截器实现基于 Axios 的核心原理,即 Axios 实例是一个包含请求和响应拦截器堆栈的对象。当发出请求或接收响应时,Axios 会遍历这些拦截器,并按照添加的顺序执行请求拦截器,以及按照相反的顺序执行响应拦截器。

在 Axios 的源码中,拦截器是通过一个 AxiosInterceptorManager 实例来管理的,它维护了一个拦截器数组。每个拦截器都是一个包含 fulfilled 和 rejected 函数的对象。这两个函数分别对应于拦截器成功处理和拦截器处理出错的情况。

以下是 Axios 拦截器管理器的一个简化版本,展示了其核心实现思路:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
> javascript复制代码class InterceptorManager {
> constructor() {
> this.handlers = []; // 存储拦截器的数组
> }
>
> use(fulfilled, rejected) {
> this.handlers.push({
> fulfilled: fulfilled,
> rejected: rejected
> });
> return this.handlers.length - 1; // 返回拦截器的ID
> }
>
> eject(id) {
> if (this.handlers[id]) {
> this.handlers[id] = null; // 移除拦截器
> }
> }
>
> forEach(fn) {
> this.handlers.forEach((h) => {
> if (h !== null) {
> fn(h);
> }
> });
> }
> }
>
>

在发送请求或接收响应时,Axios 会创建一个 promise 链,并通过 forEach 方法将拦截器中的 fulfilled 和 rejected 函数添加到这个链中。这样,每个拦截器都可以对请求或响应进行处理,然后将结果传递到链的下一个拦截器,或者在出错时结束链的执行。

1
2
3
4
5
6
7
8
9
> javascript复制代码axios.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
> chain.unshift(interceptor.fulfilled, interceptor.rejected);
> });
>
> axios.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
> chain.push(interceptor.fulfilled, interceptor.rejected);
> });
>
>

在 Axios 的完整实现中,这个拦截器机制被集成到了 Axios 的请求发送和响应处理流程中。通过这种方式,Axios 可以在发送请求之前和接收响应之后,但在用户定义的 .then 或 .catch 执行之前,插入自定义的逻辑。

请注意,这里提供的代码只是为了说明 Axios 拦截器的实现原理,并不是 Axios 源码的完整复制。如果你对 Axios 的拦截器实现细节感兴趣,建议查看 Axios 的官方 GitHub 仓库中的源码。

前端基建

当我们工作时间久了面试难免会遇到这些问题,前端工程化,前端监控,工作流,部署,性能等等。其实我们在工作中绝大部分时间都在写代码,对于这些不是所有人都有机会接触到,不过这些和所做的业务无关,是我们提升自己很好的一个思路。

技术选型

技术栈选 Vue 还是 React?Vue 选 Vue2 还是 Vue3?组件库选 ElementUI 还是 Ant Design?微前端有没有使用过?打包工具用 Vite 还是 Webpack?有那么多表单怎么实现的,有没有什么表达配置化方案,比如Formily?

对于我这种菜鸡,我这种只写简单的表单表格的人,这些都……无所谓……

image.png
不过为了应对面试我们还是需要了解下未选择技术栈的缺点,和已选择技术栈的优点(有点本末倒置…但是常规操作啦)

Vue 你可以说简单高效轻量级,面试必会问你为什么,你就开始说 Vue 的响应式系统,依赖收集等。

React 你可以说 JSX、Hooks 很灵活,那你必然要考虑 JSX 怎么编译, Hooks 实现方式等。

总体而言,对于技术选型,依赖于我们对所有可选项的理解,做选择可能很容易,给出合理的理由还是需要花费一些精力的。

开发规范

这个方面,在面试的时候我被问到的不多,我们可以在创建项目的时候,配置下 ESlint,stylelint, prettier, commitlint 等。

前端监控

干了这么多年前端,前端监控我是……一点没做过。

image.png
前端监控,简单来说就是我们在前端程序中记录一些信息并上报,一般是错误信息,来方便我们及时发现问题并解决问题。除此之外也会有性能监控,用户行为的监控(埋点)等。之前也听过有些团队分享前端监控,为了出现问题明确责任(方便甩锅)。

对于实现方案,无论使用第三方库还是自己实现,重要的都是理解实现原理。

对于错误监控,可以了解一下 Sentry,原理简单来说就是通过 window.onerror 和 window.addEventListener('unhandledrejection', ...) 去分别捕获同步和异步错误,然后通过错误信息和 sourceMap 来定位到源码。

对于性能监控,我们可以通过 window.performance、PerformanceObserver 等 API 收集页面性能相关的指标,除此之外,还需要关注接口的响应时间。

最后,收集到信息之后,还要考虑数据上报的方案,比如使用 navigator.sendBeacon 还是 Fetch、AJAX?是批量上报,实时上报,还是延迟上报?上报的数据格式等等。

CI/CD

持续集成(Continuous Integration, CI)和 持续部署(Continuous Deployment, CD),主要包括版本控制,代码合并,构建,单测,部署等一系列前端工作流。

场景的工作流有 Jenkins、 Gitlab CI 等。我们可以配置在合并代码时自动打包部署,在提交代码时自动构建并发布包等。

这块我了解不多,但感觉这些工具层面的东西,不太会涉及到原理,基本上就是使用的问题。还是需要自己亲自动手试一下,才能知道细节。比如在 Gitlab CI 中, Pipeline 、 Stage 和 Job 分别是什么,怎么配置,如何在不同环境配置不同工作流等。

了解技术动态

这个可能还是比较依赖信息收集能力,虽然我个人觉得很烦,但好像很多领导级别的面试很愿意问。

比如近几年很火的低代码,很多面试官都会问,你用过就问你细节,你没用过也会问你有什么设计思路。

还有最近的两年爆火的 AI,又或者 Vue React的最新功能,WebAssembly,还有一些新的打包工具 Vite Bun 什么的,还有鸿蒙开发……

虽然不可能学完每一项新技术,但是可以多去了解下。

总结

写了这么多,可能有人会问,如果能回到过去,你会怎么做。

啊,我只能说,说是一回事,做又是另一回事,事实上我并不希望回到过去去卷一遍,菜点没关系,快乐就好,一切都是最好的安排。

image.png

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

基于RT-Thread摄像头车牌图像采集系统 一、项目简介

发表于 2024-04-22

一、项目简介

使用基于RT-thread操作系统的AB32VG1开发板作为主控,对ov7670摄像头进行图像采集,并使用串口发送图片RGB565格式到PC供opencv进行图像识别。 原项目设想在开发板上进行采集的同时并通过简单的二值算法和插值算法实现车牌号识别,但实践中发现开发板的ram并不够保存采集回来的图像信息,与数据手册中介绍的192k有一定差距,实现用户能使用的ram是70k;同时原设想是带lcd屏幕的,但最后发觉io口数量不够,只能通过串口调试显示,但lcd屏幕的 spi代码仍保留在原码中,可供参考。 目前开发板通过摄像头采集完整数据部分已经完成,并且可以通过串口uart1发送到上位机进行图像显示。

二、硬件说明

1.摄像头ov7670带fifo:采用csi总线的普通30w摄像头。考虑到用模拟读取摄像头,io的反转速度可能不能满足高速采集的需要,因此保险起见,直接使用带fifo的摄像头。sccb总线采用全模拟的方式,跳过了所有中间层,直接操作寄存器,提高了总线的时钟。

2.串口工具PL2302(ttl转RS232),一款与pc通讯的串口工具,免驱。

3.总接线图

三、软件说明

1.软件流程图

2.关键代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
scss复制代码/* 摄像头IO口采用直接操作寄存器的方式实现,极大提升io速度 */
#define BSP_FIFO_RCK_PIN "PA.5"
#define BSP_FIFO_RCK_SET_LOW (GPIOA_BASE[GPIOx] &= ~(1ul << 5))
#define BSP_FIFO_RCK_SET_HIGH (GPIOA_BASE[GPIOx] |= (1ul << 5))

/* sccb总线的初始化并设置ov7670相应寄存器 */
sccb_init();

if(sccb_write_reg(0x12, 0x80) == RT_FALSE){
return RT_FALSE;
}

rt_thread_delay(50);

id1 = sccb_read_reg(0x0b);
id2 = sccb_read_reg(0x0a);

rt_kprintf("id1 = 0x%02x, id2 = 0x%02xn", id1, id2);

for(rt_uint16_t i = 0;i < sizeof(ov7670_init_reg_tbl) / sizeof(ov7670_init_reg_tbl[0]);i++){
sccb_write_reg(ov7670_init_reg_tbl[0], ov7670_init_reg_tbl[1]);
}

/* 开启摄像头vsync扫描线程(没有外部中断因此改用轮询的方式实现) */
rt_thread_t thread;
/* 查询VSYNC线程 */
thread = rt_thread_create("ov7670_vsync", ov7670_vsync_thread_entry, RT_NULL, 1024, 5, 100);

if (thread == RT_NULL){
rt_kprintf("ov7670_vsync thread create fail!n");
return RT_FALSE;
}
/* 启动线程 */
rt_thread_startup(thread);

/* 提取hal库实现了uart的数据发送函数 */
void uart1_send(rt_uint8_t *pbuf, rt_uint32_t len)
{
for(rt_uint32_t i = 0;i < len;i++){
hal_uart_clrflag(UART1_BASE, UART_FLAG_TXPND);
hal_uart_write(UART1_BASE, pbuf);
while(hal_uart_getflag(UART1_BASE, UART_FLAG_TXPND) == 0);
}
}


/* LCD底层驱动代码,因为引脚不够,所以无法演示,测试可用,另外程序里也配有寄存器版本的操作代码 */

static rt_uint32_t spi_bit_xfer(struct rt_spi_device *device, struct rt_spi_message *message)
{
struct rt_spi_bit_ops *ops = (struct rt_spi_bit_ops *)device->user_data;

rt_uint8_t tmp_buf[1024];
rt_memset(tmp_buf, 0, sizeof(tmp_buf));

if(message->send_buf == RT_NULL){
message->send_buf = tmp_buf;
}else if(message->recv_buf == RT_NULL){
message->recv_buf = tmp_buf;
}else{
return RT_FALSE;
}

if (message->cs_take){
ops->set_cs(ops->data, PIN_LOW);
}
#ifdef SPI_DC
message->length & SPI_DC ? ops->set_dc(ops->data, PIN_HIGH) : ops->set_dc(ops->data, PIN_LOW);
message->length &= ~SPI_DC; /* 复原消息长度 */
// rt_kprintf("message->length = %dn", message->length);
#endif
spi_rw_bytes(device, (rt_uint8_t *)message->send_buf, (rt_uint8_t *)message->recv_buf, message->length);

if (message->cs_release){
ops->set_cs(ops->data, PIN_HIGH);
}
}

static const struct rt_spi_ops spi_bit_bus_ops ={
RT_NULL,
spi_bit_xfer

};

优化思路: 1. 由于ab32vg1没有外部中断可以使用,ov7670的帧同步信号vsync只有500us的高电平时间,因此为了捕捉到该信号,vsync线程一直占用很多的资源; 2. 串口与上位通讯的速度目前最快只有115200bps,上位机可以接受256000bps的速度,但将驱动改为256000bps后,接收会出现乱码,因此串口使用的图片数据非常缓慢。

四、项目演示

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

架构演变史:从建筑学到架构设计

发表于 2024-04-22

“架构”(Architecture)的起源可追溯到建筑行业。它的英文“Architecture”源自拉丁语词“Architectura”,进一步溯源则发现其根本来自希腊语“Arkitekton”(αρχιτεκτων)。在这个希腊语词汇中,“Αρχι”(Archi)表示“主要的”或“首要的”,而“τεκτων”(tekton)意味着建筑师或工匠。因此,“Arkitekton”直译为“主建筑师”,揭示了“架构”一词背后的深层含义,指涉一种主导或核心的建设和创造角色。

1.1从建筑到社会文明

建筑,作为人类文明进步(如图1-2所示太和殿)的重要标志,与架构的概念紧密相连,深刻体现了人类社会发展的历程。在探索全球不同文明的历史进程中,无论是埃及、中国、印度、巴比伦、玛雅、希腊、苏美尔、印加还是波斯等古代文明,它们留下的宏伟建筑遗产都见证了人类文明的辉煌创造力和智慧。

图1-2

以奇琴伊察为例,这个位于墨西哥尤卡坦半岛的玛雅城邦遗址,展示了古玛雅文明的繁荣与智慧。这座城邦自公元514年建立起,成为古玛雅帝国中最具规模和繁华的城市之一,拥有卓越的艺术家、雕刻家、建筑师、工程师和天文学家。奇琴伊察的主要古迹,如千柱广场、武士庙、库库尔坎金字塔和古天文观测台等,都生动展现了玛雅人在建筑设计与空间利用方面的非凡才华。

建筑不单是一种文明的体现,它还是人类最早形成完善理论和实践经验的领域之一。历史上的文明社会在建筑方面积累了丰富的风格、技术和工艺。特别是在古代,建筑学的理论已经相当发达。如古罗马的建筑师维特鲁威(Vitruvius)在其著作《建筑十书》中提出,一个优良的建筑应当具备三个基本要素:持久性、实用性和美观性。这三个原则不仅影响了后世的建筑设计,也成为衡量建筑艺术的标准。

  • 持久性(Durability) 强调建筑的稳定性和耐久性。建筑物需要经受时间的考验,保持结构的完整性和功能的持续性。这要求建筑材料的选择和建筑结构的设计都必须能够抵御自然和人为的磨损,确保建筑能够长久地服务于人类社会。例如,古埃及的金字塔、中国的长城和罗马的斗兽场,都是持久性原则的杰出代表。
  • 实用性(Utility) 涉及建筑的功能性和适用性。建筑物不仅要满足人们的基本生活需求,还应考虑到使用的便捷性和舒适性。这意味着建筑设计要充分考虑空间布局的合理性、环境的适宜性和用户的体验,以确保建筑在实际使用中的功能性和效率。例如,现代住宅和办公楼就是在实用性原则指导下设计的,旨在为人们提供便捷、舒适的生活和工作环境。
  • 美观性(Beauty) 关注建筑的审美和艺术价值。一个美丽的建筑不仅能够提升周边环境的美感,还能激发人们的情感和灵感。建筑的美观性要求设计师充分运用创意和艺术手法,通过建筑的形状、色彩、材料和细节处理等元素,创造出既符合审美标准又能反映时代精神和文化特色的作品。例如,巴黎的埃菲尔铁塔、悉尼歌剧院和北京的国家大剧院都是美观性原则的典范。

在当代,架构的含义已经拓展,不再仅限于单一建筑物或结构的设计,而是涵盖了从城市规划、都市设计到景观建筑、建筑细节乃至内部家具设计的整个范畴。这种广泛的定义反映了建筑学作为一门综合性学科的丰富性,以及它在推动人类社会文明进步中的核心地位。随着时间的推移,建筑与架构不仅代表了一种艺术和科学的结合,也成为了人类智慧与创造力的象征。

1.2架构思想的跨领域应用

在长期的建筑行业实践中,人类积累了大量的宝贵经验和深刻见解,这些不仅在建筑领域内具有重要的指导作用,也对其他领域的人类活动产生了深远的影响。随着时间的推移和科技的发展,建筑学中的“架构”概念逐步演化,其含义和应用范围不断扩大,已经渗透到人类生活的各个方面。

几千年的建筑实践为人类留下了丰富的方法论、实践经验和教训,这些成果不仅对当代的建筑设计具有重要的实践指导价值,更重要的是,它们已经超越了建筑行业的界限,为其他工程学科领域提供了宝贵的参考和启示。这些经验和知识逐渐演变成一种固定的思维模式,成为了人类智慧和创造力的重要组成部分。

当“架构”这个词从建筑行业拓展到其他领域,如生物工程、航天工程、地理信息系统等,它展现出了全新的内涵和价值。尽管领域变化,但“架构”的内在精髓保持了一致性,本质上仍然是对结构内部元素及其相互关系的一种主观理解和映射。建筑设计中大量使用的空间、容量、纹理、光影、材料等元素,旨在确保设计元素之间在审美和功能上的和谐统一,这种思维方式与其他工程学科中追求功能性和结构优化的要求有着本质的相似性。

在计算机科学与工程这个较为年轻的学科领域中,虽然历史较短,人类以往在建筑实践中积累的经验和原则依然具有指导意义。例如,我们可以借鉴古罗马建筑师维特鲁威提出的持久性、实用性和美观性三大设计原则来指导系统设计。此外,建筑行业的其他经验和智慧,如路易斯·沙利文的“形式服从于功能”原则,也为其他设计领域提供了重要的参考和灵感。这些跨领域的应用和借鉴,充分展示了建筑领域经验和知识在人类社会发展中的广泛影响和深远意义。

1.3架构的形成

其实对于目前的记载,软件架构的具体起源已经很难被发现了,在1960年代,艾兹格·迪杰斯特拉等开始涉及软件架构领域,从1990年代后,软件架构这个概念开始变得愈发流行。而碰巧的是,1968年秋季,NATO科技委员会召集了一群优秀的软件工程师,来脑暴“软件危机”的解决方案,也是在这次会议上诞生了Software Engineering概念,即软件工程。

1993年,电气电子工程师学会(IEEE)给出了对于Software Engineering的定义:“将系统化的、规范的、可度量的方法用于软件的开发、运行和维护的过程,即将工程化应用于软件开发中”。

软件工程是面向工程领域的,软件工程包含软件架构的设计,而软件架构即是一张开发蓝图,是一个计划,是整体的规划,亦是软件工程的指导方针。

2.1技术中的架构到底是什么

通过前面的介绍,我们对架构的历史脉络有了一些基本的认识。我们来深入思考一下:这个经常出现在各种技术讨论中的“架构”,到底是什么意思呢?

是的,我们经常听到“架构”这个词,但是真正停下来思考它的定义和内涵,你会发现不是那么简单。在技术界,尤其是软件开发领域,“架构”几乎成了一个时髦的词汇,人人都在谈论它,但是真正理解它的精髓的人却不多。那么,什么是软件架构呢?它又为什么如此重要?

在我们继续深入探索之前,让我们首先澄清一下“架构”的基本含义。接下来的内容,我们将从软件架构的定义谈起,探索它的核心组成部分,然后再进一步讨论它在软件开发过程中的重要作用和价值。这样,我们不仅能够更好地理解架构这个概念,而且还能够明白为什么它对于构建高效、可维护的软件系统如此关键。

2.1.1架构的诞生

在探讨软件开发的众多新方法和理念时,我们发现“软件架构”的概念显得与众不同。不同于其他为应对新兴软件危机而诞生的理念,软件架构的出现似乎并不直接源于行业共面的某个特定问题。这里面有何玄机呢?

随着软件系统规模的日益庞大,一项明显的变化是,传统的计算算法和数据结构已不再是设计挑战的主要焦点。在一个由众多部件构成的系统中,如何高效地组织这些部件——即我们所称的“软件架构”——成为了设计师们面临的一系列新问题。比如,你可能遇到如下挑战:

  • 系统规模庞大至一定程度,内部耦合变得异常复杂,严重拖慢了开发效率;
  • 由于部件之间的紧密耦合,对系统的任何微小修改都可能引发连锁反应,使得后续的维护和扩展工作变得异常困难;
  • 复杂的系统逻辑使得问题频发,一旦出现问题,定位和修复的难度极大。

在这样的背景下,“软件架构”的概念应运而生,其历史地位和必然性不言而喻。回望过去,我们可以发现,第一次软件危机推动了“结构化编程”的发展,带来了“模块”的概念;随后,第二次软件危机促使“面向对象编程”的普及,引入了“对象”概念。而“软件架构”的提出,则标志着“组件”概念的诞生。

这一发展历程揭示了一个核心思想:无论是“模块”、“对象”还是“组件”,其本质都在于对达到一定规模的软件系统进行有效的拆分和高层次组织。随着软件的复杂度不断攀升,这种拆分的粒度和层次也随之提高,从而更好地应对软件开发过程中遇到的各种挑战,提高软件的开发效率和系统的可维护性。

2.1.2架构指什么

在我们程序员的世界里,“架构”这个词几乎无处不在。它就像是我们的老朋友,经常挂在嘴边。但是,当你真正停下脚步,试图去探究一下这个问题:“架构”究竟指的是什么?你可能会惊讶地发现,这个问题并没有想象中那么简单。其实,如果你随便问1000个技术人员,“架构”的定义是什么,你可能会得到1001种不同的答案。这就像是一个技术界的谜题,每个人心中的答案都有微妙的差异。

那么,在这个众说纷纭的情况下,我们如何才能找到对“架构”的准确理解呢?一个有效的方法是先从理解与“架构”紧密相关且相似的几对概念开始。我们可以聚焦于三对基本但至关重要的概念:系统与子系统、模块与组件、以及框架与架构。

2.1.2.1系统与子系统

当我们聊到“系统”,可能首先想到的是各种复杂的技术或机械设备。维基百科给出了一个非常宽泛但精确的定义:

系统泛指由一群有关联的个体组成,根据某种规则运作,能完成个别元件不能单独完成的工作的群体。

我们来简化一下上面的高深论述,抓住几个核心点:

  • 关联:想象一下,仅仅把一个发动机和一台电脑摆在一起,并不能让它们变成什么特别的东西,对吧?但如果你把发动机、底盘、轮胎和车架这些有关联的部分放在一起,它们就能组合成一台汽车。这就是说,系统里的每个部分都得有点关系,才能一起工作形成一个有用的整体。
  • 规则:在这个组合里,每个部分不是随便干自己的事。它们需要按照一定的规则来操作。比如说,汽车里的发动机产生动力,通过变速器和传动轴这样的装置,最终把动力传到轮胎上,让汽车能够前进。这些规则决定了谁负责干什么,怎么协作。
  • 能力:当这些部件按规则合作时,整个系统就能做到单个部件做不到的事情,比如汽车的载人载物。这种系统的能力,不是简单地把各个部件的能力加起来那么简单,而是能创造出新的能力来。

再说说子系统,其实它的概念和系统差不多,只不过是从另一个角度来看。一个系统在更大的环境中,可能就是另一个系统的子系统:

子系统由一群有关联的个体所组成的系统,多半会是更大系统中的一部分。

子系统和系统的概念其实是一回事,只是取决于你站在哪个视角看问题。一个系统在更大的环境中就可能成为另一个系统的子系统。这听起来可能有点绕,但如果我们用微信这个例子来说明,一切就变得清晰多了。

  • 微信作为一个系统:首先,把微信想象成一个庞大的生态圈,它自身就是一个系统。在这个生态圈里,有许多功能和服务,如聊天、登录、支付、朋友圈等。这些功能在微信这个大系统中,实际上都是独立运行的子系统,每个子系统负责不同的任务,但共同支撑起微信这个巨大的服务平台。
  • 朋友圈的结构:再深入到朋友圈,我们可以看到它不仅仅是微信的一个功能,实际上它自己也是一个由多个功能组成的系统。比如,动态发布、评论、点赞等功能,在朋友圈这个“小系统”里,它们各自也可以被看作是独立的子系统。
  • 评论功能的深层次:评论功能虽然是朋友圈的一部分,但如果我们仔细分析,会发现它自身也包含多个子系统,如防刷子系统、审核子系统、发布子系统、存储子系统等。这些子系统通过精密的协作,共同确保了评论功能的正常运行和数据的安全性。
  • 技术层面的系统:当我们聚焦于评论审核这一环节,它可能不再被划分为更多的业务子系统,而是转向技术实现的视角。在这个层面,各种技术模块和组件,如数据库系统MySQL、缓存系统Redis等,它们虽然是从技术角度构成的系统,但同样在整个评论功能中扮演着关键的角色。这些技术组件确保了数据的快速处理和安全存储,虽然它们不直接参与到业务流程中,却是支撑整个系统运行不可或缺的部分。

这种从整体到部分,再从部分回到整体的视角,不仅帮助我们理解了系统内部的复杂结构,也让我们明白了在设计和构建复杂系统时,如何通过层次分明的子系统来管理和简化这种复杂性。每个子系统的设计和实现,都需要考虑如何与其他子系统协作,以及如何在满足当前功能需求的同时,保持系统的灵活性和扩展性。

通过微信这个例子,我们得到的不仅仅是对系统和子系统理论的深入理解,还有对实际应用中这些理论如何被运用以解决实际问题的洞见。这种理论与实践的结合,是软件工程中不可或缺的一部分,它指导我们如何更好地设计和优化软件系统,以适应不断变化的需求和挑战。

2.1.2.2模块与组件

要深入理解软件系统的拆分,我们可以从两个不同的视角来探讨:逻辑和物理。这就像是看待一个复杂机器的两种方式。逻辑上的拆分产生了我们所说的“模块”,而物理上的拆分则给我们“组件”。

  • 模块:把这个概念想象成是把一本厚厚的教科书分成不同的章节,每一章节专注于讲解一个特定的主题。在软件里,模块就是这样一个逻辑上的单位,它封装了一系列功能,这些功能紧密相关,共同完成一项或几项特定的任务。划分模块的目的很明确:为了让我们的代码更加有条理,每部分都有明确的职责,这样不仅使得代码更易于理解和维护,还能在团队中更高效地协作。
  • 组件:再想想组件,可以将其比喻为乐高积木中的每一块积木。这些积木是实实在在的,你可以用它们来构建各种各样的结构。在软件开发中,组件是物理上的实体,可以在不同的系统中重复使用。它们就像是标准化的零件,可以根据需要组装或更换,极大地提高了开发的灵活性和效率。

提到“组件”的英文“component”,如果我们将其译为“零件”,可能会更加形象和易懂。这个词帮助我们更好地把握组件的本质:它们是独立的、可以互换的物理单位,正如机械中的零件一样,具备了可插拔和可复用的特性。这种物理上的拆分不仅让我们的系统更加模块化,还为系统的升级和维护提供了极大的便利。

通过逻辑和物理拆分,我们能够更精细地管理和构建复杂的软件系统,使之既有条理又高效。模块化和组件化的设计思想,是现代软件工程中不可或缺的一部分,它们使得软件开发像搭建乐高一样既有趣又富有创造性。

2.1.2.3框架与架构

单纯从定义的角度来看,框架关注的是“规范”,架构关注的是“结构” 。框架(Framework)为你的项目提供了一套预设的工具和库,确保你可以遵循特定的规范和模式来构建应用。想象一下,如果你正在搭建一座房子,框架就像是提供给你的施工套件和详细指南,它指导你每一步如何建造,确保每一部分都能正确地承担起它的作用。架构(Architecture),则更像是整个系统的设计方案,它详细规划了系统的组织方式,展示了组件如何互相配合,数据如何流转,以及系统如何对外提供服务。这相当于在规划整个房子的布局,决定哪里放置客厅,哪里是厨房,以及如何确保房子既满足居住功能又具有美观性。

我们经常会说,“工程采用的是MVC架构”、“工程使用的是SSH框架”等。这里,第一句话是站在结构的层面来说明,它像是在描述房子的设计理念和内部结构,告诉你这个项目是如何划分不同的功能区域,以及这些区域是如何相互关联的。第二句话是站在规范的层面来说明,它更多关注于建造过程中应该遵循的标准和规则,比如使用什么样的技术栈,以及如何利用这些技术来实现预定的功能。

同时,不同的视角会影响我们对架构的理解和描述,例如:

  • 从业务逻辑的角度分解,“后台管理系统”的架构(图1-3所示),我们会关注系统是如何处理用户的注册、信息管理、业务选择和数据统计等业务流程的。这个角度让我们深入了解系统是如何支持核心业务需求的。

图1-3

  • 从物理部署的角度分解,“后台管理系统”的架构(如图1-4所示),则关注系统的硬件布局,如何通过服务器集群、负载均衡等技术手段来保证系统的高可用性和高性能。

图1-4

  • 从开发结构的角度分解,“后台管理系统”的架构(如图1-5所示),我们则更加关注代码的组织方式、模块的划分以及技术框架的选择,以便于团队协作、功能迭代和系统维护。

图1-5

通过不同的视角,我们不仅能够更全面地理解框架和架构的概念,还能够根据项目的实际需求,从多个维度评估和选择最合适的架构设计和技术框架。这种多角度的思考方式对于软件开发是非常重要的,它帮助我们构建出既稳健又灵活的系统,能够适应不断变化的业务需求和技术挑战。

2.1.3架构的定义及边界

谈到“架构是什么”,你可能会发现这个问题没有那么简单。其实,不同的公司、不同的团队,乃至于我们每个人,对于“软件架构”的理解都各不相同。这就像我们不能百分之百精确描述一个复杂的模型一样,我们只能从多个角度来看,尝试给出一个大概的描述。所以,想要给出一个完全无懈可击的“架构定义”,那基本是不可能的。

“道可道,非常道。名可名,非常名”。

这个行业内部,充满了各种各样的声音和观点。有的组织可能会从技术的实用性出发,定义架构是一种确保系统稳定、高效运行的布局;有的个人可能更注重架构在解决复杂问题中的策略角色,视之为一种艺术。每个人都在尝试从自己独特的视角,解读和定义“架构是什么”,这就像是在无数的色彩中寻找那一抹最为合适的蓝,既充满了个人色彩,也反映了这个领域的多元和复杂。

2.1.3.1架构的定义

the fundamental organization of a system, embodied in its components, their relationships to each other and the environment, and the principles governing its design and evolution –ANSI/IEEE

IEEE 关于架构的这个概念:其实,把它想象成一个三明治,上面是系统的大架构,中间夹着的是各个组件以及它们之间的纽带(这里的纽带,既包括组件彼此之间的联系,也涵盖了它们与外界环境的互动),底下则是一套行动准则。咱们若是用图形来表达这个定义,那么制作出来的图示应该既清爽又直观,能够将架构的主要组成部分和核心思想,一目了然地展示出来(如图1-6所示):

  • 系统的大架构:这部分其实就是在讲,让人一眼就能看懂的系统整体布局。就像是给你一张地图,上面标注了所有重要的地标和路径,让你对整个地形有个基本的认识。
  • 组件及其关系:这里的意思是,把整个系统拆分成一个个模块,这样不仅便于理解和管理,同时也强调了这些模块之间,以及模块与外部环境之间的相互作用和联系。就像是一张复杂的网络图,每个节点都有它的位置和作用,而线则表示了节点之间的连接。
  • 行动准则:这部分提供的是设计和演进系统时需要遵守的规则和指导原则。就好比是一本指南,告诉你在追求目标的过程中,哪些是应该坚持的原则,哪些是需要避免的错误。

图1-6

大师 Martin Fowler对于架构的定义有着更加简洁的抽象,Martin Fowler 认为软件架构是:重要并且难以改变的决策。架构设计是关于权衡的艺术,架构设计过程中充满了各种各样的决策,这些决策也终将反应系统架构。

在讨论软件架构这个课题时,大师 Martin Fowler 给出了自己的看法,既精炼又深刻:软件架构,那就是那些特别重要而且改起来头疼的决策。这就好比是在说,架构设计其实就是一个充满智慧的权衡过程,你在这个过程中做出的每一个决策,都像是在未来的系统蓝图上,悄悄地加上了你的个人签名。

Software Architecture = Important and hard to change decisions –Martin Fowler

而 Ralph Johnson 这位大佬,给出了一个更加哲学的定义,他说,软件架构嘛,简单点说,就是那些重要的东西,具体是啥呢?他似乎在告诉我们:这个问题,答案就像是宇宙的终极问题,重要的是你怎么去理解它!

The software architecutre is the important stuff ! Whatever it is ! –Ralph Johnson

与之相比,Neil Ford 则像是那位脚踏实地的实践者,他不在云端飘,而是带着工具箱走到了前线。他从实际操作的角度出发,详细列出了构成软件系统架构的基石(如图1-7所示):

  • 结构:这不仅仅是选一个风格的问题,而是定义了整个应用的骨架,是微服务、是单体,还是服务导向架构(SOA),每一种选择都像是在向系统注入不同的灵魂。
  • 架构属性:这些看不见摸不着的特性,比如性能如何、可用性高低、维护起来是不是友好,它们决定了系统能否在残酷的环境中生存下来。
  • 架构决策:在设计系统的过程中,那些至关重要的选择点,就像是在茫茫大海中航行的舵手,一次又一次地决定着软件的未来方向。
  • 设计原则:这些原则就像是老船长的智慧,指引着设计者避开暗礁,顺利抵达目的地。

图 1-7

2.1.3.2结构

结构是系统架构的重要组成部分,其从宏观上表述了系统的结构组成。架构设计的核心任务之一是为系统选择合适的架构风格。架构师需要基于上下文的权衡,可以选择模块化单体架构风格,也可以选择微服务架构风格。

说到架构中的“结构”,这块可不是随便一说就能过去的。它在整个系统架构里面占的位子,就好比心脏在人体里的角色一样重要。它从一个宏大的视角,告诉我们这个系统是由哪些部分组成的,每部分又是怎么协同工作的。搞架构设计的时候,挑选一个恰到好处的架构风格,基本上就是架构师的核心日常了。

举个例子,架构师们在面对具体的项目时,得先在大脑中过一遍这个系统的全貌,然后根据这个系统要面对的各种情况和挑战,来做一个权衡。这时候,他们可能会选择走模块化的单体架构风格,这种方式好处是简单、直接,容易管理;但如果项目特别复杂,服务之间需要高度的解耦,那微服务架构风格可能就更合适了,虽然这样会带来更多的管理和协调工作,但它能提供更好的灵活性和扩展性。

“选择合适的架构风格”的过程(如图 1-8所示),其实就像是在做一场精心策划的活动,架构师需要凭借自己的经验和对项目需求的深刻理解,做一个详细的规划,希望能赢得未来的便利和效率。这个过程充满了挑战,但也正是这些挑战,让架构设计成为了一门艺术。

图 1-8

2.1.3.3架构属性

在聊软件架构的时候,绝不能忽视的一个重点就是所谓的架构属性,也就是大家常说的质量属性或非功能属性。这些属性,其实就是在描述系统应该具备的一些“超能力”,比如能跑得快(高性能)、能灵活扩大或缩小规模(可扩展性和伸缩性)、碰到问题不崩溃(弹性和容错性)、好测好修(可测试性和可维护性)等等。设计架构的时候,我们的一个核心任务就是要确保系统能够满足这些架构属性的要求,因为它们直接关系到系统能否在野蛮生长的环境中存活下来。

但这里头有个问题,那就是架构属性多得跟星星一样数不清,我们必须明白,在不同的项目和场景下,重点关注哪一部分属性。这就要求架构师得根据具体的问题域和上下文环境,来做出精准的分析和选择。比如,在一个对性能要求极高的金融交易系统中,高性能和容错性可能就是你得重点关注的属性;而在一个云服务产品中,可扩展性和伸缩性可能更为关键。

同时,架构设计不是一帆风顺的,这些架构属性(如图 1-9所示)之间很可能存在着某种程度的冲突。例如,追求极致的性能可能会牺牲一定的可维护性和可测试性。这种时候,架构师就得像是在玩一个平衡游戏,既要保证系统的核心能力,又要尽量避免负面影响,这就需要他们运用自己的经验和智慧,做出恰当的权衡和决策。

架构设计其实就是一个不断权衡和选择的过程,旨在打造一个既强大又平衡的系统。这个过程中,架构师的角色就像是一位艺术家,既要有科学的严谨性,又要有艺术的创造力。

图1-9

2.1.3.4架构决策

当我们深入探讨软件架构设计的精髓时,架构决策这个概念浮现为其核心。它不仅仅是关于解决方案的选择,更是一系列必须遵守的规则的体现。这些规则或决策,正如导航灯指引船只航向,为整个系统设计指明方向。在这个过程中,要认识到并非所有决策都能被称为架构决策。只有那些对系统有着重大且深远影响的决策,才配得上这个称号。比如说,决定采用何种架构风格,这不仅会深刻影响到系统的整体设计,一旦设定,更改的代价极为昂贵,因此,它无疑属于架构决策的范畴。

架构决策的范围广泛,内容丰富,它们包括但不限于以下几个关键领域:

  • 直接对架构属性中的优先级高的问题产生影响:这意味着任何影响性能、可扩展性、安全性等关键架构属性的决策都是至关重要的。
  • 修改对外接口:这类决策需要进行周全的影响分析,因为它们可能会对系统与外部世界的交互方式产生根本性的改变。
  • 引入或移除依赖:依赖关系的变化标志着系统功能的增减,对架构的影响深远,需要慎重考虑。
  • 改变系统的通用结构:这关乎到系统的基础架构布局,任何调整都是对系统架构的重大干预。
  • 促使开发团队改变开发模式:这类决策往往涉及到开发流程和文化,对团队的工作方式和产品的质量都会产生深远的影响。
  • 承担战略性技术债务:有时为了项目的长期发展,需要做出一些短期内看似不利但长远来看有益的技术选择,这些决策对系统的未来发展至关重要。

这些架构决策,远不止是技术层面的选择,它们背后蕴含的是对项目未来方向的深思熟虑。一个正确的架构决策可以为项目带来顺利的发展,而错误的选择可能导致项目陷入重构甚至失败的困境。因此,在架构设计过程中,架构师需要具备前瞻性和深度的思考能力,确保每一个决策都能促进项目向着正确的方向发展。

2.1.3.5设计原则

设计原则和架构决策,这两者在软件设计的世界里,就像是指南和指导的关系。两者都指引着你前行的方向,但具体的用法和意义,却大有不同。核心区别在这儿:设计原则,它更像是一位经验丰富的前辈,给你提供的那些指导和建议,并不会强迫你一定要这么做。而架构决策,那就相当于设计的规章制度,一旦确定下来,你就必须得遵守。

举个例子来说得更明白一点,设计原则可能会这样建议你:在软件系统间的通讯上,尽可能利用异步消息机制。这么做的好处是啥呢?首先,它能够提升系统的性能,让你的应用跑得更快;其次,还能降低系统间的耦合度,让系统之间的关系更加松散,互不干扰。这就好比是在教你如何在繁忙的城市中开车,不仅能高效地从A点到B点,还能在路上享受风景,避免不必要的麻烦。

但这都是建议,它不会对你说:“你必须得这么干!”它留给你足够的空间去探索和实验,找到最适合你自己项目的方法。而一旦我们谈到架构决策,比如你决定用微服务架构而非单体架构,这就像是签了一个契约,需要你严格遵守,因为它关系到整个项目的基础和未来的可维护性。

在设计和构建软件系统的时候,理解设计原则与架构决策之间的这种微妙差异,就显得尤为重要。它们一个给你灵感和自由,一个给你方向和规则,正确地运用它们,就能让你的软件设计既灵活又稳健。

2.1.3.6总结

在深入探讨软件架构的定义时,我们会发现行业内各位大师提供了多样化且各具特色的解读,它们就像是不同风格的画派,每一种都有其独到之处:

  • IEEE的定义,就好比是经典主义画派,追求结构的严谨和形式的规范,给我们提供了一个架构定义的标准框架,像是在告诉我们,建筑的每一砖每一瓦都要符合建筑学的原则。
  • Martin Fowler的角度,则像是强调了画作中的构图决策,就如同在绘画中选择主题和色彩的重要性一样,他告诉我们在架构设计中,关键的决策是构筑整个系统的基石。
  • Ralph Johnson的定义更像是抽象艺术,他没有给出具体的形式,而是强调了“重要性”这个核心元素,就像是抽象画中强调情感和概念的表达,让我们理解到架构的核心在于其重要的价值和作用。
  • Neil Ford的看法则更接近于现实主义,他通过具体化的描述,就像是通过细腻的画面描绘出生活的样态,让我们能够清晰地理解和操作架构的具体元素。

在这些定义中,我个人特别偏爱Ralph Johnson的抽象化定义,它简洁而深刻,像是用一笔带过却又点出架构本质。在我的工作实践中,这种将复杂问题抽象化的能力,成为了我判断和理解架构边界时的重要准则。就像是在画画时,虽然不被具体形式所束缚,但能够深刻把握作品的灵魂,这样的定义不仅指导了我的架构设计思路,也帮助我在面对复杂系统时,能够更加聚焦于那些真正重要的事情。

2.1.4架构的目标

其实搞架构的根本目的,就是为了解决软件系统那头疼的复杂度问题。要想搞好架构,核心就是学会怎么做决策和选择。

你面对的是一个有限制的“盒子”,这个盒子代表了你在设计软件时必须考虑的各种约束条件,比如团队的经验水平、预算成本、可用资源、时间限制,以及业务的当前阶段等。这些因素交织在一起,构成了你必须在其中寻找最优解的复杂环境。

在这样的背景下,架构师的任务就变得清晰了:深入分析这个复杂的“综合体”,理解它对系统架构复杂度的影响,然后在这个基础上做出合理的判断和选择。这意味着,你需要评估不同架构方案的优劣,考虑它们如何适应或改变现有的约束条件,最终设计出既符合需求又能在给定的条件下发挥最大效能的软件架构。

这个过程并非一成不变,而是需要随着项目进展、团队能力的成长以及外部环境的变化而不断调整和优化。因此,作为一名架构师,不仅需要具备深厚的技术知识和经验,还需要有前瞻性的思维和灵活应变的能力,以确保在不同阶段做出最适合项目的架构决策。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SwiftUI 学习笔记【02】:新建SwifUI项目

发表于 2024-04-22

在本文中,你将学会如何使用xCode创建一个SwifUI项目

1 首先,启动 xCode 之后,根据当前是否已经打开已有项目,分两种情况来创建 SwiftUI 项目,
1.1 若没有打开已有项目,显示如下界面的话,则点击“Create New Project…”菜单项

image.png

1.2 若已打开已有项目,在顶栏创建一个新项目,选择File,New,Project

image.png

2 接着选择 iOS 页签,再选择 Application 下的 App,最后点击“Next”按钮

image.png

3 完善要创建的项目的基本信息

image.png
在上面的截图中,完善大红框里面的内容,

  • 项目名称(Project Name):FirstApp;
  • 组织名称(Team):可以设置为“公司”或“个人组织”,也可以选择 None
  • 组织标识符(Organiztion Identifier):一般使用反向的域名,例如:com.exampe;
  • 反向域名(Bundle Identifier):自动生成,组织标识符+项目名称;
  • 用户界面(Interface):选择SwiftUI;
  • 语言(Language):选择Swift;
  • 语言(Storage):选择None;

完善项目信息后,点击“Next”按钮

4 选择保存目录

image.png
在这一步选择要保存的目录,若勾选了“Create Git repository on my Mac”,xCode将为该项目初始化 Git 仓库,至此你就成功创建了一个SwiftUI项目。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

一文带你了解AI程序员DEVIKA 1 引言 2 DEV

发表于 2024-04-22
  1. 引言

随着人工智能技术的飞速发展,其能力正逐渐比肩人类。在软件工程领域,AI程序员如 Devin 和 AutoDev 已经成为热门话题。这些人工智能工具不仅极大提升了软件开发的效率和自动化程度,而且预示着程序员工作方式可能会发生重大变革。

以 Cognition Labs 推出的历史上第一位 AI 程序员 Devin 为例,其可以从头到尾独立规划和完成软件工程任务,无需人工协助即可完成复杂的编码任务。在 SWE-Bench 基准测试中,Devin 在没有任何帮助的情况下(目前有爆出 Devin 造假)解决了 13.86% 的问题,显著超过了之前表现最好的模型,这表明 Devin 已经超越了其他人工智能工具,可以作为一名自主程序开发工程师。(首个AI程序员造假被抓,Devin再次“震撼”硅谷)

image.png

遗憾的是,Cognition Labs 未披露支持 Devin AI 的人工智能模型的技术细节,现在也未对外开放使用,无法及时体验到。幸运的是,由印度 Stition AI 模仿 Cognition Labs 开发的 Devin AI的替代品 Devika AI 已开源,Devika AI 的目标是达到 Devin 在sw基准测试中相同的分数,并最终战胜它。

接下来,本文将为大家介绍 Devika AI 的实际能力、架构以及实际使用体验。

  1. DEVIKA AI 介绍

Devika AI,由印度团队 Stition AI 精心打造的一位人工智能软件工程师。它具备理解复杂人类指令的能力,并能将其拆解为一系列可执行的步骤。通过深入研究和信息搜集,Devika 能够编写出实现人类目标指令的代码。Devika AI 的主要目标是通过提供能够在最少的人工指导下处理复杂编码任务的 AI 结对程序员来彻底改变软件开发过程。

首先,介绍一下 Devika 处理人类指令的基本流程:Devika 通过大模型实现自动拆解任务、自动下发任务或交付任务、以及自动将用户最近的询问转为行动指令等流程,直到完成用户的指令。

目前, Devika 支持的行动指令有6 种,我们与 Devika 对话时应贴近这些指令,效果会更好。

1
2
3
4
5
6
markdown复制代码- `answer` - Answer a question about the project.
- `run` - Run the project.
- `deploy` - Deploy the project.
- `feature` - Add a new feature to the project.
- `bug` - Fix a bug in the project.
- `report` - Generate a report on the project.

Devika 的基本流程如下图所示:

Devika 基本流程

Devika 的系统架构由以下关键组件组成:

  1. 用户界面:基于网络的聊天界面,用于与 Devika 交互、查看项目文件以及监控代理的状态。
    image.png
  2. Agent Core:编排AI规划、推理和执行过程的核心组件。它与各种子代理和模块通信以完成任务。
  3. 大型语言模型:Devika 利用Claude、GPT3.5/4和Ollama 的本地 LLM等最先进的语言模型进行自然语言理解、生成和推理。
  4. 规划和推理引擎:负责将高层目标分解为可操作的步骤,并根据当前环境做出决策。
    image.png
  5. 研究模块:利用关键字提取和网页浏览功能来收集当前任务的相关信息。
    image.png
  6. 代码编写模块:根据计划、研究结果和用户需求生成代码。支持多种编程语言。
  7. 浏览器交互模块:使 Devika 能够根据需要导航网站、提取信息并与 Web 元素交互。该模块使用 Playwright 实现:Playwright提供了跨浏览器、跨平台的支持,可以模拟用户在浏览器中的交互行为,如点击、填写表单、截图等
  8. 知识库:存储和检索项目特定信息、代码片段和学到的知识,以便高效访问。
  9. 数据库:保存项目数据、代理状态和配置设置。

Devika 整体架构如下图所示:

img

  1. DEVIKA AI 安装配置

安装过程请参照官方文档:GitHub - stitionai/devika

安装及使用过程中可能需要注意以下三点:

  1. 环境要求
1
2
3
diff复制代码- Python >= 3.9 and < 3.12
- NodeJs >= 18 # UI运行要求
- bun
  1. 模型配置:可以在代码跑起来后,通过设置界面修改配置文件,也可直接修改config.toml配置文件。
    推荐使用 Google Gemini 1.0 Pro,可以白嫖。
    image.png
  2. 科学上网:大模型 API 接口基本都有 IP 校验,需要科学上网。本文是在本地构建运行,需要配置VPN 和终端代理绕过 IP 检测。
    例如,本文运行设置的终端代理如下:
1
shell复制代码export https_proxy=http://127.0.0.1:7890 http_proxy=http://127.0.0.1:7890 all_proxy=socks5://127.0.0.1:7890
  1. DEVIKA AI 使用体验

4.1. 完整视频 demo:创建一个简单的贪吃蛇游戏

这是一个要求 Devika 创建一个简单的贪吃蛇游戏的完整录屏 demo:
录屏

Devika 自动化的开始拆解并完成该任务流程,并通过导航网站、提取信息来提供更多有用的信息给大模型。

起初生成的代码不符合要求,经过人工介入提示可以慢慢修复代码,Gemini 对中文的支持不是很友好,修复 bug 的很多命令识别成了 run,更换 GPT 会更佳。生成的最终代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
python复制代码import pygame
import random

pygame.init()
screen = pygame.display.set_mode((640, 480))
clock = pygame.time.Clock()

snake = [(320, 240), (320, 240), (320, 240)]
food = (random.randint(0, 59) * 10, random.randint(0, 47) * 10)
direction = pygame.K_RIGHT

running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_UP and direction != pygame.K_DOWN:
direction = pygame.K_UP
elif event.key == pygame.K_DOWN and direction != pygame.K_UP:
direction = pygame.K_DOWN
elif event.key == pygame.K_LEFT and direction != pygame.K_RIGHT:
direction = pygame.K_LEFT
elif event.key == pygame.K_RIGHT and direction != pygame.K_LEFT:
direction = pygame.K_RIGHT

if snake[0] == food:
food = (random.randint(0, 59) * 10, random.randint(0, 47) * 10)
snake.append((0, 0))
else:
for i in range(len(snake) - 1, 0, -1):
snake[i] = snake[i - 1]
if direction == pygame.K_UP:
snake[0] = (snake[0][0], snake[0][1] - 10)
elif direction == pygame.K_DOWN:
snake[0] = (snake[0][0], snake[0][1] + 10)
elif direction == pygame.K_LEFT:
snake[0] = (snake[0][0] - 10, snake[0][1])
elif direction == pygame.K_RIGHT:
snake[0] = (snake[0][0] + 10, snake[0][1])

if snake[0][0] < 0 or snake[0][0] > 630 or snake[0][1] < 0 or snake[0][1] > 470:
running = False
for i in range(1, len(snake)):
if snake[0] == snake[i]:
running = False

screen.fill((0, 0, 0))
for part in snake:
pygame.draw.rect(screen, (255, 255, 255), pygame.Rect(part[0], part[1], 10, 10))
pygame.draw.rect(screen, (255, 0, 0), pygame.Rect(food[0], food[1], 10, 10))

pygame.display.update()
clock.tick(10)

pygame.quit()

该代码的贪吃蛇游戏比较简单,更复杂的代码建议配合 GPT4 等更优秀的大模型使用。

4.2. 使用 Pygame 实现 Conway’s Game of Life

康威生命游戏( Conway’s game of Life)是官方演示的一个 demo,本文接下来对这个 demo 进行复现。

简要介绍一下康威生命游戏:该游戏在方格网上进行,有点像围棋。有填充的网格代表有生命,或理解成一个细胞,再或者按中国传统,把填充和无填充理解成“有”和“无”。游戏规则只有四条:

  1. 当周围仅有1个或没有存活细胞时, 原来的存活细胞进入死亡状态(模拟生命数量稀少)
  2. 当周围有2个或3个存活细胞时, 网格保持原样
  3. 当周围有4个及以上存活细胞时,原来的存活细胞亦进入死亡状态(模拟生命数量过多)
  4. 当周围有3个存活细胞时,空白网格变成存活细胞(模拟繁殖)

康威生命游戏的四条规则一目了然地对应着宇宙中的生命规律,它是一种元胞自动机(cellular automaton),体现了冯·诺依曼(Von Neumann)关于机器自我进化的思想。

下面,让我们深入了解实际的使用体验:

首先使用本地模型 Llama2 测试:

image.png

llama2 能正常分解并完成该任务流程,并通过导航网站、提取信息来提供更多有用的信息给大模型,但**最终生成的代码仅有print(“Example”) **。

经过排查源码,本质是因为 Devika 的prompt 包含这样的代码,以规定大模型返回代码的格式。

image.png

继续排查日志记录,发现在纠错过程中有返回任务代码相关的记录。然而,Llama2 模型没有以 Devika 预期的格式提供响应,在回复中包含了额外的解释、论证部分,导致 JSON 验证失败,Devika 继续向模型提示返回指定格式的修复代码,模型返回了预期格式的 JSON 但错误的代码,即上述提到的提示词。

image.png

本文根据 issue 修改 prompt 来显式通知模型遵循所请求的输出格式后,实际测试 llama 也没有改善这个问题。

image.png

接下来,实际体验一下 Google 大模型 Gemini 1.0 Pro:

Devika 按照既定流程拆分并推进完成指定的任务流程,并从网页提取浏览信息,最后返回了预期格式的代码,生成代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
python复制代码import numpy as np
import pygame

# Initialize Pygame
pygame.init()

# Game settings
SCREEN_WIDTH = 500
SCREEN_HEIGHT = 500
CELL_SIZE = 10
FPS = 30

# Colors
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)

# Create the game screen
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Conway's Game of Life")

# Create the game grid
grid = np.zeros((SCREEN_WIDTH // CELL_SIZE, SCREEN_HEIGHT // CELL_SIZE))

# Game loop
running = True
while running:
# Handle events
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False

# Update the game grid
for i in range(grid.shape[0]):
for j in range(grid.shape[1]):
# Count the number of alive neighbors
neighbors = 0
for x in range(-1, 2):
for y in range(-1, 2):
if x == 0 and y == 0:
continue
if i + x >= 0 and i + x < grid.shape[0] and j + y >= 0 and j + y < grid.shape[1] and grid[i + x, j + y] == 1:
neighbors += 1

# Apply the game rules
if grid[i, j] == 1:
if neighbors < 2 or neighbors > 3:
grid[i, j] = 0
else:
if neighbors == 3:
grid[i, j] = 1

# Draw the game grid
screen.fill(BLACK)
for i in range(grid.shape[0]):
for j in range(grid.shape[1]):
if grid[i, j] == 1:
pygame.draw.rect(screen, WHITE, (i * CELL_SIZE, j * CELL_SIZE, CELL_SIZE, CELL_SIZE))

# Update the display
pygame.display.update()

# Clock tick
pygame.time.Clock().tick(FPS)

# Quit the game
pygame.quit()

尝试在本地运行 Devika 生成的代码,能拉起终端,但是屏幕上看不到白色活细胞的演化进程。提示 Devika 自行修复未能成功。

继续让 Devika 使用正确代码(John Conways Game of Life in Python)替换掉他生成的代码并运行,经过一次人工提醒后Devika 完成了这个任务。

image.png

image.png

让他再次运行替换的代码,他会在当前项目重复创建文件,且再次使用的data/projects/当前项目名称的路径。即使后续我给出本地路径,也不会按照我的要求去完成,看了下项目代码,路径是可配置的,根据配置的路径写死的,有灵活性但不多。

image.png

从以上体验可以看出模型的优劣对 AI 程序员体验是至关重要的,本文后续均使用 Gemini 1.0 Pro 模型进行测试。

4.3. 克隆项目并编写 vue3 虚拟滚动列表组件

1
bash复制代码克隆***项目并安装依赖,克隆地址(https://github.com/***),完成后编写一个虚拟滚动列表的 vue3 组件,放在指定目录 packages/***/components/gemini/下

任务拆分毫无问题,但实际表现是并未按照步骤克隆项目。只增加了编写组件,但未放至指定文件路径下。

按照进一步提示,Devika 能完成克隆项目。Devika 完成稍微复杂指令任务的能力较弱。

image.png

4.4. 提供本地项目代码写单元测试

1
vbnet复制代码请为本地路径/Users/xxx/xxx/game-private/packages/shared/utils/format.ts的所有函数编写单元测试,要求语言使用jest

除了图中 format 文件,本文还让 Devika 为一个 Vue 组件编写单元测试, 实际体验下来证明, Devika 不会读取本地文件内容把它当作大模型的提示,总结就是不够智能。

人工提示他读取本地文件内容,日志报错提示文件不存在(Devika 只会读取默认配置路径下的文件),他并不能准确获取到本地文件路径。

image.png

4.5. 验证 Devika 知识库

新建项目,让 Devika 完成之前做过的任务(prompt 完全一致)。Devika 不会再通过 web 提取信息,也不会再通过模型生成代码,而是直接返回知识库的答案。

image.png

查看知识库相关的源码,发现只是简单的查询相等判断,可用性不大。

image.png

4.6. 使用体验小结

  1. Devika 提供了规划和推理引擎,能够将高级目标拆分为可操作的步骤,将用户后续提问或者指示统一转化为规定的行动指令
  2. 浏览器交互模块使 Devika 能够导航网站、提取信息以及与 Web 元素交互。此功能增强了 Devika 收集数据和提供准确代码生成的能力,收集的 web 信息通过截图展示给用户
  3. Devika 的代码生成质量严格依赖于大模型的代码生成质量
  4. Devika 遇到错误能自我纠正,但不稳定,执行一些指令遇到错误后再次发起指令Devika 无任何响应,基本上重启服务才得以解决
  5. Devika 生成的代码集中放在 data/projects/当前项目名称目录下,可通过配置文件更改路径
  6. Devika 不会主动读取本地文件内容,经提示也不能获取到文件内容,目前实际可落地性较差,如有需要可以自己根据需要完善工具代码(猜测这也是该开源工具后续的一个改进点)
  7. Devika 集成了一个知识库和数据库来存储和检索特定于项目的信息、代码片段和学到的知识。但目前只是任务标签的简单相等匹配,可用性较低
  8. Devika 提供了 UI 和服务的日志,能完成部分问题的定位,但与大模型的交互仅打印了错误日志,人工很难根据模型响应及时提醒优化代码质量
  9. Devika 生成代码后不会主动运行来完成自我纠错,只有人工让他运行遇到错误才会自我纠正
  1. 总结

尽管 Stition AI 的 Devika 仍处于初期发展阶段,但它已经初步展示了作为软件工程领域中有一定能力的人工智能程序员的潜力。通过不断的改进,它可以自动执行重复性任务并提高生产力。凭借其强大的功能以及开源社区持续不断迭代,Devika AI 有望成为 Devin AI 具有竞争力的开源替代品。

此外,随着 AI 程序员不断发展,通过利用其强大功能,开发人员可以提高工作效率、简化工作流程并轻松处理复杂的编码任务。总体而言,像 Devika 和 Devin 这样的工具标志着人类与人工智能的合作编码时代的到来。

参考文献:

GitHub - stitionai/devika

AutoDev: Automated AI-Driven Development

最后,如果客官觉得文章还不错,👏👏👏欢迎点赞、转发、收藏、关注,这是对小编的最大支持和鼓励,鼓励我们持续产出优质内容。

点赞

  1. 关于我们

MoonWebTeam目前成员均来自于腾讯,我们致力于分享有深度的前端技术,有价值的人生思考。

  1. 往期推荐

前端依赖注入的探索和实践

MoonWebTeam前端技术月刊第1期

2024年前端技术演进&优秀实践技术扫描

E2E测试用例生成方案探索

穿越时空:2023年前端技术盘点与2024年技术展望

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…323334…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%