diff --git a/device.go b/device.go index 6d3d83e..5df66d9 100644 --- a/device.go +++ b/device.go @@ -5,16 +5,15 @@ import ( "context" "errors" "fmt" - "os" - "path" - "strings" - "time" - "github.com/electricbubble/gidevice/pkg/ipa" "github.com/electricbubble/gidevice/pkg/libimobiledevice" "github.com/electricbubble/gidevice/pkg/nskeyedarchiver" uuid "github.com/satori/go.uuid" "howett.net/plist" + "os" + "path" + "strings" + "time" ) const LockdownPort = 62078 @@ -591,6 +590,133 @@ func (d *device) MoveCrashReport(hostDir string, opts ...CrashReportMoverOption) return d.crashReportMover.Move(hostDir, opts...) } +func (d *device) GetPerfmon(opts *PerfmonOption) (out chan interface{}, outCancel context.CancelFunc, perfErr error) { + if d.lockdown == nil { + if _, err := d.lockdownService(); err != nil { + return nil, nil, err + } + } + if opts==nil { + return nil, nil, fmt.Errorf("parameter is empty") + } + if !opts.OpenChanCPU &&!opts.OpenChanMEM &&!opts.OpenChanGPU &&!opts.OpenChanFPS &&!opts.OpenChanNetWork { + opts.OpenChanCPU = true + opts.OpenChanMEM = true + opts.OpenChanGPU = true + opts.OpenChanFPS = true + opts.OpenChanNetWork = true + } + + var err error + + var instruments Instruments + ctx, cancel := context.WithCancel(context.Background()) + + chanCPU := make(chan CPUInfo) + chanMEM := make(chan MEMInfo) + var cancelSysmontap context.CancelFunc + + if opts.OpenChanCPU || opts.OpenChanMEM { + instruments, err = d.lockdown.InstrumentsService() + if err != nil { + return nil, cancel, err + } + chanCPU, chanMEM, cancelSysmontap, err = instruments.StartSysmontapServer(opts.PID, ctx) + if err != nil { + cancelSysmontap() + return nil, cancel, err + } + } + + chanFPS := make(chan FPSInfo) + chanGPU := make(chan GPUInfo) + var cancelOpengl context.CancelFunc + + if opts.OpenChanGPU || opts.OpenChanFPS { + instruments, err = d.lockdown.InstrumentsService() + if err != nil { + return nil, cancel, err + } + chanFPS, chanGPU, cancelOpengl, err = instruments.StartOpenglServer(ctx) + if err != nil { + cancelOpengl() + return nil, cancel, err + } + } + + chanNetWork := make(chan NetWorkingInfo) + var cancelNetWork context.CancelFunc + if opts.OpenChanNetWork { + instruments, err = d.lockdown.InstrumentsService() + if err != nil { + return nil, cancel, err + } + chanNetWork, cancelNetWork, err = instruments.StartNetWorkingServer(ctx) + if err != nil { + cancelNetWork() + return nil, cancel, err + } + } + // 弃用之前的PerfMonData ,统一汇总到interface里,由用户自行决定处理数据 + result := make(chan interface{}) + go func() { + for { + select { + case v, ok := <-chanCPU: + if opts.OpenChanCPU && ok { + result<-v + } + case v, ok := <-chanMEM: + if opts.OpenChanMEM && ok { + result<-v + } + case v, ok := <-chanFPS: + if opts.OpenChanFPS && ok { + result<-v + } + case v, ok := <-chanGPU: + if opts.OpenChanGPU && ok { + result<-v + } + case v, ok := <-chanNetWork: + if opts.OpenChanNetWork && ok { + result<-v + } + case <-ctx.Done(): + err:=d.stopPerfmon(opts) + if err!=nil { + fmt.Println(err) + } + close(result) + return + } + } + }() + return result, cancel, err +} + +func (d *device)stopPerfmon(opts *PerfmonOption)(err error) { + if _, err = d.instrumentsService(); err != nil { + return err + } + if opts.OpenChanCPU || opts.OpenChanMEM { + if err = d.instruments.StopSysmontapServer(); err != nil { + return err + } + } + if opts.OpenChanGPU || opts.OpenChanFPS { + if err = d.instruments.StopOpenglServer(); err != nil { + return err + } + } + if opts.OpenChanNetWork { + if err = d.instruments.StopNetWorkingServer(); err != nil { + return err + } + } + return nil +} + func (d *device) XCTest(bundleID string, opts ...XCTestOption) (out <-chan string, cancel context.CancelFunc, err error) { xcTestOpt := defaultXCTestOption() for _, fn := range opts { @@ -836,4 +962,4 @@ func (d *device) _uploadXCTestConfiguration(bundleID string, sessionId uuid.UUID } return -} +} \ No newline at end of file diff --git a/device_test.go b/device_test.go index ac18e95..45bebff 100644 --- a/device_test.go +++ b/device_test.go @@ -1,6 +1,7 @@ package giDevice import ( + "encoding/json" "fmt" "os" "os/signal" @@ -68,7 +69,7 @@ func Test_device_SavePairRecord(t *testing.T) { func Test_device_XCTest(t *testing.T) { setupLockdownSrv(t) - bundleID = "com.leixipaopao.WebDriverAgentRunner.xctrunner" + bundleID = "com.DataMesh.CheckList" out, cancel, err := dev.XCTest(bundleID) // out, cancel, err := dev.XCTest(bundleID, WithXCTestEnv(map[string]interface{}{"USE_PORT": 8222, "MJPEG_SERVER_PORT": 8333})) if err != nil { @@ -154,6 +155,51 @@ func Test_device_Shutdown(t *testing.T) { dev.Shutdown() } +func Test_device_Perf(t *testing.T) { + //setupDevice(t) + //SetDebug(true,true) + setupLockdownSrv(t) + + c := make(chan os.Signal) + signal.Notify(c, os.Interrupt, os.Kill) + + var opts = &PerfmonOption{ + //PID: "0", + //OpenChanNetWork: true, + //OpenChanFPS: true, + OpenChanMEM: true, + //OpenChanCPU: true, + //OpenChanGPU: true, + } + + outData, cannel, err := dev.GetPerfmon(opts) + if err != nil { + fmt.Println(err) + os.Exit(0) + } + var timeLast = time.Now().Unix() + for { + select { + case <-c: + if cannel!=nil { + cannel() + } + default: + data ,ok := <-outData + if !ok { + fmt.Println("end get perfmon data") + return + } + result, _ := json.MarshalIndent(data, "", "\t") + fmt.Println(string(result)) + if time.Now().Unix()-timeLast >60 { + cannel() + } + } + } + +} + func Test_device_InstallationProxyBrowse(t *testing.T) { setupDevice(t) diff --git a/idevice.go b/idevice.go index 26c5df1..93a2753 100644 --- a/idevice.go +++ b/idevice.go @@ -77,6 +77,8 @@ type Device interface { springBoardService() (springBoard SpringBoard, err error) GetIconPNGData(bundleId string) (raw *bytes.Buffer, err error) GetInterfaceOrientation() (orientation OrientationState, err error) + + GetPerfmon(opts *PerfmonOption) (out chan interface{}, outCancel context.CancelFunc, perfErr error) } type DeviceProperties = libimobiledevice.DeviceProperties @@ -153,6 +155,19 @@ type Instruments interface { // SysMonStart(cfg ...interface{}) (_ interface{}, err error) registerCallback(obj string, cb func(m libimobiledevice.DTXMessageResult)) + + StartOpenglServer(ctx context.Context) (chanFPS chan FPSInfo, chanGPU chan GPUInfo, cancel context.CancelFunc, err error) + + StopOpenglServer() (err error) + + StartSysmontapServer(pid string, ctx context.Context) (chanCPU chan CPUInfo, chanMem chan MEMInfo, cancel context.CancelFunc, err error) + + StopSysmontapServer() (err error) + //ProcessNetwork(pid int) (out <-chan interface{}, cancel context.CancelFunc, err error) + + StartNetWorkingServer(ctx context.Context) (chanNetWorking chan NetWorkingInfo, cancel context.CancelFunc, err error) + + StopNetWorkingServer() (err error) } type Testmanagerd interface { @@ -357,6 +372,15 @@ func WithUpdateToken(updateToken string) AppListOption { } } +type PerfmonOption struct { + PID string + OpenChanGPU bool + OpenChanFPS bool + OpenChanCPU bool + OpenChanMEM bool + OpenChanNetWork bool +} + type Process struct { IsApplication bool `json:"isApplication"` Name string `json:"name"` diff --git a/instruments.go b/instruments.go index a07f09d..fc4c2d6 100644 --- a/instruments.go +++ b/instruments.go @@ -1,8 +1,11 @@ package giDevice import ( + "context" "encoding/json" "fmt" + "time" + "github.com/electricbubble/gidevice/pkg/libimobiledevice" ) @@ -257,6 +260,381 @@ func (i *instruments) registerCallback(obj string, cb func(m libimobiledevice.DT i.client.RegisterCallback(obj, cb) } +func (i *instruments) StartSysmontapServer(pid string, ctxParent context.Context) (chanCPU chan CPUInfo, chanMem chan MEMInfo, cancel context.CancelFunc, err error) { + var id uint32 + if ctxParent == nil { + return nil, nil, nil, fmt.Errorf("missing context") + } + ctx, cancelFunc := context.WithCancel(ctxParent) + _outMEM := make(chan MEMInfo) + _outCPU := make(chan CPUInfo) + if id, err = i.requestChannel("com.apple.instruments.server.services.sysmontap"); err != nil { + return nil, nil, cancelFunc, err + } + + selector := "setConfig:" + args := libimobiledevice.NewAuxBuffer() + + var config map[string]interface{} + config = make(map[string]interface{}) + { + config["bm"] = 0 + config["cpuUsage"] = true + + config["procAttrs"] = []string{ + "memVirtualSize", "cpuUsage", "ctxSwitch", "intWakeups", + "physFootprint", "memResidentSize", "memAnon", "pid"} + + config["sampleInterval"] = 1000000000 + // 系统信息字段 + config["sysAttrs"] = []string{ + "vmExtPageCount", "vmFreeCount", "vmPurgeableCount", + "vmSpeculativeCount", "physMemSize"} + // 刷新频率 + config["ur"] = 1000 + } + + args.AppendObject(config) + if _, err = i.client.Invoke(selector, args, id, true); err != nil { + return nil, nil, cancelFunc, err + } + selector = "start" + args = libimobiledevice.NewAuxBuffer() + + if _, err = i.client.Invoke(selector, args, id, true); err != nil { + return nil, nil, cancelFunc, err + } + + i.registerCallback("", func(m libimobiledevice.DTXMessageResult) { + select { + case <-ctx.Done(): + return + default: + mess := m.Obj + chanCPUAndMEMData(mess, _outMEM, _outCPU, pid) + } + }) + + go func() { + i.registerCallback("_Golang-iDevice_Over", func(_ libimobiledevice.DTXMessageResult) { + cancelFunc() + }) + select { + case <-ctx.Done(): + var isOpen bool + if _outCPU != nil { + _, isOpen = <-_outMEM + if isOpen { + close(_outMEM) + } + } + if _outMEM != nil { + _, isOpen = <-_outCPU + if isOpen { + close(_outCPU) + } + } + } + return + }() + return _outCPU, _outMEM, cancelFunc, err +} + +func chanCPUAndMEMData(mess interface{}, _outMEM chan MEMInfo, _outCPU chan CPUInfo, pid string) { + switch mess.(type) { + case []interface{}: + var infoCPU CPUInfo + var infoMEM MEMInfo + messArray := mess.([]interface{}) + if len(messArray) == 2 { + var sinfo = messArray[0].(map[string]interface{}) + var pinfolist = messArray[1].(map[string]interface{}) + if sinfo["CPUCount"] == nil { + var temp = sinfo + sinfo = pinfolist + pinfolist = temp + } + if sinfo["CPUCount"] != nil && pinfolist["Processes"] != nil { + var cpuCount = sinfo["CPUCount"] + var sysCpuUsage = sinfo["SystemCPUUsage"].(map[string]interface{}) + var cpuTotalLoad = sysCpuUsage["CPU_TotalLoad"] + // 构建返回信息 + infoCPU.CPUCount = int(cpuCount.(uint64)) + infoCPU.SysCpuUsage = cpuTotalLoad.(float64) + //finalCpuInfo["attrCpuTotal"] = cpuTotalLoad + + var cpuUsage = 0.0 + pidMess := pinfolist["Processes"].(map[string]interface{})[pid] + if pidMess != nil { + processInfo := sysmonPorceAttrs(pidMess) + cpuUsage = processInfo["cpuUsage"].(float64) + infoCPU.CPUUsage = cpuUsage + infoCPU.Pid = pid + infoCPU.AttrCtxSwitch = uIntToInt64(processInfo["ctxSwitch"]) + infoCPU.AttrIntWakeups = uIntToInt64(processInfo["intWakeups"]) + + infoMEM.Vss = uIntToInt64(processInfo["memVirtualSize"]) + infoMEM.Rss = uIntToInt64(processInfo["memResidentSize"]) + infoMEM.Anon = uIntToInt64(processInfo["memAnon"]) + infoMEM.PhysMemory = uIntToInt64(processInfo["physFootprint"]) + + } else { + infoCPU.Mess = "invalid PID" + infoMEM.Mess = "invalid PID" + + infoMEM.Vss = -1 + infoMEM.Rss = -1 + infoMEM.Anon = -1 + infoMEM.PhysMemory = -1 + } + + infoMEM.TimeStamp = time.Now().UnixNano() + infoCPU.TimeStamp = time.Now().UnixNano() + + _outMEM <- infoMEM + _outCPU <- infoCPU + } + } + } +} + +// 获取进程相关信息 +func sysmonPorceAttrs(cpuMess interface{}) (outCpuInfo map[string]interface{}) { + if cpuMess == nil { + return nil + } + cpuMessArray, ok := cpuMess.([]interface{}) + if !ok { + return nil + } + if len(cpuMessArray) != 8 { + return nil + } + if outCpuInfo == nil { + outCpuInfo = map[string]interface{}{} + } + // 虚拟内存 + outCpuInfo["memVirtualSize"] = cpuMessArray[0] + // CPU + outCpuInfo["cpuUsage"] = cpuMessArray[1] + // 每秒进程的上下文切换次数 + outCpuInfo["ctxSwitch"] = cpuMessArray[2] + // 每秒进程唤醒的线程数 + outCpuInfo["intWakeups"] = cpuMessArray[3] + // 物理内存 + outCpuInfo["physFootprint"] = cpuMessArray[4] + + outCpuInfo["memResidentSize"] = cpuMessArray[5] + // 匿名内存 + outCpuInfo["memAnon"] = cpuMessArray[6] + + outCpuInfo["PID"] = cpuMessArray[7] + return +} + +func (i *instruments) StopSysmontapServer() (err error) { + id, err := i.requestChannel("com.apple.instruments.server.services.sysmontap") + if err != nil { + return err + } + selector := "stop" + args := libimobiledevice.NewAuxBuffer() + if _, err = i.client.Invoke(selector, args, id, true); err != nil { + return err + } + return nil +} + +// todo 获取单进程流量情况,看情况做不做 +// 目前只获取到系统全局的流量情况,单进程需要用到set,go没有,并且实际用python测试单进程的流量情况不准 +func (i *instruments) StartNetWorkingServer(ctxParent context.Context) (chanNetWorking chan NetWorkingInfo, cancel context.CancelFunc, err error) { + var id uint32 + if ctxParent == nil { + return nil, nil, fmt.Errorf("missing context") + } + ctx, cancelFunc := context.WithCancel(ctxParent) + _outNetWork := make(chan NetWorkingInfo) + if id, err = i.requestChannel("com.apple.instruments.server.services.networking"); err != nil { + return nil, cancelFunc, err + } + selector := "startMonitoring" + args := libimobiledevice.NewAuxBuffer() + + if _, err = i.client.Invoke(selector, args, id, true); err != nil { + return nil, cancelFunc, err + } + i.registerCallback("", func(m libimobiledevice.DTXMessageResult) { + select { + case <-ctx.Done(): + return + default: + receData, ok := m.Obj.([]interface{}) + if ok && len(receData) == 2 { + sendAndReceiveData, ok := receData[1].([]interface{}) + if ok { + var netData NetWorkingInfo + // 有时候是uint8,有时候是uint64。。。恶心 + netData.RxBytes = uIntToInt64(sendAndReceiveData[0]) + netData.RxPackets = uIntToInt64(sendAndReceiveData[1]) + netData.TxBytes = uIntToInt64(sendAndReceiveData[2]) + netData.TxPackets = uIntToInt64(sendAndReceiveData[3]) + netData.TimeStamp = time.Now().UnixNano() + _outNetWork <- netData + } + } + } + }) + go func() { + i.registerCallback("_Golang-iDevice_Over", func(_ libimobiledevice.DTXMessageResult) { + cancelFunc() + }) + select { + case <-ctx.Done(): + _, isOpen := <-_outNetWork + if isOpen { + close(_outNetWork) + } + } + return + }() + return _outNetWork, cancelFunc, err +} + +func uIntToInt64(num interface{}) (cnum int64) { + switch num.(type) { + case uint64: + return int64(num.(uint64)) + case uint32: + return int64(num.(uint32)) + case uint16: + return int64(num.(uint16)) + case uint8: + return int64(num.(uint8)) + case uint: + return int64(num.(uint)) + } + return -1 +} + +func (i *instruments) StopNetWorkingServer() (err error) { + var id uint32 + id, err = i.requestChannel("com.apple.instruments.server.services.networking") + if err != nil { + return err + } + selector := "stopMonitoring" + args := libimobiledevice.NewAuxBuffer() + + if _, err = i.client.Invoke(selector, args, id, true); err != nil { + return err + } + return nil +} + +func (i *instruments) StartOpenglServer(ctxParent context.Context) (chanFPS chan FPSInfo, chanGPU chan GPUInfo, cancel context.CancelFunc, err error) { + var id uint32 + if ctxParent == nil { + return nil, nil, nil, fmt.Errorf("missing context") + } + ctx, cancelFunc := context.WithCancel(ctxParent) + _outFPS := make(chan FPSInfo) + _outGPU := make(chan GPUInfo) + if id, err = i.requestChannel("com.apple.instruments.server.services.graphics.opengl"); err != nil { + return nil, nil, cancelFunc, err + } + + selector := "availableStatistics" + args := libimobiledevice.NewAuxBuffer() + + if _, err = i.client.Invoke(selector, args, id, true); err != nil { + return nil, nil, cancelFunc, err + } + + selector = "setSamplingRate:" + if err = args.AppendObject(0.0); err != nil { + return nil, nil, cancelFunc, err + } + if _, err = i.client.Invoke(selector, args, id, true); err != nil { + return nil, nil, cancelFunc, err + } + + selector = "startSamplingAtTimeInterval:processIdentifier:" + args = libimobiledevice.NewAuxBuffer() + if err = args.AppendObject(0); err != nil { + return nil, nil, cancelFunc, err + } + if err = args.AppendObject(0); err != nil { + return nil, nil, cancelFunc, err + } + if _, err = i.client.Invoke(selector, args, id, true); err != nil { + return nil, nil, cancelFunc, err + } + + i.registerCallback("", func(m libimobiledevice.DTXMessageResult) { + select { + case <-ctx.Done(): + return + default: + mess := m.Obj + var deviceUtilization = mess.(map[string]interface{})["Device Utilization %"] // Device Utilization + var tilerUtilization = mess.(map[string]interface{})["Tiler Utilization %"] // Tiler Utilization + var rendererUtilization = mess.(map[string]interface{})["Renderer Utilization %"] // Renderer Utilization + + var infoGPU GPUInfo + + infoGPU.DeviceUtilization = uIntToInt64(deviceUtilization) + infoGPU.TilerUtilization = uIntToInt64(tilerUtilization) + infoGPU.RendererUtilization = uIntToInt64(rendererUtilization) + infoGPU.TimeStamp = time.Now().UnixNano() + _outGPU <- infoGPU + + var infoFPS FPSInfo + var fps = mess.(map[string]interface{})["CoreAnimationFramesPerSecond"] + infoFPS.FPS = int(uIntToInt64(fps)) + infoFPS.TimeStamp = time.Now().UnixNano() + _outFPS <- infoFPS + } + }) + go func() { + i.registerCallback("_Golang-iDevice_Over", func(_ libimobiledevice.DTXMessageResult) { + cancelFunc() + }) + select { + case <-ctx.Done(): + var isOpen bool + if _outGPU != nil { + _, isOpen = <-_outGPU + if isOpen { + close(_outGPU) + } + } + if _outFPS != nil { + _, isOpen = <-_outFPS + if isOpen { + close(_outFPS) + } + } + } + return + }() + return _outFPS, _outGPU, cancelFunc, err +} + +func (i *instruments) StopOpenglServer() (err error) { + + id, err := i.requestChannel("com.apple.instruments.server.services.graphics.opengl") + if err != nil { + return err + } + selector := "stop" + args := libimobiledevice.NewAuxBuffer() + + if _, err = i.client.Invoke(selector, args, id, true); err != nil { + return err + } + return nil +} + type Application struct { AppExtensionUUIDs []string `json:"AppExtensionUUIDs,omitempty"` BundlePath string `json:"BundlePath"` @@ -282,3 +660,44 @@ type DeviceInfo struct { ProductVersion string `json:"_productVersion"` XRDeviceClassName string `json:"_xrdeviceClassName"` } + +type CPUInfo struct { + Pid string `json:"PID"` // 线程 + CPUCount int `json:"cpuCount"` // CPU总数 + TimeStamp int64 `json:"timeStamp"` // 时间戳 + CPUUsage float64 `json:"cpuUsage,omitempty"` // 单个进程的CPU使用率 + SysCpuUsage float64 `json:"sysCpuUsage,omitempty"` // 系统总体CPU占用 + AttrCtxSwitch int64 `json:"attrCtxSwitch,omitempty"` // 上下文切换数 + AttrIntWakeups int64 `json:"attrIntWakeups,omitempty"` // 唤醒数 + Mess string `json:"mess,omitempty"` // 提示信息,当PID没输入或者信息错误时提示 +} + +type FPSInfo struct { + FPS int `json:"fps"` + TimeStamp int64 `json:"timeStamp"` +} + +type GPUInfo struct { + TilerUtilization int64 `json:"tilerUtilization"` // 处理顶点的GPU时间占比 + TimeStamp int64 `json:"timeStamp"` + Mess string `json:"mess,omitempty"` // 提示信息,当PID没输入时提示 + DeviceUtilization int64 `json:"deviceUtilization"` // 设备利用率 + RendererUtilization int64 `json:"rendererUtilization"` // 渲染器利用率 +} + +type MEMInfo struct { + Anon int64 `json:"anon"` // 虚拟内存 + PhysMemory int64 `json:"physMemory"` // 物理内存 + Rss int64 `json:"rss"` // 总内存 + Vss int64 `json:"vss"` // 虚拟内存 + TimeStamp int64 `json:"timeStamp"` // + Mess string `json:"mess,omitempty"` // 提示信息,当PID没输入或者信息错误时提示 +} + +type NetWorkingInfo struct { + RxBytes int64 `json:"rxBytes"` + RxPackets int64 `json:"rxPackets"` + TxBytes int64 `json:"txBytes"` + TxPackets int64 `json:"txPackets"` + TimeStamp int64 `json:"timeStamp"` +} diff --git a/instruments_test.go b/instruments_test.go index 8e208e6..98bf203 100644 --- a/instruments_test.go +++ b/instruments_test.go @@ -22,7 +22,7 @@ func setupInstrumentsSrv(t *testing.T) { func Test_instruments_AppLaunch(t *testing.T) { setupInstrumentsSrv(t) - // bundleID = "com.leixipaopao.WebDriverAgentRunner.xctrunner" + bundleID = "com.DataMesh.CheckList" // pid, err := dev.AppLaunch(bundleID) pid, err := instrumentsSrv.AppLaunch(bundleID) diff --git a/pkg/libimobiledevice/keyedarchiver.go b/pkg/libimobiledevice/keyedarchiver.go index 1cb2e52..f942c5c 100644 --- a/pkg/libimobiledevice/keyedarchiver.go +++ b/pkg/libimobiledevice/keyedarchiver.go @@ -2,6 +2,7 @@ package libimobiledevice import ( "reflect" + "strconv" "time" "howett.net/plist" @@ -202,9 +203,17 @@ func (ka *NSKeyedArchiver) convertValue(v interface{}) interface{} { values := m["NS.objects"].([]interface{}) for i := 0; i < len(keys); i++ { - key := ka.objRefVal[keys[i].(plist.UID)].(string) + var keyValue string + key := ka.objRefVal[keys[i].(plist.UID)] + switch key.(type) { + case uint64: + keyValue = strconv.Itoa(int(key.(uint64))) + break + default: + keyValue = key.(string) + } val := ka.convertValue(ka.objRefVal[values[i].(plist.UID)]) - ret[key] = val + ret[keyValue] = val } return ret case NSMutableArrayClass.Classes[0], NSArrayClass.Classes[0]: