android,flutter 混合开发,pigeon通信,传参
文章目录
- app效果
- native和flutter通信的基础知识
- 1. 编解码器 一致性和完整性,安全性,性能优化
- 2. android代码
- 3. dart代码
- 1. 创建flutter_module
- 2.修改 Android 项目的 settings.gradle,添加 Flutter module
- 3. 在 Android app 的 build.gradle 中添加依赖
- 4. 原生和flutter_module通信BasePlugin
- 5. io.flutter.embedding.engine.plugins.util.GeneratedPluginRegister 自动注册插件
- 6 手动注册
- 7. 通信关闭flutter页面
- 8. dartEntrypointArgs传入参数
- 9. 通信传入参数
- 9.1 发送参数
- 9.1 接收参数
- 10. android代码
- 11 源码
app效果
native和flutter通信的基础知识
低级的消息通道,一般不直接使用
ServicesBinding.instance.defaultBinaryMessenger
高级别的消息通道,对defaultBinaryMessenger进行了封装,提供的更简单的api发送和接收消息
BasicMessageChannel MethodChannel EventChannel
MethodChannel EventChannel底层都是基于BasicMessageChannel
1. 编解码器 一致性和完整性,安全性,性能优化
MethodChannel,EventChannel->StandardMethodCodec
BasicMessageChannel->StandardMessageCodec
StandardMessageCodec:支持基本的数据类型(字符串、数字、布尔值、字节数组、列表、映射等)。
StandardMethodCodec:除了支持基本的数据类型外,还支持方法调用的参数和返回结果的编码和解码。
StringCodec:由于只处理字符串,StringCodec 的实现更为简单,性能更高
2. android代码
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {basicMessageChannel = BasicMessageChannel(flutterEngine.dartExecutor.binaryMessenger, BASIC_CHANNEL, StringCodec.INSTANCE)//创建MethodChannelmethodChannel =MethodChannel(flutterEngine.dartExecutor, BASIC_CHANNEL, StandardMethodCodec.INSTANCE)//创建EventChanneleventChannel = EventChannel(flutterEngine.dartExecutor, BASIC_CHANNEL)basicMessageChannel.send("dddd")basicMessageChannel.setMessageHandler { message, reply ->reply.reply("dddd")Log.e("dddd", message.toString())}methodChannel.invokeMethod("getPlatformVersion", null)methodChannel.setMethodCallHandler { call, result ->when (call.method) {"getPlatformVersion" -> {result.success("Android ${android.os.Build.VERSION.RELEASE}")}else -> {result.notImplemented()}}}eventChannel.setStreamHandler(object : EventChannel.StreamHandler {override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {eventSink = eventseventSink?.success("Event")}override fun onCancel(arguments: Any?) {eventSink = null}})}
3. dart代码
class _StudyChannelState extends State<StudyChannel> {MethodChannel methodChannel = const MethodChannel('base_plugin');EventChannel eventChannel = const EventChannel('base_plugin');static const basicMessageChannel = BasicMessageChannel<String>('base_plugin', StringCodec()); void initState() {super.initState();//只接收eventChannel.receiveBroadcastStream().listen((value){});//监听methodChannel.setMethodCallHandler((call){call.method == "getData" ? print("getData") : print("method=$call");return Future.value("methodChannel=$call");});//执行,相当于发送methodChannel.invokeListMethod("getData");//发送basicMessageChannel.send("data");//监听basicMessageChannel.setMessageHandler((message){return Future.value("basicMessageChannel=$message");});}Widget build(BuildContext context) {return const Placeholder();}
}
1. 创建flutter_module
File->New->New Flutter Project
name为flutter_module
选择project type为module
2.修改 Android 项目的 settings.gradle,添加 Flutter module
pluginManagement {repositories {google {content {includeGroupByRegex("com\\.android.*")includeGroupByRegex("com\\.google.*")includeGroupByRegex("androidx.*")}}mavenCentral()gradlePluginPortal()}
}
dependencyResolutionManagement {repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)repositories {google()mavenCentral()maven(url = "https://storage.googleapis.com/download.flutter.io")}
}rootProject.name = "hunheandroid"
include(":app")
//主要是这一句
apply(from = "../flutter_module/.android/include_flutter.groovy")
3. 在 Android app 的 build.gradle 中添加依赖
dependencies {implementation(project(":flutter"))
}
4. 原生和flutter_module通信BasePlugin
参考文章生成对应的io/flutter/plugins/Messages.g.kt
用pigeon kotlin swift写一个自己的插件
拷贝BasePlugin
package io.flutter.pluginsimport android.app.Activity
import android.util.Log
import androidx.annotation.NonNull
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding/** BasePlugin */
/** BasePlugin */
class BasePlugin : FlutterPlugin, HostMessageApi, ActivityAware {companion object {private var flutterMessageApi: FlutterMessageApi? = nullfun getFlutterMessageApi(): FlutterMessageApi? {return flutterMessageApi}}private var activity: Activity? = nullprivate var tag = "BasePlugin"override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {Log.e(tag, "onAttachedToEngine-start")HostMessageApi.setUp(flutterPluginBinding.binaryMessenger, this)flutterMessageApi = FlutterMessageApi(flutterPluginBinding.binaryMessenger)Log.e(tag, "onAttachedToEngine-end")}override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {HostMessageApi.setUp(binding.binaryMessenger, null)}override fun flutter2Native(message: String, type: Long): String {print("flutter2Native=message=start=>$message=type=$type")return "flutter2Native=message=$message=type=$type"}override fun flutter2NativeAsync(message: String,type: Long,callback: (Result<String>) -> Unit) {print("flutter2NativeAsync=message=$message=type=$type")callback(Result.success("flutter2NativeAsync=message=$message=type=$type"))if (type == 1L) {closeCurrentActivity()}}private fun closeCurrentActivity() {activity?.finish()}override fun onAttachedToActivity(binding: ActivityPluginBinding) {this.activity = binding.activity}override fun onDetachedFromActivityForConfigChanges() {}override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {}override fun onDetachedFromActivity() {this.activity = null}
}
5. io.flutter.embedding.engine.plugins.util.GeneratedPluginRegister 自动注册插件
public static void registerGeneratedPlugins(@NonNull FlutterEngine flutterEngine) {try {Class<?> generatedPluginRegistrant =Class.forName("io.flutter.plugins.GeneratedPluginRegistrant");Method registrationMethod =generatedPluginRegistrant.getDeclaredMethod("registerWith", FlutterEngine.class);registrationMethod.invoke(null, flutterEngine);} catch (Exception e) {Log.e(TAG,"Tried to automatically register plugins with FlutterEngine ("+ flutterEngine+ ") but could not find or invoke the GeneratedPluginRegistrant.");Log.e(TAG, "Received exception while registering", e);}}
package io.flutter.plugins;import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import io.flutter.Log;import io.flutter.embedding.engine.FlutterEngine;/*** Generated file. Do not edit.* This file is generated by the Flutter tool based on the* plugins that support the Android platform.*/
@Keep
public final class GeneratedPluginRegistrant {private static final String TAG = "GeneratedPluginRegistrant";public static void registerWith(@NonNull FlutterEngine flutterEngine) {try {Log.e(TAG, "registerWith-start");flutterEngine.getPlugins().add(new BasePlugin());Log.e(TAG, "registerWith-end");} catch (Exception e) {Log.e(TAG, "Error registering plugin base_plugin, com.app.base_plugin.BasePlugin", e);}}
}
6 手动注册
flutterEngine.getPlugins().add(new BasePlugin());
7. 通信关闭flutter页面
使用flutter和原生通信,在收到消息以后,关闭当前页面
BasePlugin实现ActivityAware,获取到activity
在收到flutter传入进来的消息的时候关闭activity
8. dartEntrypointArgs传入参数
//跳转带入参数
val initialRoute = listOf( "aaa","bbbb")startActivity(withNewEngine().dartEntrypointArgs(initialRoute) .build(this))
//args就是传入的参数
void main(List<String> args){print("args=$args");runApp(const MyApp());
}
9. 通信传入参数
9.1 发送参数
val initialRoute = listOf( "/page2","bbbb")BasePlugin.getFlutterMessageApi()?.native2Flutter(initialRoute.joinToString("#"), 1){Log.e(tag, "flutter2NativeAsync: $it")FlutterLibrary.startFlutterPage(this)}
9.1 接收参数
class RouterPage extends StatefulWidget {const RouterPage({super.key});State<RouterPage> createState() => _RouterPageState();
}class _RouterPageState extends State<RouterPage> implements FlutterMessageApi {late String args;void initState() {super.initState();FlutterMessageApi.setUp(this);}String native2Flutter(String message, int type) {print("native2Flutter=$message $type");setState(() {args = message;});return "我是原生调用flutter方法返回的值=》native2Flutter";}Future<String> native2FlutterAsync(String message, int type) {args = message;print("native2Flutter=$message $type");return Future.value("我是原生调用flutter方法返回的值=》native2FlutterAsync");}Widget build(BuildContext context) {// final value = widget.args.isNotEmpty ? widget.args[0] : "/";if (args.isEmpty||!args.contains("#")) {return MyHomePage(title: "首页");}final value = args.split("#").first;switch (value) {case '/page2':return SecondPage();}print("_RouterPageState=build");return MyHomePage(title: "首页");}
}class MyHomePage extends StatefulWidget {const MyHomePage({super.key, required this.title});final String title;State<MyHomePage> createState() => _MyHomePageState();
}class _MyHomePageState extends State<MyHomePage> {int _counter = 0;void _incrementCounter() {setState(() {_counter++;});}void initState() {super.initState();}Widget build(BuildContext context) {print("_MyHomePageState=build");return Scaffold(appBar: AppBar(title: Text(widget.title), leading: BackButton()),body: Center(child: Column(mainAxisAlignment: MainAxisAlignment.center,children: <Widget>[TextButton(onPressed: () {BasePlugin.flutter2NativeAsync("flutter2NativeAsync", 1).then((value,) {print("flutter2NativeAsync=$value");});},child: Text('flutter2NativeAsync'),),TextButton(onPressed: () {Navigator.of(context).pushNamed("/page2");},child: Text('flutter2NativeAsync'),),const Text('You have pushed the button this many times:'),Text('$_counter',style: Theme.of(context).textTheme.headlineMedium,),],),),floatingActionButton: FloatingActionButton(onPressed: _incrementCounter,tooltip: 'Increment',child: const Icon(Icons.add),), // This trailing comma makes auto-formatting nicer for build methods.);}}class SecondPage extends StatelessWidget {const SecondPage({super.key});Widget build(BuildContext context) {print("SecondPage=build");return Scaffold(appBar: AppBar(title: Text("SecondPage")),body: Center(child: TextButton(onPressed: () {Navigator.of(context).pop();},child: Text('关闭'),),),);}
}
10. android代码
package com.example.hunheandroidimport android.app.Application
import android.content.Context
import android.content.Intent
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.FlutterEngineCache
import io.flutter.embedding.engine.FlutterEngineGroup
import io.flutter.embedding.engine.dart.DartExecutor
import io.flutter.plugins.BasePluginconst val ENGINE_ID = "my_engine_id"
object FlutterLibrary {fun init(application: Application) {FlutterEngineManager.getInstance().init(application)}fun startFlutterPage(context: Context) {val intent = FlutterActivity.withCachedEngine(ENGINE_ID).build(context).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)context.startActivity(intent)}
}// FlutterEngineManager.kt
class FlutterEngineManager private constructor() {private lateinit var engineGroup: FlutterEngineGroupprivate var engine: FlutterEngine? = nullcompanion object {private var instance: FlutterEngineManager? = nullfun getInstance(): FlutterEngineManager {return instance ?: synchronized(this) {instance ?: FlutterEngineManager().also { instance = it }}}}fun init(application: Application) {engineGroup = FlutterEngineGroup(application)engine = engineGroup.createAndRunEngine(application,DartExecutor.DartEntrypoint.createDefault())FlutterEngineCache.getInstance().put(ENGINE_ID, engine)engine!!.plugins.add(BasePlugin())}fun getEngine(): FlutterEngine? {return engine}fun destroyEngine() {engine?.destroy()engine = null}
}class GoFlutterActivity : FlutterActivity(), HostMessageApi {private val tag = "GoFlutterActivity"private lateinit var binding: ActivityGoFlutterBindingoverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)binding = ActivityGoFlutterBinding.inflate(layoutInflater)setContentView(binding.root)binding.goFlutter.setOnClickListener {//跳转到flutterval initialRoute = listOf( // Map 形式的参数"/","aaaa")BasePlugin.getFlutterMessageApi()?.native2Flutter(initialRoute.joinToString("#"), 1) {Log.e(tag, "flutter2NativeAsync: $it")FlutterLibrary.startFlutterPage(this)}}binding.goFlutter2.setOnClickListener {//跳转到flutterval initialRoute = listOf( // Map 形式的参数"/page2","bbbb")BasePlugin.getFlutterMessageApi()?.native2Flutter(initialRoute.joinToString("#"), 1) {Log.e(tag, "flutter2NativeAsync: $it")FlutterLibrary.startFlutterPage(this)}}}override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {Log.e(tag, "configureFlutterEngine")
// GeneratedPluginRegistrant.registerWith(flutterEngine)}override fun flutter2Native(message: String, type: Long): String {Log.e(tag, "$message$type")return "我是flutter调用原生方法的返回值=》flutter2Native"}override fun flutter2NativeAsync(message: String,type: Long,callback: (Result<String>) -> Unit) {Log.e(tag, "$message$type")callback(Result.success("我是flutter调用原生方法的返回值=》flutter2NativeAsync"))}}
11 源码
gitee
相关文章:
android,flutter 混合开发,pigeon通信,传参
文章目录 app效果native和flutter通信的基础知识1. 编解码器 一致性和完整性,安全性,性能优化2. android代码3. dart代码 1. 创建flutter_module2.修改 Android 项目的 settings.gradle,添加 Flutter module3. 在 Android app 的 build.gradl…...
at32f403a rt thread led基础bsp工程测试
1.led工程官方bsp使用 导出一个独立的AT32F403A的BSP工程 下载RTT源代码 gitee更新较慢 https://gitee.com/rtthread/rt-thread github版本更新最新 https://github.com/RT-Thread/rt-thread. 切换到V5.1.0分支(使用一个发布版本可以避免不必要的bug) 导出一个独立的AT32BSP…...
DeepSeek写贪吃蛇手机小游戏
DeepSeek写贪吃蛇手机小游戏 提问 根据提的要求,让DeepSeek整理的需求,进行提问,内容如下: 请生成一个包含以下功能的可运行移动端贪吃蛇H5文件: 要求 蛇和食物红点要清晰,不超过屏幕外 下方有暂停和重新…...
【好玩的工具和命令】 ASCII 艺术生成工具: figlet
figlet 是一款用于生成 ASCII 艺术文字的工具,支持多种字体样式。它能将输入的文本转换为由字符组成的大型字母图案,广泛应用于命令行环境下的标题展示或装饰。 核心功能 生成 ASCII 文字艺术:将普通文本转化为大号的、由字符构成的艺术字…...
工具--安川伺服故障代码
上传一下安川伺服故障代码,后续结合实际维修经验,逐个整理分析,绝对超出手册经验 故障代码 故障描述 a.020/a.02 用户参数和数检查异常 1 a.021/a.02 参数格式化异常 1 a.022/a.02 系统参数和数检查异常 1 a.023/a.02 参数密码异常…...
车载软件架构 --- OEM主机厂如何打入软件供应商内部?
我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 简单,单纯,喜欢独处,独来独往,不易合同频过着接地气的生活,除了生存温饱问题之外,没有什么过多的欲望,表面看起来很高冷,内心热情,如果你身…...
AI 编程助手 cursor的系统提示词 prompt
# Role 你是一名极其优秀具有10年经验的产品经理和精通java编程语言的架构师。与你交流的用户是不懂代码的初中生,不善于表达产品和代码需求。你的工作对用户来说非常重要,完成后将获得10000美元奖励。 # Goal 你的目标是帮助用户以他容易理解的…...
Matlab写入点云数据到Rosbag
最近有需要读取一个点云并做处理后,重新写回rosbag。网上有很多读取的教程,但没有写入。自己写入时也遇到了很多麻烦,踩了一堆坑进行记录。 1. rosbag中一个lidar的msg有哪些信息? 通过如下代码,先读取一个rosbag的l…...
业务流程相关的权威认证和培训有哪些
业务流程的认证和培训种类繁多,旨在帮助专业人士掌握业务流程管理 (BPM) 的知识和技能,从而提升个人职业发展和组织运营效率。下面分别介绍: 一、 业务流程认证和培训的种类 业务流程的认证和培训可以大致分为以下几类,涵盖了不…...
基于Spring Boot的兴顺物流管理系统设计与实现(LW+源码+讲解)
专注于大学生项目实战开发,讲解,毕业答疑辅导,欢迎高校老师/同行前辈交流合作✌。 技术范围:SpringBoot、Vue、SSM、HLMT、小程序、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容:…...
【算法系列】荷兰国旗问题:三指针法原地排序
一、题目(leetcode75 颜色分类 --三分数组) 二、思路 算法核心:三指针分治策略 该问题被称为“荷兰国旗问题”(Dutch National Flag Problem),由计算机科学家Edsger Dijkstra提出。其核心思想是通过三个指针将数组划分为三个区…...
DeepSeek R1本地+私有云版医疗AI部署开发成功案例技术剖析
1. 引言 1.1 研究背景与意义 随着科技的飞速发展,人工智能(AI)在医疗领域的应用正逐渐成为推动医疗行业变革的重要力量。近年来,医疗 AI 取得了显著的进展,从疾病诊断、药物研发到医疗管理等各个环节,AI 技术都展现出了巨大的潜力。它能够处理和分析海量的医疗数据,为…...
ARM64 Trust Firmware [五]
本章介绍 ATF 中的 Runtime Service 是如何定义和被调用的。 要了解 SMC,必须从 SMC 指令本身开始,其指令如下图: 指令格式为:SMC #<imm>,从官方文档了解到该指令只能在 EL1 以及更高的异常等级上调用ÿ…...
rkipc main.c 中 rk_param_init函数分析
rk_param_init函数 这个函数是用来读取配置文件进行参数配置 这个函数在 luckfox-pico/project/app/rk_smart_door/smart_door/common/uvc/param/param.c 中 这个函数在main函数中被调用 //通过-c 配置文件路径 把配置文件传进来 case c:rkipc_ini_path_ optarg;//调用&am…...
正确清理C盘空间
一.系统清理 正确清理C盘空间主要是删除不需要的文件和应用程序,以释放磁盘空间。以下是一些常用的方法: 删除临时文件:在Windows搜索框中输入“%temp%”,打开临时文件夹,将其中的文件全部删除。 清理回收站…...
http代理IP怎么实现?如何解决代理IP访问不了问题?
HTTP代理是一种网络服务,它充当客户端和目标服务器之间的中介。当客户端发送请求时,请求首先发送到代理服务器,然后由代理服务器转发到目标服务器。同样,目标服务器的响应也会先发送到代理服务器,再由代理服务器返回给…...
【Gin-Web】Bluebell社区项目梳理5:投票功能分析与实现
本文目录 一、投票功能投票流程实现代码redis投票 一、投票功能 投票流程 首先我们要明确,就是 谁(哪个用户:userID) 给 哪个帖子(postID) 投了 什么票(赞成票or反对票)。 赞成票…...
多人协同创作gitea
多人协同创作gitea 在多台设备上协同使用Gitea,主要是通过网络访问Gitea服务器上的仓库来进行代码管理和协作。以下是一些关键步骤和建议,帮助你在多台设备上高效地使用Gitea进行协作: 1. 确保Gitea服务可访问 首先,你需要确保…...
Java 大视界 -- Java 大数据未来十年的技术蓝图与发展愿景(95)
💖亲爱的朋友们,热烈欢迎来到 青云交的博客!能与诸位在此相逢,我倍感荣幸。在这飞速更迭的时代,我们都渴望一方心灵净土,而 我的博客 正是这样温暖的所在。这里为你呈上趣味与实用兼具的知识,也…...
idea连接gitee完整教程
文章目录 idea连接gitee(使用idea远程兼容gitee) 使用idea远程兼容gitee并反向创建仓库和分支...
Flask RESTful 示例
目录 1. 环境准备2. 安装依赖3. 修改main.py4. 运行应用5. API使用示例获取所有任务获取单个任务创建新任务更新任务删除任务 中文乱码问题: 下面创建一个简单的Flask RESTful API示例。首先,我们需要创建环境,安装必要的依赖,然后…...
Lombok 的 @Data 注解失效,未生成 getter/setter 方法引发的HTTP 406 错误
HTTP 状态码 406 (Not Acceptable) 和 500 (Internal Server Error) 是两类完全不同的错误,它们的含义、原因和解决方法都有显著区别。以下是详细对比: 1. HTTP 406 (Not Acceptable) 含义: 客户端请求的内容类型与服务器支持的内容类型不匹…...
Oracle查询表空间大小
1 查询数据库中所有的表空间以及表空间所占空间的大小 SELECTtablespace_name,sum( bytes ) / 1024 / 1024 FROMdba_data_files GROUP BYtablespace_name; 2 Oracle查询表空间大小及每个表所占空间的大小 SELECTtablespace_name,file_id,file_name,round( bytes / ( 1024 …...
多场景 OkHttpClient 管理器 - Android 网络通信解决方案
下面是一个完整的 Android 实现,展示如何创建和管理多个 OkHttpClient 实例,分别用于长连接、普通 HTTP 请求和文件下载场景。 <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas…...
EtherNet/IP转DeviceNet协议网关详解
一,设备主要功能 疆鸿智能JH-DVN-EIP本产品是自主研发的一款EtherNet/IP从站功能的通讯网关。该产品主要功能是连接DeviceNet总线和EtherNet/IP网络,本网关连接到EtherNet/IP总线中做为从站使用,连接到DeviceNet总线中做为从站使用。 在自动…...
爬虫基础学习day2
# 爬虫设计领域 工商:企查查、天眼查短视频:抖音、快手、西瓜 ---> 飞瓜电商:京东、淘宝、聚美优品、亚马逊 ---> 分析店铺经营决策标题、排名航空:抓取所有航空公司价格 ---> 去哪儿自媒体:采集自媒体数据进…...
OPENCV形态学基础之二腐蚀
一.腐蚀的原理 (图1) 数学表达式:dst(x,y) erode(src(x,y)) min(x,y)src(xx,yy) 腐蚀也是图像形态学的基本功能之一,腐蚀跟膨胀属于反向操作,膨胀是把图像图像变大,而腐蚀就是把图像变小。腐蚀后的图像变小变暗淡。 腐蚀…...
安卓基础(aar)
重新设置java21的环境,临时设置 $env:JAVA_HOME "D:\Android Studio\jbr" 查看当前环境变量 JAVA_HOME 的值 echo $env:JAVA_HOME 构建ARR文件 ./gradlew :private-lib:assembleRelease 目录是这样的: MyApp/ ├── app/ …...
JS手写代码篇----使用Promise封装AJAX请求
15、使用Promise封装AJAX请求 promise就有reject和resolve了,就不必写成功和失败的回调函数了 const BASEURL ./手写ajax/test.jsonfunction promiseAjax() {return new Promise((resolve, reject) > {const xhr new XMLHttpRequest();xhr.open("get&quo…...
mac 安装homebrew (nvm 及git)
mac 安装nvm 及git 万恶之源 mac 安装这些东西离不开Xcode。及homebrew 一、先说安装git步骤 通用: 方法一:使用 Homebrew 安装 Git(推荐) 步骤如下:打开终端(Terminal.app) 1.安装 Homebrew…...
