GPM合并资料整理-GEM部分

一、性能数据上报项

1. CPU模块

上报键值 说明 采集平台
cpu 当前进程cpu使用率平均值 Android & iOS
totcpu 系统cpu总使用率平均值 Android & iOS
cpu_temp_max cpu最高温度 Android
cpu_temp_avg cpu温度平均值 Android
gpu_temp_avg gpu温度平均值 Android
gpu_temp_max gpu最高温度 Android
gpu gpu使用率平均值 Android
gpu_model gpu型号 Android
cpu_model cpu型号 Android
cpu_core cpu核心数 Android
cpu_freq cpu最大时钟频率 Android

2. memory及存储模块

上报键值 说明 采集平台
mem 当前进程内存使用量平均值 Android & iOS
availmem 当前设备可用内存量平均值 Android & iOS
max_mem 内存使用量最大值 Android & iOS
start_mem 开始对局时内存使用量 Android & iOS
end_mem 结束对局时内存使用量 Android & iOS
totalmem 当前设备总内存量 Android & iOS
total_storage 当前设备总存储空间大小 Android & iOS
free_storage 当前设备可用存储空间大小 Android & iOS

3. 网卡模块

上报键值 说明 采集平台
wsndpkt 无线网卡发包总数 Android & iOS
wrcvpkt 无线网卡收包总数 Android & iOS
wsnddrop 无线网卡发包丢包 Android & iOS
wrcvdrop 无线网卡收包丢包 Android & iOS
wsnderr 无线网卡发包错包 Android & iOS
wrcverr 无线网卡收包错包 Android & iOS
msndpkt 移动网卡发包总数 Android & iOS
mrcvpkt 移动网卡收包总数 Android & iOS
msnddrop 移动网卡发包丢包 Android & iOS
mrcvdrop 移动网卡收包丢包 Android & iOS
msnderr 移动网卡发包错包 Android & iOS
mrcverr 移动网卡收包错包 Android & iOS

4. FPS模块

上报键值 说明 采集平台
fmin fps最小值 Android & iOS
fmax fps最大值 Android & iOS
favg fps平均值 Android & iOS
fheavy fps严重抖动个数(本次fps值比上次fps值低10以上) Android & iOS
flight fps轻微抖动个数(本次fps值比上次fps值低4以上,10以下) Android & iOS
ftotal fps采集总个数 Android & iOS
fcntx0 fps自定义抖动个数(阈值由云控下发) Android & iOS
lfps1 低于云控下发的lfps1阈值的个数 Android & iOS
lfps2 低于云控下发的lfps2阈值的个数 Android & iOS
lfps3 低于云控下发的lfps3阈值的个数 Android & iOS

5. 电量模块

上报键值 说明 采集平台
battery 电量消耗(开始-结束) Android & iOS
start_battery 开始对局时电量值 Android & iOS
end_battery 结束对局时电量值 Android & iOS
bs 对局过程中是否有充电操作 Android & iOS
bt 电池温度平均值 Android
max_battery_temp 电池温度最大值 Android

6. 网络相关模块

上报键值 说明 采集平台
netflow 流量消耗 Android & iOS
devices 当前wifi下连接设备数 Android & iOS
wifi_num wifi个数 Android
wifi_rssi 当前wifi信号强度 Android
wifi_speed 当前wifi链接速度 Android
gate_delay 到网关延迟(ping) Android & iOS
signal_level 移动信号强度 Android
xg 当前网络类型 Android & iOS
ldns 本机DNS服务器 Android & iOS

7. 机型相关

上报键值 说明 采集平台
manufacturer 厂商 Android
brand 品牌 Android
model 型号 Android
resolution 分辨率 Android

cpu&memory采集具体实现代码

1. cpu

  • Android采集代码:

      public static float totalCpuUsageRate() {
      	return getTotalCpuUsageRate(USAGE_RATE_CAL_INTERVAL_MILLS_DEFAULT);
      }
    
      public static float getTotalCpuUsageRate(long calIntervalMills) {
      	if (0 > calIntervalMills) {
      		return VALUE_GET_FAILED;
      	}
      	float totalCpuUsageRate = VALUE_GET_FAILED;
      	try {
      		TotalCpuStatInfo totalCpuStatInfo1 = TotalCpuStatInfo.get();
      		long cpuTotal1 = totalCpuStatInfo1.getCpuTotal();
      		if (0 > cpuTotal1) {
      			return totalCpuUsageRate;
      		}
      		long total1 = totalCpuStatInfo1.getTotal();
      		if (0 > total1) {
      			return totalCpuUsageRate;
      		}
      		try {
      			Thread.sleep(calIntervalMills);
      		} catch (Exception e) {
      			GPMLogger.w(e, "getTotalCpuUsageRate failed");
      		}
      		TotalCpuStatInfo totalCpuStatInfo2 = TotalCpuStatInfo.get();
      		long cpuTotal2 = totalCpuStatInfo2.getCpuTotal();
      		if (0 > cpuTotal2) {
      			return totalCpuUsageRate;
      		}
      		long total2 = totalCpuStatInfo2.getTotal();
      		if (0 > total2) {
      			return totalCpuUsageRate;
      		}
      		long totalDiff = total2 - total1;
      		if (0 == totalDiff) {
      			// NOTE: totalDiff为分母
      			return totalCpuUsageRate;
      		}
      		long cpuTotalDiff = cpuTotal2 - cpuTotal1;
      		return 100 * cpuTotalDiff / (totalDiff + 0f);
      	} catch (Exception e) {
      		GPMLogger.w(e, "getTotalCpuUsageRate failed");
      	}
      		return totalCpuUsageRate;
      	}
    
      public static float myCpuUsageRate() {
      	return getMyCpuUsageRate(USAGE_RATE_CAL_INTERVAL_MILLS_DEFAULT);
      }
    
      public static float getMyCpuUsageRate(long calIntervalMills) {
      	if (0 > calIntervalMills) {
      		return VALUE_GET_FAILED;
      	}
      	float myCpuUsageRate = VALUE_GET_FAILED;
      	try {
      		TotalCpuStatInfo totalCpuStatInfo1 = TotalCpuStatInfo.get();
      		long total1 = totalCpuStatInfo1.getTotal();
      		if (0 > total1) {
      			return myCpuUsageRate;
      		}
      		MyCpuStatInfo myCpuStatInfo1 = MyCpuStatInfo.get();
      		long myCpuTotal1 = myCpuStatInfo1.getCpuTotal();
      		if (0 > myCpuTotal1) {
      			return myCpuUsageRate;
      		}
      		try {
      			Thread.sleep(calIntervalMills);
      		} catch (Exception e) {
      			GPMLogger.w(e, "getMyCpuUsageRate failed");
      		}
      		TotalCpuStatInfo totalCpuStatInfo2 = TotalCpuStatInfo.get();
      		long total2 = totalCpuStatInfo2.getTotal();
      		if (0 > total2) {
      			return myCpuUsageRate;
      		}
      		MyCpuStatInfo myCpuStatInfo2 = MyCpuStatInfo.get();
      		long myCpuTotal2 = myCpuStatInfo2.getCpuTotal();
      		if (0 > myCpuTotal2) {
      			return myCpuUsageRate;
      		}
      		long totalDiff = total2 - total1;
      		if (0 == totalDiff) {
      			// NOTE: totalDiff为分母
      			return myCpuUsageRate;
      		}
      		long myCpuTotalDiff = myCpuTotal2 - myCpuTotal1;
      		return 100 * myCpuTotalDiff / (totalDiff + 0f);
      	} catch (Exception e) {
      		GPMLogger.w(e, "getTotalCpuUsageRate failed");
      	}
      	return myCpuUsageRate;
      }
    
      private static class TotalCpuStatInfo {
      	private static final int MIN_CPU_STAT_ITEM_NUM = 9;
      	private static final String[] INVALID_RAW_CPU_STAT_ITEMS =
              new String[MIN_CPU_STAT_ITEM_NUM];
    
      	public final long user;
      	public final long nice;
      	public final long system;
      	public final long idle;
      	public final long iowait;
      	public final long irq;
      	public final long softirq;
    
      	public static TotalCpuStatInfo get() {
      		BufferedReader cpuStatInfoReader = null;
      		try {
      			cpuStatInfoReader = new BufferedReader(new InputStreamReader(new FileInputStream(STAT_ABS_DIR), UTF8_CHARSET));
      			return new TotalCpuStatInfo(cpuStatInfoReader.readLine());
      		} catch (Exception e) {
      			// NOTE: 高版本上获取不到, 不打印堆栈
      			GPMLogger.d("get total cpu stat info failed");
      			return new TotalCpuStatInfo(null);
      		} finally {
      			if (null != cpuStatInfoReader) {
      				try {
      					cpuStatInfoReader.close();
      				} catch (IOException ignored) {
      				}
      			}
      		}
      	}
    
      	private TotalCpuStatInfo(String cpuStatLine) {
      		String[] rawCpuStatItems = CpuStatInfoHelper.buildRawCpuStatItems(cpuStatLine, MIN_CPU_STAT_ITEM_NUM, INVALID_RAW_CPU_STAT_ITEMS);
      		user = CpuStatInfoHelper.buildCpuStatItem(rawCpuStatItems[2]);
      		nice = CpuStatInfoHelper.buildCpuStatItem(rawCpuStatItems[3]);
      		system = CpuStatInfoHelper.buildCpuStatItem(rawCpuStatItems[4]);
      		idle = CpuStatInfoHelper.buildCpuStatItem(rawCpuStatItems[5]);
      		iowait = CpuStatInfoHelper.buildCpuStatItem(rawCpuStatItems[6]);
      		irq = CpuStatInfoHelper.buildCpuStatItem(rawCpuStatItems[7]);
      		softirq = CpuStatInfoHelper.buildCpuStatItem(rawCpuStatItems[8]);
      	}
    
      	public long getCpuTotal() {
      		return user + nice + system + iowait + irq + softirq;
      	}
    
      	public long getTotal() {
      		return user + nice + system + idle + iowait + irq + softirq;
      	}
      }
    
      private static class MyCpuStatInfo {
      	private static final int MIN_CPU_STAT_ITEM_NUM = 17;
      	private static final String[] INVALID_RAW_CPU_STAT_ITEMS = new String[MIN_CPU_STAT_ITEM_NUM];
    
      	public final long utime;
      	public final long stime;
      	public final long cutime;
      	public final long cstime;
      	
      	public static MyCpuStatInfo get() {
      		BufferedReader cpuStatInfoReader = null;
      		try {
      			cpuStatInfoReader = new BufferedReader(new InputStreamReader(new FileInputStream(MY_STAT_ABS_DIR), UTF8_CHARSET));
      			return new MyCpuStatInfo(cpuStatInfoReader.readLine());
      		} catch (Exception e) {
      			GPMLogger.w(e, "get my cpu stat info failed");
      			return new MyCpuStatInfo(null);
      		} finally {
      			if (null != cpuStatInfoReader) {
      				try {
      					cpuStatInfoReader.close();
      				} catch (IOException ignored) {
      				}
      			}
      		}
      	}
    
      	private MyCpuStatInfo(String cpuStatLine) {
      		String[] rawCpuStatItems = CpuStatInfoHelper.buildRawCpuStatItems(cpuStatLine, MIN_CPU_STAT_ITEM_NUM, INVALID_RAW_CPU_STAT_ITEMS);
      		utime = CpuStatInfoHelper.buildCpuStatItem(rawCpuStatItems[13]);
      		stime = CpuStatInfoHelper.buildCpuStatItem(rawCpuStatItems[14]);
      		cutime = CpuStatInfoHelper.buildCpuStatItem(rawCpuStatItems[15]);
      		cstime = CpuStatInfoHelper.buildCpuStatItem(rawCpuStatItems[16]);
      	}
    
      	public long getCurrentThreadCpuTotal() {
      		return utime + stime;
      	}
    
      	public long getCpuTotal() {
      		return utime + stime + cutime + cstime;
      	}
      }
    
  • IOS采集代码:

      // ============当前进程cpu使用率============
      - (double) getCPU {
      	kern_return_t kr;
      	task_info_data_t tinfo;
      	mach_msg_type_number_t task_info_count;
      	task_info_count = TASK_INFO_MAX;
      	kr = task_info(mach_task_self(), TASK_BASIC_INFO, (task_info_t)tinfo, &task_info_count);
      	if (kr != KERN_SUCCESS) {
      		GSDKLOG(@"================CPU:%.2f",-1.0f);
      		return -1;
      	}
      	task_basic_info_t      basic_info;
      	thread_array_t         thread_list;
      	mach_msg_type_number_t thread_count;
      	thread_info_data_t     thinfo;
      	mach_msg_type_number_t thread_info_count;
      	thread_basic_info_t basic_info_th;
      	uint32_t stat_thread = 0; // Mach threads
      	basic_info = (task_basic_info_t)tinfo;
      	// get threads in the task
      	kr = task_threads(mach_task_self(), &thread_list, &thread_count);
      	if (kr != KERN_SUCCESS) {
      		GSDKLOG(@"=============CPU:%.2f",-1.0f);
      		return -1;
      	}
      	if (thread_count > 0)
      		stat_thread += thread_count;
      	long tot_sec = 0;
      	long tot_usec = 0;
      	float tot_cpu = 0;
      	int j;
      	for (j = 0; j < thread_count; j++) {
      		thread_info_count = THREAD_INFO_MAX;
      		kr = thread_info(thread_list[j], THREAD_BASIC_INFO, (thread_info_t)thinfo, &thread_info_count);
      		if (kr != KERN_SUCCESS) {
      			GSDKLOG(@"=========CPU:%.2f",-1.0f);
      			return -1;
      		}
      		basic_info_th = (thread_basic_info_t)thinfo;
      		if (!(basic_info_th->flags & TH_FLAGS_IDLE)) {
      			tot_sec = tot_sec + basic_info_th->user_time.seconds + basic_info_th->system_time.seconds;
      			tot_usec = tot_usec + basic_info_th->system_time.microseconds + basic_info_th->system_time.microseconds;
      			tot_cpu = tot_cpu + basic_info_th->cpu_usage / (float)TH_USAGE_SCALE * 100.0;
      		}
      	} // for each thread
      	kr = vm_deallocate(mach_task_self(), (vm_offset_t)thread_list, thread_count * sizeof(thread_t));
      	GSDKLOG(@"=============CPU:%.2f",tot_cpu);
      	return tot_cpu;
      }
    
    
      // ============当前进程cpu使用率============
      processor_info_array_t cpuInfo, prevCpuInfo;
      mach_msg_type_number_t numCpuInfo, numPrevCpuInfo;
      unsigned numCPUs;
      NSLock * CPUUsageLock;
    
      - (void) getSystemCPU {
      	int mib[2U] = { CTL_HW, HW_NCPU };
      	size_t sizeOfNumCPUs = sizeof(numCPUs);
      	int status = sysctl(mib, 2U, &numCPUs, &sizeOfNumCPUs, NULL, 0U);
      	if (status)
      		numCPUs = 1;
      	CPUUsageLock = [[NSLock alloc] init];
      }
    
      - (double) getSystemCPUCircle {
      	natural_t numCPUsU = 0U;
      	kern_return_t err = host_processor_info(mach_host_self(), PROCESSOR_CPU_LOAD_INFO, &numCPUsU, &cpuInfo, &numCpuInfo);
      	if (err == KERN_SUCCESS) {
      		[CPUUsageLock lock];
      		float tot_inUse = 0;
      		float tot_total = 0;
      		for(unsigned i = 0U; i < numCPUs; ++i) {
      			float inUse, total;
      			if(prevCpuInfo) {
      				inUse = ((cpuInfo[(CPU_STATE_MAX * i) + CPU_STATE_USER] - prevCpuInfo[(CPU_STATE_MAX * i) + CPU_STATE_USER])
                       + (cpuInfo[(CPU_STATE_MAX * i) + CPU_STATE_SYSTEM] - prevCpuInfo[(CPU_STATE_MAX * i) + CPU_STATE_SYSTEM])
                       + (cpuInfo[(CPU_STATE_MAX * i) + CPU_STATE_NICE]   - prevCpuInfo[(CPU_STATE_MAX * i) + CPU_STATE_NICE]));
      				total = inUse + (cpuInfo[(CPU_STATE_MAX * i) + CPU_STATE_IDLE] - prevCpuInfo[(CPU_STATE_MAX * i) + CPU_STATE_IDLE]);
      			} else {
      				inUse = cpuInfo[(CPU_STATE_MAX * i) + CPU_STATE_USER] + cpuInfo[(CPU_STATE_MAX * i) + CPU_STATE_SYSTEM] + cpuInfo[(CPU_STATE_MAX * i) + CPU_STATE_NICE];
      				total = inUse + cpuInfo[(CPU_STATE_MAX * i) + CPU_STATE_IDLE];
      			}
      			tot_inUse += inUse;
      			tot_total += total;
      		}
      		[CPUUsageLock unlock];
      		if (prevCpuInfo) {
      			size_t prevCpuInfoSize = sizeof(integer_t) * numPrevCpuInfo;
      			vm_deallocate(mach_task_self(), (vm_address_t)prevCpuInfo, prevCpuInfoSize);
      		}
      
      		prevCpuInfo = cpuInfo;
      		numPrevCpuInfo = numCpuInfo;
      
      		cpuInfo = NULL;
      		numCpuInfo = 0U;
      		if (tot_total > 0) {
      			GSDKLOG(@"CPU Usage: %.2f%%",tot_inUse/tot_total * 100.f);
      			return tot_inUse/tot_total * 100.f;
      		} else {
      			return -1;
      		}
      	} else {
      		GSDKLOG(@"Error!");
      		return -1;
      	}
      }
    

2. memory

  • Android采集代码:

      public static long getAvailableMemory(Context context) {
      	if (null == context) {
      		return VALUE_GET_FAILED;
      	}
      	@SuppressWarnings("AlibabaLowerCamelCaseVariableNaming")
      	long availableMemoryInMB = VALUE_GET_FAILED;
      	try {
      		ActivityManager activityManager =
                  (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
      		if (null == activityManager) {
      			return availableMemoryInMB;
      		}
      		ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
      		activityManager.getMemoryInfo(memoryInfo);
      		availableMemoryInMB = memoryInfo.availMem / (1024 * 1024);
      	} catch (Exception e) {
      		GPMLogger.w(e, "availableMemoryInMB failed");
      	}
      	return availableMemoryInMB;
      }
      
      // ============当前设备可用内存大小(原方案)============
      public static long getProcessMemery() {
      	// TODO: 厂商反馈Debug.getPss()计算还是过于复杂,后续考虑使用Process.getPss()或者仿照Process.getPss()实现
      	// NOTE: 当前实现应该在低优先级后台线程调用
      	if (Build.VERSION_CODES.ICE_CREAM_SANDWICH <= Build.VERSION.SDK_INT) {
      		return Debug.getPss() / 1024;
      	}
      	// NOTE: 仅在Android14下使用,不涉及私有API访问限制
      	// Android 2.3上Process已有getPss方法
      	// https://android.googlesource.com/platform/frameworks/base/+/refs/heads/gingerbread/core/java/android/os/Process.java
      	try {
      		Method getPssMethod = Process.class.getMethod(GET_PSS_METHOD_NAME, int.class);
      		return ((long) getPssMethod.invoke(null, Process.myPid())) / 1024;
      	} catch (Exception e) {
      		return VALUE_GET_FAILED;
      	}
      }
      // ============当前设备可用内存大小(优化后方案)============
      private static ActivityManager sActivityMgr = null;
      private static int sPid = 0;
    
      public static int getProcessMemery(Context context) {
      	try {
      		if (sActivityMgr == null) {
      			sActivityMgr = (ActivityManager) context.getSystemService(context.ACTIVITY_SERVICE);
      		}
    
      		if (sPid == 0) {
      			sPid = android.os.Process.myPid();
      		}
    
      		if (sActivityMgr == null || sPid == 0) {
      			return -1;
      		}
    
      		int[] myMempid = new int[]{sPid};
      		Debug.MemoryInfo[] memoryInfo = sActivityMgr.getProcessMemoryInfo(myMempid);
      		int memSize = 0;
      		if (memoryInfo != null && memoryInfo.length > 0) {
      			memSize = memoryInfo[0].getTotalPss();
      		}
      		return memSize;
      	} catch (Exception e) {
      		Logger.e("PSS fetch error: " + e.getMessage());
      	}
      	return -1;
      }
    
  • IOS采集代码:

      // ============当前设备可用内存大小============
      - (double) getSystemAvailableMemory {
      	// 获取当前设备可用内存(单位:MB)
      	vm_statistics_data_t vmStats;
      	mach_msg_type_number_t infoCount = HOST_VM_INFO_COUNT;
      	kern_return_t kernReturn = host_statistics(mach_host_self(), HOST_VM_INFO, (host_info_t)&vmStats, &infoCount);
      	if (kernReturn != KERN_SUCCESS) {
      		GSDKLOG(@"AvailableMemory=====%.2f",-1.0f);
      		return -1;
      	}
    
      	double availMem = (vm_page_size * vmStats.free_count + vm_page_size * vmStats.inactive_count) / 1024.0 / 1024.0;
      	GSDKLOG(@"AvailableMemory=====%.2f",availMem);
      	return availMem;
      }
    
      // ============当前进程使用内存大小(原方案)============
      - (double) getFootPrint {
      	task_vm_info_data_t vmInfo;
      	mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
      	kern_return_t result = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t) &vmInfo, &count);
      	if (result != KERN_SUCCESS) {
      		GSDKLOG(@"FootPrint=====%.2f",-1.0f);
      		return -1;
      	}
      	GSDKLOG(@"FootPrint=====%i",(int)((vmInfo.phys_footprint) / 1024.0 / 1024.0));
      	return vmInfo.phys_footprint / 1024.0 / 1024.0;
      }
    
      // ============当前进程使用内存大小(优化后方案)============
      - (instancetype) init {
      	if (self = [super init]) {
      		_isLowLevelDevice = NO;
      		struct utsname systemInfo;
      		uname(&systemInfo);
      		NSString *platform = [NSString stringWithCString:systemInfo.machine encoding:NSASCIIStringEncoding];
      		if ([platform isEqualToString:@"iPhone7,1"] || [platform isEqualToString:@"iPhone7,2"] || [platform isEqualToString:@"iPhone6,1"] || [platform isEqualToString:@"iPhone6,2"]) {
      			_isLowLevelDevice = YES;
      		}
      	}
      	return self;
      }
      
      - (double) getFootPrint {
      	task_vm_info_data_t vmInfo;
      	mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
      	kern_return_t result = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t) &vmInfo, &count);
      	if (result != KERN_SUCCESS) {
      		GSDKLOG(@"FootPrint=====%.2f",-1.0f);
      		return -1;
      	}
      	double footPrint = -1;
      	if (_isLowLevelDevice || [UIDevice.currentDevice.systemVersion doubleValue] >= 12.0) {
      		footPrint = vmInfo.phys_footprint / 1024.0 / 1024.0;
      	}
      	if ([UIDevice.currentDevice.systemVersion doubleValue] < 12.0) {
      		footPrint = (vmInfo.internal + vmInfo.compressed - vmInfo.purgeable_volatile_pmap) / 1024.0 / 1024.0;
      	}
      	GSDKLOG(@"FootPrint=====%i",int((vmInfo.phys_footprint) / 1024.0 / 1024.0));
      	return footPrint;
      }
    

二、性能数据统一上报类示例

使用同一个TDMReportHelper实例进行上报即可:

TDMReportHelper eventReportHandler = new TDMReportHelper(eventName);
eventReportHandler.addSS(key, value);
eventReportHandler.report();
eventReportHandler.destory();
相关推荐
Java小白笔记2 小时前
Mac中安装homebrew
macos
HerayChen5 小时前
HbuildderX运行到手机或模拟器的Android App基座识别不到设备 mac
android·macos·智能手机
hairenjing11235 小时前
在 Android 手机上从SD 卡恢复数据的 6 个有效应用程序
android·人工智能·windows·macos·智能手机
小李飞刀李寻欢7 小时前
Mac电脑如何解压rar压缩包
macos·rar·解压
Java小白笔记7 小时前
Mac中禁用系统更新
macos
AndyFrank7 小时前
mac crontab 不能使用问题简记
linux·运维·macos
Mac新人7 小时前
一招解决Mac没有剪切板历史记录的问题
macos·mac
王拴柱7 小时前
Mac保护电池健康,延长电池使用寿命的好方法
macos·mac
daa207 小时前
macos中安装和设置ninja
macos
Java小白笔记9 小时前
Mac解决 zsh: command not found: ll
macos