1. 程式人生 > 程式設計 >如何使用Swift來實現一個命令列工具的方法

如何使用Swift來實現一個命令列工具的方法

本文即簡單介紹瞭如何在Swift中開發命令列工具,以及與Shell命令的互動。水文一篇,不喜勿噴。

主要是使用該工具來解析微信的效能監控元件Matrix的OOM Log。

基本模組

這裡,僅簡單介紹了常見的基本模組。

Process

Process類可以用來開啟另外一個子程序,並監控其執行情況。

  1. launchPath:指定了執行路徑。如可以設定為 /usr/bin/env ,這個命令可以用於列印本機上所有的環境變數;也可以用於執行shell命令,如果你接了引數的話。本文的Demo就用它來執行輸入的命令。
  2. arguments:引數,以陣列形式傳遞即可。
  3. launch:呼叫launch函式即可啟動process,用於執行命令。
  4. waitUntilExit:一般執行Shell命令,需要等待命令返回。
  5. terminationStatus:當前process的結束狀態,正常為0.
  6. standardOutput:standardOutput對應於終端的標準輸出。standardError則是錯誤輸出。

Pipe

Pipe這個類就是作業系統的管道,在這裡用來接受子程序的輸出。這裡,可以用於將process的輸出傳遞至管道指定的地方,如一個output變數,或者檔案也可以。

  • fileHandleForReading:pipe從哪裡讀取內容?
  • fileHandleForWriting:pipe將內容寫到哪裡?

CommandLine

用於獲取指令碼引數而已。

print(CommandLine.argc) // 2
print(CommandLine.arguments) // ["./test.swift","hello"]

封裝Shell命令

僅執行Shell命令

這裡提供了兩種呼叫Shell命令的封裝函式,個人更傾向於第二種,直接將Shell命令及引數封裝成一個字串傳入即可。

@discardableResult
func runShell(_ command: String) -> Int32 {
 let task = Process()
 task.launchPath = "/bin/bash"
 task.arguments = ["-c",command]
 task.launch()
 task.waitUntilExit()
 return task.terminationStatus
}

@discardableResult
func runShellWithArgs(_ args: String...) -> Int32 {
 let task = Process()
 task.launchPath = "/usr/bin/env"
 task.arguments = args
 task.launch()
 task.waitUntilExit()
 return task.terminationStatus
}

使用如下:

runShell("pwd")
runShell("ls -l")

runShellWithArgs("pwd")
runShellWithArgs("ls","-l")

需要Shell命令的輸出內容

這裡就需要使用到Pipe了。

@discardableResult
func runShellAndOutput(_ command: String) -> (Int32,String?) {
 let task = Process()
 task.launchPath = "/bin/bash"
 task.arguments = ["-c",command]
 
 let pipe = Pipe()
 task.standardOutput = pipe
 task.standardError = pipe
 
 task.launch()
 
 let data = pipe.fileHandleForReading.readDataToEndOfFile()
 let output = String(data: data,encoding: .utf8)
 
 task.waitUntilExit()
 
 return (task.terminationStatus,output)
}

@discardableResult
func runShellWithArgsAndOutput(_ args: String...) -> (Int32,String?) {
 let task = Process()

 task.launchPath = "/usr/bin/env"
 task.arguments = args
 
 let pipe = Pipe()
 task.standardOutput = pipe
 task.standardError = pipe
 
 task.launch()
 
 let data = pipe.fileHandleForReading.readDataToEndOfFile()
 let output = String(data: data,output)
}

使用如下:

let (ret1,output1) = runShellAndOutput("ls -l")
if let output11 = output1 {
 print(output11)
}

let (ret2,output2) = runShellWithArgsAndOutput("ls","-l")
if let output22 = output2 {
 print(output2)
}

如何解析Matrix的OOM Log

Matrix的OOM Log格式如下,其實就是一個大JSON:

{
 "head": {
  "protocol_ver": 1,"phone": "iPhone10,1","os_ver": "13.4","launch_time": 1589361495000,"report_time": 1589362109100,"app_uuid": ""
 },"items": [
  {
   "tag": "iOS_MemStat","info": "","scene": "","name": "Malloc 12.54 MiB","size": 146313216,"count": 1,"stacks": [
    {
     "caller": "f07199ac8a903127b17f0a906ffb0237@84128","frames": [
      {
       "uuid": "a0a7d67af0f3399a8f006f92716d8e6f","offset": 67308
      },{
       "uuid": "a0a7d67af0f3399a8f006f92716d8e6f","offset": 69836
      },{
       "uuid": "f07199ac8a903127b17f0a906ffb0237","offset": 84128
      },{
       "uuid": "b80198f7beb93e79b25c7a27d68bb489","offset": 14934312
      },{
       "uuid": "1a46239df2fc34b695bc9f38869f0c85","offset": 1126304
      },"offset": 123584
      },"offset": 1135100
      }]
    }
   ]
  }
 ]
}

解析的思路其實非常簡單,將JSON轉為Model,然後根據所需,提取對應的資訊即可。

uuid是mach-o的唯一標識,offset則是符號相對於mach-o基地址的偏移量。拿到dSYM檔案,使用 atos 命令即可進行符號化。

guard let rawLogModel = MatrixOOMLogParser.parse() else { exit(-1) }
print("______ Start to process Matrix OOM Log ...")

let group = DispatchGroup()

var metaLog = ""

for item in bodyInfo.items {
 guard let stacks = item.stacks else { continue }
 
 group.enter()
 
 DispatchQueue.global().async {
  var log = "______ item ______ name: \(item.name),size: \(item.size),count: \(item.count) \n"
  metaLog += log
  
  for stack in stacks {
   let outputs = stack.frames.map({ (frame: MatrixOOMLogModelFrame) -> String in
    // let uuid = frame.uuid
    let offset = frame.offset
    let instructionAddress = loadAddress + offset
    let (_,output) = runShellAndOutput("xcrun atos -o \(dwarf) -arch arm64 -l 0x1 \(instructionAddress.hexValue)")
    return output ?? ""
   })
   
   log += outputs.joined()
   
   print(log)
  }
  
  group.leave()
 }
}

group.wait()

print("\n\(metaLog)\n")

print("______ Finished processing Matrix OOM Log ...")

MatrixOOMLogParser.parse() 就是將JSON轉為Model,這裡用的就是Swift裡邊的Codable。

這裡有一個需要注意的點,Mac CLI沒有Bundle的概念,只有一個bin檔案。所以對於原始的JSON檔案,只能通過外部bundle的方式來新增。通過 New->Target 單獨建立一個bundle。需要在 Xcode -> Build Phases -> Copy Files 中新增該bundle名,然後即可通過 Bundle(url: mockDataBundleURL) 來載入該bundle並獲取其中的log檔案了。

因為atos的執行時間較長,所以大量的符號化操作會非常耗時。一般來說,這段程式碼執行六七分鐘左右,可以將一個Matrix的OOM Log完全符號化。而符號化之後的記錄如何分析,就是另外一個話題了。

參考資料

How do I run an terminal command in a swift script? (e.g. xcodebuild)

到此這篇關於如何使用Swift來實現一個命令列工具的文章就介紹到這了,更多相關Swift 命令列內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!