WWDC23 Analyze hangs with Instruments

更新於 發佈於 閱讀時間約 30 分鐘
On the road from Queenstown to Glenorchy Wharf & Viewpoint, New Zealand.

On the road from Queenstown to Glenorchy Wharf & Viewpoint, New Zealand.

Prerequisites

建議先看過 WWDC 19 的 Getting Started with Instruments

處理 hang 有三步驟

raw-image

這 session 重點在 Analyze 及 Fix,關於 Find 可以查看另外一個 session WWDC22 Track down hangs with Xcode and on-device detection,這 session 有關於查找 hang 的所有工具,包括 Instruments,或 device 端的 hang detection (can enable in iOS Developer settings),和 Xcode Organizer

raw-image


What is a hang?


這邊用一個電燈的例子,來敘述什麼是 hang,當電燈泡插線之後,應該要馬上亮起來,拔掉之後就要馬上暗下來

raw-image
raw-image

但假如有 delay 的話會像下面這樣

raw-image

這個延遲可能只有 500ms,但我們一定很好奇這燈泡盒子發生什麼事,燈不能即時開關,很怪

raw-image

但有些情況 500ms 的延遲是無傷大雅的,例如兩個人在對話時,而燈泡這例子是對真實物體操作,直覺是要立即有反應

raw-image
raw-image

delay 大多數在 100ms 是很難察覺的,基本上 250ms 以內是可以接受的 ,但超過 250ms 就不太 ok 了,因此在 Apple 提供的工具裡 default 是 250ms 超過就會收到 hang report,超過 500ms 就是 hang 了

raw-image

但記得有些東西需要感覺 instant,就要 < 100ms,而有些東西 Request — Response 不用就在 < 500ms 也可以

raw-image

有些情境是會兩種狀況都有的,像是在 mail 裡,點擊 send button 是 instant 的,而畫面 dismiss 是 request-response

raw-image

所以 UI 類的東西,像是 tap button,盡可能在 < 100ms

Event handling and rendering loop


我們需要保持 main thread 的工作量,避免給他非 UI 工作的 task

raw-image

Improving app responsiveness

我們更新 UI 都需要在 main thread 花費一些時間,像下圖這樣,假設給 main thread 任務量太大,或當 event 要進入時 main thread 還有其他 task 在執行,都會造成 hang

raw-image

所以理想情況下 main thread 的任何任務都不該超過 100ms

raw-image

其他細節可參考:

Explore UI animation hitches and the render loop

Improving app responsiveness


Busy main thread hang


首先我們在 Xcode 中使用 Profile,快捷鍵 command + i ,開啟 Instruments 後,選擇 Time Profiler, 當還不確定要怎麼做,但想更好了解 app 正在做什麼,Time Profiler 是個好的起點

之後我們就建立了一個新的 Instruments document,仔細看左邊就會看到除了 Time Profiler,還有 Hangs tool,這對我們分析很有幫助

raw-image

再來就可以點擊 record button 開始錄製,這時 app 就會開啟了

這邊的情境是點了一顆按鈕後,整個畫面卡住,過了好幾秒有個 sheet present 才恢復正常 UI

raw-image

操作完後,回到 Instruments 按下暫停錄製,並且仔細看這些資料,會看到 Severe Hang (3.44 s),這符合我們剛剛用 app 的情況,按下按鈕後等了超久 UI 才正常

raw-image


Two types of hangs

主要分兩種類型會造成 main thread hang

  1. 真的把 main thread 操爆,CPU 的部分就會很明顯有大量活動
  2. main thread 被 block,可能,可能是被堵住,這時的 CPU 的部分不會有什麼活動

raw-image

回到 Instruments,選到 main thread 的部分,我們關注 hang 的部分,所以在他的區塊右鍵點擊 Set Inspection Range and Zoom

raw-image

仔細看 main thread 上的 CPU 狀況,很明顯使用率都在 60–90% 之間,這情況就是 busy main thread

raw-image

接下來好好查看 CPU 在忙什麼,可以先透過右下角的 Heaviest Stack Trace,點擊了其中一項,detail view 的 tree view 就會更新了,在 heaviest stack trace default 會隱藏非來自 source code 的後續 function 呼叫,這是因為要更方便查看 source code 所涉及的範圍

raw-image

可以將 Call Tree 打開,複選 Hide System Libraries,這樣 detail view 就會更單純了,這樣就過濾掉 system libraries 的所有 function,我們更專注於自己的 code

raw-image

接著要再更深入找出原因,會有兩個可能

  1. 這 function 就是如此花費 CPU 時間?
  2. 但也有可能這 function 一直被呼叫

這會影響該如何減少 main thread 上的 work

raw-image

下面這樣是一個典型的 call stack 結構,由 main function 到 UI framework,然後到 something() 後執行我們的程式碼

假設這個 function (Turtle)需要很長的時間,那麼就要查看一下它在裡面做了什麼,如果他做很多事情,那我們也許就減少它 code 的工作量,就解決耗時問題了

raw-image

那也有可能我們發現某個 function 一直被 call,像右邊 Unicorn function,他反覆做了很多次,這情況通常就是源頭,一直 call 的,與其去優化 Unicorn function,不如去處理源頭的 for loop,看如何減少呼叫次數

raw-image

所以在耗時的 function ,我們應該往下看,看 function 的實作細節,優化 code 減少工作量

而在被多次 call 的 function 情境,應該往上看源頭,研究如何減少被呼叫次數

raw-image

但在 Time Profiler 無法得知是什麼情況,而且從 Time Profiler 收集的訊息就只有每個間隔,當前 CPU 上被呼叫的 function,所以下圖的這些場景都有可能發生

raw-image


Time Profiler only samples

  • Cannot differentiate between long-running and often-called functions:
  • Use os_signpost or other instruments to measure precise runtime or how often specific work is performed

所以要測量特定 function 的執行時間,記得使用 os_signposts ,做法可參考 WWDC19 Getting Started with Instruments

但也有其他工具能準確告訴我們發生什麼事,而 SwiftUI View Body 工具就是其中之一

要使用 SwiftUI View Body 這工具,回到 Instruments 畫面,點擊右上角 + 號按鈕,這是提供所有工具的列表,甚至我們自己也可以編寫自定義工具

raw-image

最後可以把它拖曳進來,那因為剛剛第一次紀錄的時候還沒加進來,所以會是空的,沒資料

raw-image

紀錄完之後,可以使用 Ctrl + Plus 增加 track 的高度

raw-image
981

981

再來把 Severe Hang 給放大來看,看到 SwiftUI 的 track,每個間隔代表執行一次 view body 的執行,其中第二排 track 有很多橘色的間隔,都被標記為 BackgroundThumbnailView ,這代表執行了多少次 view body,以及每次執行需要的時間

raw-image

仔細看 detail view 裡把 Backyard Birds 給展開,可以看到 BackgroundThumbnailView 執行了 70 次,總共花費 3.23s,平均持續時間為 50ms,這很明顯一定會 Hang

raw-image

依照 app 的狀況來看,只需要顯示 6 張圖片,但卻跑了 view body 70次,似乎不是很正常,所以應該要減少 call view body 的次數

raw-image

為了找到相關 code,可以用 main thread track,選到該 function,然後右鍵 Reveal in Xcode,馬上就找到 code 了

raw-image
raw-image

可以對這個 View 右鍵 Find → Find Selected Symbol in Workspace

raw-image

這個 View 被包在兩層 ForEach 裡,確實會一直重複跑到

raw-image

解決方案,可使用 LazyVGrid,關於 Lazy 的相關細節可參考 WWDC20 Stacks, Grids, and Outlines in SwiftUI

raw-image

這樣調整後的結果,Hangs 的部分變成 Microhang,並不到 400ms

raw-image

右鍵 Set Inspection Range 看細節,會發現原本的 BackgroundThumbnailView 只執行了 8 次,從 severe hang 變成 micro hang,已經不是名顯的 delay 了,有些人可能覺得這樣就夠了,但接下來看 iPad 的例子,就會知道這樣的優化還不夠

raw-image

我們試試在 iPad 上的效能如何,另外一點是因為我們也要確保在各設備上能正常運行,結果會發現在 iPad 上 run,明明做和剛剛 iPhone 一樣的事情,卻又變慢了,其實仔細看這個 sheet,能明白因為螢幕變大,要呈現的 thumbnails 變多了

raw-image

我們的 Instruments 也記錄了這次的狀況,一樣把 Hang 的部分右鍵 Set Inspection Range and Zoom 展開,然後選起 View Body 的 tack,看 detail view 會發現 BackgroundThumbnailView 又執行了很多次,共 40 次,總共花費 1.49s

raw-image

所以這樣的範例能夠知道,一樣的程式碼在 iPhone 和 iPad 上執行會有不一樣的效能結果,所以往後就算看到 mirco hang,也記得處理

剛剛前面處理方向是因為多次 call BackgroundThumbnailView 造成效能的問題,現在看看如何提升單次執行的效率

首先在 View Body track 裡對單個 BackgroundThumbnailView hang 右鍵 Set Inspection Range,然後再點選 Main Thread track,在右下角 Heaviest Stack Trace 裡選起 BackyardBackground.thumbnail.getter

raw-image

從 detail view 可以得知到需要消耗 150ms,然後在 Heaviest Stack Trace 裡可以看到 thumbnail getter call 了 UIImage imageByPreparingThumbnailOfSize: ,這看起來只是順手處理縮圖,但這種處理縮圖也算耗時,在這邊也需要 150ms 左右,這件事應該由 background thread 來執行,不應該交給 main thread

raw-image

接著我們在 Heaviest Stack Trace 裡選 BackgroundThumbnailView.body.getter ,

然後對按下右鍵選擇 Open in Source Viewer

raw-image

這時 detail view 會從 call tree 改顯示為 source viewer,會看到 body getter 的實作

raw-image

接下來我們透過 path 的最右邊 menu button,在 Xcode 把這個檔案打開,目前我們知道在處理縮圖這件事,不應該給 main thread 來執行,應該交給 background load thumbnail

raw-image

首先調整 BackgroundThumbnailView ,看他 image loaded 了嗎,假如沒有先顯示 ProgressView

raw-image

最後就是 load thumbnail 的優化了,我們可以透過 .task 這 modifier 來 load thumbnail

raw-image

接下來我們來看看成果,很明顯當點擊 button 後 sheet 是立即顯示,thumbnail 會先顯示 progress indicators 顯示,load 完就顯示完整 thumbnail

raw-image


Async hang


雖然在使用上看起來成功了,但回到 Instruments,還是有接近 2 秒的 hang

raw-image

現在的情況是 hang 的時間稍微晚一些,看來是 load thumbnail 的部分沒問題,但後續可能有,我們再來看看 hang 在 Backyard Birds App 中的哪裡發生

raw-image

很明顯當 sheet 起來之後,瘋狂的點擊 done button 沒有立即反應,所以等於在 loading 的過程 UI interaction 是被卡住的,這就是 Instrument 告訴我們的 hang

Types of hangs: Interaction

這種 hang 和之前介紹的有點不一樣,前面說的是 main thread busy 或 blocked,但還有一種給 hang 分類的方式,就是 hang 的發生原因及時間,這稱為 synchronous and asynchronous hang

raw-image

簡單來說

  • Synchronous/Direct: 假設 main thread 已在 work,當有一個事件進入之後,需要很長時間處理它,那麼這事件就是一個 hang
  • Asynchronous/Delayed: 假設控制剛剛上述的問題,讓事件能快速處理,但我們可能還是會延遲一些工作,讓 main thread 稍後完成,或者 main thread 又有其他事件要處理,所以後面的事件需要等待之前的工作完成才能得到處理,這樣會導致 hang

raw-image


Hang detection

而 Hang detection 的工作方式為,查看 main thread 上所有工作項目,並檢查工作項目是否過長,如果是就會被標記成 potential hang

raw-image

無論是否有用戶 input,它都這樣檢查,因為用戶 input 可能在任何時刻發生,然後就可以檢查到 potential hang,所以 asynchronous or delay 的情況一樣會被檢查到

raw-image

但這些只是算 potential delay,而不是真實的 delay

asynchronous hangs 通常由 dispaych_async 在 main queue 或 main actor 上 run asynchronously 在 Swift Concurrency task 引起的,簡單來說就是任何在 main thread 的工作原因都有可能導致 hang

raw-image

像我們剛剛的例子,一開始是第一個 hang 是 synchronous hang,點擊顯示 sheet 的 button 後,後續要做了一個很長時間的 work,導致顯示 delay。

點擊 done button 本身不會觸發什麼耗時的 work,但因為 main thread 上有正在處理的 work,導致 done button 的點擊沒有被 main thread 處理

現在來處理這問題,回到 Instruments,在 detail view 可以看到 BackgroundThumbnailView 有 75 次呼叫,因為大多數 thumbnail body getter 被 execute 兩次,SwiftUI 大概建立了 40 個 progress indicators 顯示在 grid 上,但實際上最終只顯示了 35 個,然後這些 thumbnail loaded 後,view 會更新再次 call body,總共就會 execute 75 次 body getter,而這樣總執行時間也小於 1ms,所以 body getter 的速度已經很快了

raw-image

但我們還是有一個 hang,再次點擊 main thread track,查看 Heaviest Stack Trace,依然是 thumbnail getter 讓 main thread 花費很多時間,但仔細看他這次是在一個 closure 裡

raw-image

double-click 它,detail view 會打開 source viewer,這段 code 應該在這時刻執行沒錯,但不應該在 main thread 上執行

raw-image

類似這樣的問題 Swift Concurrency tasks 沒有按照期望的方式 execute,我們有另外一個輔助解決工具,就叫做 Swift Concurrency Tasks,使用就像加上 Hangs track 一樣

raw-image

在 main thread track 中,因為加了 Swift Tasks,我們可以顯示多個圖表,

raw-image

再來放大 hang 的時間間隔,一樣右鍵 Set Inspection Range

raw-image

這時看 Main Thread track 就會看到 Swift Task 那條有很多 task 在執行了

raw-image

選擇一項 task,右鍵 Set Inspection Range

raw-image

在 Heaviest Stack Trace,確認這 task 是不是在處理 thumbnail 的 work,看來沒有錯,該 task 確實是在處理 thumbnail,只是不希望這 task 在 main thread 上執行

raw-image

會這樣是因為,我們使用了 View,他預設是在 @MainActor 執行,所有 body 也會被影響

再來 .task {} 也受到上下文影響,也跟著會在 @MainActor 做事

最後是 thumbnail,因為它是 synchronous 的,所以在哪個 thread 上呼叫,就會被同步影響,最後也在 main actor 上執行

raw-image


Tasks and action isolation

raw-image

default 情況下 Swift Concurrency Tasks 會繼承上下文的 actor isolation,所以 SwiftUI 的 .task modifier 也一樣

有兩種方式擺脫 main actor

  1. 使用 synchronous call 未綁定在 main actor 的 function,這樣就可以允許 task 脫離 main actor,但有些情況是不可行的
  2. 使用 Task.detached {} ,將 task 從上下文的 actor 分離出來,但這不是最簡潔的方法,而且是建立一個獨立的 task,比簡單暫停一個現有的 task 成本還高,而使用第一種方法,當 View 消失時 SwiftUI 還會自動取消 task modifier 裡的 task,但是 Task.detached 就沒有這樣的特性

更多細節可以參考 WWDC22 Visualize and optimize Swift concurrency

Improving app responsiveness

因為我們的情況是 asynchronous context,加上 thumbnail function 轉為 nonisolated 及 asynchronous 是很容易的,所以方法1更合適我們

做法很簡單,首先找到定義 thumbnail 的地方,將原本 synchronous 改為 asynchronous,加上 keyword async 即可

raw-image

最後在 call 的地方加上 await ,因為它現在是 asynchronous 的

raw-image

這樣就會讓他在 Swift Concurrency 中的 concurrent thread pool 中執行,而非 main thread 了

現在來看看這修改在 Instruments 上的變化,Main Thread 上的 task 時間間隔都變很短

raw-image

往下滑動來看,其他 thread 的 track 有很多我們的 task,現在他們在 thread 上不是 parallel 執行,也不是 sequentially 的,已經善用我們的 CPU 多核心

raw-image


Blocked main thread hung


Hangs

raw-image

我們已經調查且修復 main thread unresponsive 的狀況,知道是 main thread 卡住造成

也知道 hang 可能有 synchronous (直接單一個大 task 要給 main thread)及 asynchronous(安排大量的 task 要給 main thread 上 execute,導致有些 delay) 的,這在 Instruments 都可以檢測。

最後在 fix 的部分也有概念,把 task 放在 background 或者讓單個 task 的 work 量減少,只有更新 UI 才需要 main thread 上

raw-image

但還有一個情況是 Blocked Main thread,這種情況 main thread 也不會佔用 CPU 太多,這情況還是需要透過 Instruments 來分析

仔細看這個 Track,明明已經是 Severe Hang 但是 CPU 的用量卻極低,這就是典型的 Blocked Main thread,

raw-image

我們把 CPU Usage 的 track 放大來看,會看到很多 mark,都是透過 Time Profiler 採集的 sample,但到後面就沒有了

raw-image

假如選這個沒有 sample 及 CPU 沒有活動的區段,會發現 detail view 上沒有任何資料

raw-image

這時我們可以使用另一個 tool Thread States,跟之前加的 tool 一樣,從 Instruments library 新增,在畫面上的右上角有顆 plus button

raw-image

現在多一條 Track,和 Swift Concurrency instrument 一樣

raw-image

但我們要關注的點在 Main Thread track 上,仔細看有一個很長的 blocked 的時間間隔

raw-image

超過了 6s,這就是我們 hang 的持續時間

raw-image

當我們點擊那個 blocked 時間間隔,底下的 detail view 也會更新,這稱 Narrative view,裡面顯示了 blocked state,這裡面告訴我們 thread 的狀況,做什麼,何時做,為什麼要做

raw-image

仔細看時間項目,有一條就告訴我們 thread 被 blocked 6.64s,並且是因為 syscall mach_msg2_trap

raw-image

而右下角是 Backtrace,但這個 backtrace 並不是最重要的 backtrace,他不是某種集合,而是 mach_msg2_trap 的精準 backtrace

raw-image

function 呼叫是最底下的 leaf node,然後 call stack 在它的上方,而透過 call stack ,可以得知 syscall 是由 MLModel 分配發起,而 MLModel 由於分配 ColorizingService 的 object 發生的,而 object 上有一個 property 叫 shared 是個 singleton,最後就會看到 body getter 的 closure 中呼叫的(一直往上源看)

1471

1471

raw-image

double-click 它,會看到 source viewer,直接看 code 似乎沒什麼問題對吧?

raw-image

因為我們在其中使用了一個 singleton,任何的 access 就會啟動建立這個 ColorizingService instance,而這過程載入就會 blocked thread(假設 ColorizingService 的 init 過程很複雜,work 量很大,那一定會造成 main thread 的工作量大增),

raw-image

那直覺可能就會想說,那我們用 await async 避免 blocked main thread 就好了,但這並不能解決問題

首先 await async 只能用在 asynchronous function,而目前的 shared 並不是,它只是一個 static let property,是一個 synchronous 的,而假如要用在 colorize(:_) 這 function 是可以的,因為他是一個 asynchronous function

raw-image


Fix

raw-image

  1. 把 shared 加上 async,這樣就可以讓他脫離 main actor,基本上這做法是 ok 的,當情境是等待其他地方的 thread 要繼續執行時,通常沒問題
  2. 但是有時候 blocked thread 可能是因為 locked or semaphores,要特別注意,可參考下面的 session,避免寫出有問題的 code

其他細節可參考

WWDC21 Swift concurrency: Behind the scenes

最後再來看一個 blocked main thread 的例子

這是我們剛剛處理的 case

raw-image

但在往左邊看還有另外一個 blocked main thread,但明明也是 blocked,他卻沒有被 Instruments 標記成 potential hang,這邊 main thread 只是休眠了,因為沒有 user input,但從 operating system 的角度來看確實是 blocked,但它只是在無事可做時不運行,主要是為了節省資源,只要有 input,就會被喚醒,進行處理,因此要確定 blocked main thread 是否是 responsiveness 的問題,需要查看 Hangs 工具,而非 Thread State 工具

raw-image


Clarifications

所以當 Blocked Main Thread 或 High CPU usage 時不一定是 Unresponsive Main Thread

但 Unresponsive Main Thread 一定是 Blocked Main Thread or Busy Main Thread

raw-image

hang detection 會考慮這些所有細節,並且只會標記 main thread unresponsive 時的 potential hang

Wrap-up


  • Keep work on the main thread below 100ms
  • Determine whether the main thread is busy or locked
  • Hangs can surface synchronously or asynchronously
  • Do less work
  • Move work to the background
  • Measure fires, the optimize





留言
avatar-img
留言分享你的想法!
avatar-img
CHENGYANG的沙龍
0會員
17內容數
CHENGYANG的沙龍的其他內容
2025/08/06
Orientation Instruments 可以在開發初期就使用,建議要頻繁檢測自己的 app 效能,提早發現問題提早處理。 它包括 iOS、macOS、watchOS 和 tvOS。 雖然他內建在 Xcode 裡,但他其實是一個獨立 app,所以有時候可以獨立使用 Ins
Thumbnail
2025/08/06
Orientation Instruments 可以在開發初期就使用,建議要頻繁檢測自己的 app 效能,提早發現問題提早處理。 它包括 iOS、macOS、watchOS 和 tvOS。 雖然他內建在 Xcode 裡,但他其實是一個獨立 app,所以有時候可以獨立使用 Ins
Thumbnail
2025/07/27
Introduction 這是可以跑在設備端的大語言模型,這 framework 支援 macOS, iOS, iPadOS 和 visionOS。 可以藉助這個 framework 來增強 app 中現有的功能,例如提供個性化搜尋建議 也可以開發新功能,例如在旅遊
Thumbnail
2025/07/27
Introduction 這是可以跑在設備端的大語言模型,這 framework 支援 macOS, iOS, iPadOS 和 visionOS。 可以藉助這個 framework 來增強 app 中現有的功能,例如提供個性化搜尋建議 也可以開發新功能,例如在旅遊
Thumbnail
2025/07/26
Introduction 在新的 iOS 26 以及 macOS Tahoe 有全新的外觀及風格,這次變更的核心就是用於 Control 及 Navigation 的全新自適應材質 Liquid Glass 假如想要深入了解 Liquid Glass 的設計理念思維請看 W
Thumbnail
2025/07/26
Introduction 在新的 iOS 26 以及 macOS Tahoe 有全新的外觀及風格,這次變更的核心就是用於 Control 及 Navigation 的全新自適應材質 Liquid Glass 假如想要深入了解 Liquid Glass 的設計理念思維請看 W
Thumbnail
看更多
你可能也想看
Thumbnail
嗨!歡迎來到 vocus vocus 方格子是台灣最大的內容創作與知識變現平台,並且計畫持續拓展東南亞等等國際市場。我們致力於打造讓創作者能夠自由發表、累積影響力並獲得實質收益的創作生態圈!「創作至上」是我們的核心價值,我們致力於透過平台功能與服務,賦予創作者更多的可能。 vocus 平台匯聚了
Thumbnail
嗨!歡迎來到 vocus vocus 方格子是台灣最大的內容創作與知識變現平台,並且計畫持續拓展東南亞等等國際市場。我們致力於打造讓創作者能夠自由發表、累積影響力並獲得實質收益的創作生態圈!「創作至上」是我們的核心價值,我們致力於透過平台功能與服務,賦予創作者更多的可能。 vocus 平台匯聚了
Thumbnail
這篇內容,將簡單介紹Asset Browser、Workspace、Inspector、Code Browser,作為入門的介面導覽。
Thumbnail
這篇內容,將簡單介紹Asset Browser、Workspace、Inspector、Code Browser,作為入門的介面導覽。
Thumbnail
需求情境: 在設計畫面時,資料來源是後台的 api,每一次畫面細節的修修改改,都會觸發 Xcode Preview 程序,導致不斷呼叫後台。此時若資料結構和大小都具有一定規模,就會導致效率低落,不斷等待,且消耗伺服器資源甚鉅。 解決方案: 將後台傳回的資料以檔案形式暫存在本地端,每次 pr
Thumbnail
需求情境: 在設計畫面時,資料來源是後台的 api,每一次畫面細節的修修改改,都會觸發 Xcode Preview 程序,導致不斷呼叫後台。此時若資料結構和大小都具有一定規模,就會導致效率低落,不斷等待,且消耗伺服器資源甚鉅。 解決方案: 將後台傳回的資料以檔案形式暫存在本地端,每次 pr
Thumbnail
Part.1 搞定基本的 UI 開始開發 iOS App。 首先準備一台 Mac,然後安裝 Xcode,新增專案,系統即刻生成基本的專案結構。coding 的起點在檔案 ContentView.swift: import SwiftUI struct ContentView: View {  
Thumbnail
Part.1 搞定基本的 UI 開始開發 iOS App。 首先準備一台 Mac,然後安裝 Xcode,新增專案,系統即刻生成基本的專案結構。coding 的起點在檔案 ContentView.swift: import SwiftUI struct ContentView: View {  
Thumbnail
軟體系統的發展歷程大多相似,首重解決基本需求、提供操作介面,進而提升安全性、擴充功能、優化操作。
Thumbnail
軟體系統的發展歷程大多相似,首重解決基本需求、提供操作介面,進而提升安全性、擴充功能、優化操作。
Thumbnail
上次完成到基本的CRUD及權限控制,後面花了點時間把排序、分頁、圖表總覽的部分做完,其他細節是佈署上線,一般在公司內有專屬的部門處理,僅了解一下流程。
Thumbnail
上次完成到基本的CRUD及權限控制,後面花了點時間把排序、分頁、圖表總覽的部分做完,其他細節是佈署上線,一般在公司內有專屬的部門處理,僅了解一下流程。
Thumbnail
這篇文章分享了申請 AppWorks School 轉職培訓的過程,包括背景及源起、報名前準備、報名及面談、總結等內容,並提供了未來申請者的建議。如果你正在尋求轉職機會,這篇文章可能對你有所幫助。
Thumbnail
這篇文章分享了申請 AppWorks School 轉職培訓的過程,包括背景及源起、報名前準備、報名及面談、總結等內容,並提供了未來申請者的建議。如果你正在尋求轉職機會,這篇文章可能對你有所幫助。
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News