前言

工作中或多或少都会遇到困扰自己很久的问题,我也毫无例外,曾经在项目中对蓝牙打印这一块也困惑和迷茫过,最近在做项目重构的时候,翻看了之前写的代码,还是决定通过两篇文章详细阐述蓝牙连接打印机完成整个打印流程的操作,目的是对工作的一种总结,其次是输出。好比玩个压缩,又是绿叉又是无尽,一身暴击 装,却不知怎么打输出,好像特无语,工作中应该也是一样,学会总结和输出这样才能提升自己,当然也希望能帮助在蓝牙打印方面存在疑惑和困扰的同学,希望在看完这两篇文章后能对经典蓝牙有更多了解和认识。

概述

我将通过上下两部分,详细阐述蓝牙打印功能整体思路以及流程,上部分主要是阐述如何通过协程开启子任务连接蓝牙,以及如何通过系统广播建立心跳包,实现断线重连机制,下部分主要阐述应用和蓝牙打印机间的通信。

基本了解

1,了解经典蓝牙和低功耗蓝牙的区别,太必要了,当初我就走了弯路,如果你对这两种蓝牙有了解,那实在是太好了。
2,协程相关知识,如何建立耗时任务等,如果你不了解协程,你也可以使用RxJava实现,或者直接开启线程建立耗时任务,姿势很多。
3,对Service和Broadcast有一定了解,当然我相信你对这两大组件一定再熟悉不过了。

UML类图

实现过程

  • 1,准备工作
    在我们日常开发中,对整个功能的需求把控设计,应该是首当其冲的必要条件,所以通过类图我们大致定义了两个功能接口,BlueClient和BlueConnector,BlueClient接口大致包含开启蓝牙,关闭,扫描周围蓝牙设备,连接蓝牙,断开蓝牙设备等功能,具体功能细节类图或者代码都做了详细说明,BlueConnector接口主要是用来定义蓝牙设备连接,断开和重连等,BlueClient中的功能定义依赖BlueConnector
  • 2,具体实现
    定义BlueClient接口 主要用来打开蓝牙,连接蓝牙设备等功能
/***author  : Frank*e-mail  : zhuhuitao_struggle@163.com*date    : 2021/1/15 14:40*desc    :*version :*/
interface BlueClient {/*** 根据传入状态 打开和关闭蓝牙** @param state 蓝牙状态*/fun onBlueState(state: Int)/*** 开始扫描蓝牙设备** @param callback 扫描结果回调* @throws NotInitException 回调异常捕获* @throws BlueScanException    蓝牙扫描异常捕获*/@Throws(NotInitException::class, BlueScanException::class)fun onStartScan(callback: ScanCallback)/*** 停止或者终止扫描设备后回调** @throws NotInitException 异常捕获*/@Throws(NotInitException::class)fun onStopScan()/*** 停止或者终止扫描*/fun cancelScan()/*** 发现新设备** @param device 发现的新设备* @throws NotInitException 回调异常捕获*/@Throws(NotInitException::class)fun onFind(device: BluetoothDevice?)/*** 连接一个设备** @param device 所需要连接的设备* @return 返回连接结果*/fun connect(device: BluetoothDevice, callback: BlueConnectCallback)/*** 指定mac地址连接一台蓝牙设备** @param mac             mac地址* @param connectCallback 连接结果回调*/fun connect(mac: String, connectCallback: BlueConnectCallback)/*** 断开一个设备** @param device 所需要断开的设备* @return 返回断开结果*/fun disConnect(device: BluetoothDevice, callback: DisconnectCallback)/*** 设备主动断开后重连,此过程由于蓝牙设备间通信出现异常断开,不存在人为操作*/fun onReconnect()/*** 打开蓝牙*/fun openBlue(callback: BlueSwitchCallback)/*** 关闭蓝牙*/fun closeBlue(callback: BlueSwitchCallback)/*** 获取上下文** @return*/fun getContext(): Context/*** 初始化相关工作*/fun init(context: Context): BlueClient/*** 成功连接了一台蓝牙设备** @param device 连接的蓝牙设备*/fun onConnected(device: BluetoothDevice)/*** 成功断开一台设备** @param device*/fun onDisconnect(device: BluetoothDevice)/*** 蓝牙配对状态** @param device 蓝牙配对状态回调*/fun bondStatus(device: BluetoothDevice)/*** 获取已绑定的蓝牙设备** @return*/fun getBondedDevices(): List<BluetoothDevice?>/*** 是否开启日志功能* @param isDebug 日志开关*/fun enableDebug(isDebug:Boolean)
}

定义BlueClint具体实现类BlueClientImpl

/***author  : Frank*e-mail  : zhuhuitao_struggle@163.com*date    : 2021/1/15 15:12*desc    :*version :*/
class BlueClientImpl : BlueClient {private lateinit var mCtx: Context//蓝牙适配器private var mBlueAdapter: BluetoothAdapter? = null//连接状态回调private var mConnectCallback: BlueConnectCallback? = null//断开状态回调private var mDisconnectCallback: DisconnectCallback? = null//开启关闭蓝牙状态回调private var mBlueSwitchCallback: BlueSwitchCallback? = null//扫描周围蓝牙设备回调private var mScanCallback: ScanCallback? = null//存放周围扫描到的蓝牙设备,已去除重复设备private var mDeviceList: MutableList<BluetoothDevice>? = null//蓝牙广播接收者private var mBlueReceiver: BluetoothReceiver? = nullcompanion object {//标识当前的蓝牙设备是否已连接var connected = false//用于标识蓝牙开关状态var isOPen = false//用于标识是否是人为断开var isArtifical = false//伴生对象val instance = BlueClientImpl()}override fun onBlueState(state: Int) {//通知UI层当前的蓝牙开启状态if (state == BluetoothAdapter.STATE_ON) {//已打开mBlueSwitchCallback?.onStateChange(true)}if (state == BluetoothAdapter.STATE_OFF) {//已关闭mBlueSwitchCallback?.onStateChange(false)//将连接状态置为falseconnected = false}}override fun onStartScan(callback: ScanCallback) {//初始化callback对象mScanCallback = callbackif (mBlueAdapter != null) {//如果正在扫描周围的蓝牙设备,则返回if (mBlueAdapter?.isDiscovering == true) {return}//初始化集合,用于存放周围发现的蓝牙设备if (mDeviceList == null) mDeviceList = arrayListOf()//开始扫描mBlueAdapter?.startDiscovery()//通知ui层正在扫描mScanCallback?.onStartScan(true)}}override fun onStopScan() {//当停止扫描后把当前的设备集合返回到UI层mScanCallback?.onStopScan(mDeviceList!!)}override fun cancelScan() {if (mBlueAdapter != null) {//1,当用户点击ui连接设备时,调用此方法,停止扫描周围设备//2,接收系统扫描的结束的通知,当然自己也可以做一个定时任务主动停止扫描mBlueAdapter?.cancelDiscovery()}}override fun onFind(device: BluetoothDevice?) {//如果接收到的设备为null直接返回不做处理if (device == null) return//如果集合不为null直接遍历集合,if (mDeviceList!!.isNotEmpty()) {mDeviceList?.forEach {//如果发现设备已存在则返回,不在继续加入集合if (it.address == device.address) return}}//回调发现的设备mScanCallback?.onFindDevice(device)//将设备加入集合列表 用于ui展示this.mDeviceList?.add(device)}override fun connect(device: BluetoothDevice, callback: BlueConnectCallback) {//初始化callback对象mConnectCallback = callback//连接一台指定的蓝牙设备BlueConnectorImpl.instant.connect(mBlueAdapter, device, mConnectCallback)}override fun connect(mac: String, connectCallback: BlueConnectCallback) {//初始化callback对象mConnectCallback = connectCallback//指定mac 连接一台蓝牙设备BlueConnectorImpl.instant.connect(mBlueAdapter, mac, mConnectCallback)}override fun disConnect(device: BluetoothDevice, callback: DisconnectCallback) {//初始化callbackmDisconnectCallback = callback//断开蓝牙设备BlueConnectorImpl.instant.disconnect()}override fun onReconnect() {//调用BlueConnector实现类完成重连BlueConnectorImpl.instant.reconnect()}override fun openBlue(callback: BlueSwitchCallback) {//初始化switchCallbackmBlueSwitchCallback = callbackif (!supportBluetooth()) {return}if (mBlueAdapter?.isEnabled == false) {//开启蓝牙mBlueAdapter?.enable()} else {mBlueSwitchCallback?.onStateChange(true)}}override fun closeBlue(callback: BlueSwitchCallback) {//关闭蓝牙,此处一般程序中用不到,若不支持蓝牙,不做任何操作if (!supportBluetooth()) returnif (mBlueAdapter?.isEnabled == true) {//调用蓝牙适配器关闭蓝牙mBlueAdapter?.disable()}}override fun getContext(): Context {return mCtx}override fun init(context: Context): BlueClient {mCtx = context//获取蓝牙适配器if (mBlueAdapter == null) {mBlueAdapter = BluetoothAdapter.getDefaultAdapter()}//初始化蓝牙广播接收 BroadcastReceiverif (mBlueReceiver == null) {mBlueReceiver = BluetoothReceiver(mCtx, this)}return this}override fun onConnected(device: BluetoothDevice) {//将连接状态置为trueconnected = true//f返回当前连接的设备以及当前的socketmConnectCallback?.connectSuccess(BlueConnectorImpl.instant.getBlueSocket(), device)}override fun onDisconnect(device: BluetoothDevice) {//将连接状态置为falseconnected = false//通知连接失败状态,这里断开分为两种状态,// 1认为操作断开,2无人为干预自动断开(此中状态会触发自动重连)mDisconnectCallback?.disConnectSuccess(device)}override fun bondStatus(device: BluetoothDevice) {//返回当前配对蓝牙设备mConnectCallback?.bondStatus(device)}override fun getBondedDevices(): MutableList<BluetoothDevice> {//这里返回配对过的蓝牙设备,这里由于暂未使用此功能,直接返回了空的list集合return arrayListOf()}/*** 检查设备是否支持蓝牙功能*/private fun supportBluetooth(): Boolean {if (mBlueAdapter == null) {//通过判断蓝牙适配器是否为空,如果未空直接返回蓝牙关闭状态falsemBlueSwitchCallback?.onStateChange(false)}return true}
}

定义BlueConector接口,连接指定设备或者mac蓝牙设备,断开蓝牙设备

/***author  : Frank*e-mail  : zhuhuitao_struggle@163.com*date    : 2021/1/15 15:07*desc    :*version :*/
interface BlueConnector {/*** 尝试连接一个指定蓝牙设备** @param device          所要连接的设备* @param connectCallback 连接状态回调* @param adapter         设备蓝牙适配器*/fun connect(adapter: BluetoothAdapter?,device: BluetoothDevice?,connectCallback: BlueConnectCallback?)/*** 尝试连接一个指定mac的蓝牙设备** @param mac             要连接的蓝牙设备mac地址* @param connectCallback 连接状态回调* @param adapter         设备蓝牙适配器*/fun connect(adapter: BluetoothAdapter?,mac: String?,connectCallback: BlueConnectCallback?)/*** 设备重连,只适用于当前已连接的设备*/fun reconnect()/*** 尝试断开当前连接的蓝牙设备*/fun disconnect()/*** 获取当前连接的蓝牙设备socket** @return*/fun getBlueSocket(): BluetoothSocket?}

定义BlueConnector接口实现类BlueConnectorImpl

/***author  : Frank*e-mail  : zhuhuitao_struggle@163.com*date    : 2021/1/15 15:34*desc    :*version :*/
class BlueConnectorImpl : BlueConnector {//蓝牙适配器private var mBlueAdapter: BluetoothAdapter? = null//当前连接的蓝牙设备private var mDevice: BluetoothDevice? = null//socketprivate var mBlueSocket: BluetoothSocket? = null//连接状态回调private var mBlueConnectCallback: BlueConnectCallback? = nullcompanion object {//半生对象val instant = BlueConnectorImpl()val CBT_UUID: UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB")}/*** 指定连接一台蓝牙设备*/override fun connect(adapter: BluetoothAdapter?,device: BluetoothDevice?,connectCallback: BlueConnectCallback?) {if (device != null) {if (mDevice?.address == device.address) {reconnect()return}}//赋值mDevice = deviceif (adapter == null) returnmBlueAdapter = adaptermBlueConnectCallback = connectCallback//声明临时的socket对象var temp: BluetoothSocket? = nulltry {val uuid = CBT_UUID ?: return//获取蓝牙socket对象temp = mDevice?.createRfcommSocketToServiceRecord(uuid)} catch (e: Exception) {mBlueConnectCallback?.connectFailed(e)}if (temp == null) {mBlueConnectCallback?.connectFailed(IOException("Bluetooth connect failed..."))return}mBlueSocket = tempconnect()}/*** 根据指定的mac 连接一台蓝牙设备*/override fun connect(adapter: BluetoothAdapter?,mac: String?,connectCallback: BlueConnectCallback?) {if (adapter == null) returnif (mac.isNullOrEmpty()) returnif (connectCallback == null) return//赋值mBlueAdapter = adaptermBlueConnectCallback = connectCallbackmDevice = mBlueAdapter?.getRemoteDevice(mac) ?: returnvar temp: BluetoothSocket? = nulltry {temp = mDevice?.createInsecureRfcommSocketToServiceRecord(CBT_UUID)} catch (e: Exception) {mBlueConnectCallback?.connectFailed(e)}if (temp == null) {mBlueConnectCallback?.connectFailed(IOException("Bluetooth connect failed..."))return}mBlueSocket = tempconnect()}/*** 重连*/override fun reconnect() {connect()}/*** 断开蓝牙设备*/override fun disconnect() {if (mBlueSocket == null) returnmBlueSocket?.close()mBlueSocket = nullmBlueAdapter = nullmDevice = null}/*** 获取当前连接的蓝牙socket对象*/override fun getBlueSocket(): BluetoothSocket? {return mBlueSocket}private fun connect() {LogUtil.debug("开始连接蓝牙设备...")if (mBlueSocket == null) returntry {//建立子任务,开始连接connect,不阻塞UI线程GlobalScope.launch {runCatching {mBlueSocket?.connect()}.onSuccess {//这里可以回调成功,为了准确还可以以系统广播连接成功为准mBlueConnectCallback?.connectSuccess(mBlueSocket!!, mDevice)LogUtil.debug("蓝牙连接成功....")}.onFailure {//当抛出异常直接回调连接失败mBlueConnectCallback?.connectFailed(it)LogUtil.debug("蓝牙设备连接失败....")}}} catch (e: Exception) {//回调连接失败mBlueConnectCallback?.connectFailed(e)LogUtil.debug("蓝牙设备连接失败...")}}}

定义系统广播接收者BluetoothReceiver extends BroadcastReceiver

/***author  : Frank*e-mail  : zhuhuitao_struggle@163.com*date    : 2021/1/16 9:55*desc    :*version :*/
class BluetoothReceiver(ctx: Context, client: BlueClient) : BroadcastReceiver() {private var mClient: BlueClient = clientinit {val filter = IntentFilter()//蓝牙开关状态filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED)//蓝牙开始扫描状态filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED)//蓝牙扫描结束filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)//蓝牙扫描发现新设备(未配对)filter.addAction(BluetoothDevice.ACTION_FOUND)//蓝牙配对状态改变filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED)//设备建立连接filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED)//设备断开连接filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED)//蓝牙适配器连接状态改变filter.addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED)//BluetoothA2dp连接状态filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED)filter.addAction("android.media.VOLUME_CHANGED_ACTION")ctx.registerReceiver(this, filter)//创建心跳机制 这里为了节省开销,可以在蓝牙设备连接成功后在建立心跳机制,这里为了简单直接在此初始化createHeartBeat(mClient)}override fun onReceive(context: Context?, intent: Intent?) {val action = intent?.action ?: returnwhen (action) {BluetoothAdapter.ACTION_STATE_CHANGED -> {val state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, 0)BlueClientImpl.isOPen = (state == BluetoothAdapter.STATE_ON)if (!BlueClientImpl.isOPen)//当蓝牙设备为开启状态时回调给上层操作对象mClient.onBlueState(state)}BluetoothAdapter.ACTION_DISCOVERY_STARTED -> {//start scan bluetooth device}BluetoothAdapter.ACTION_DISCOVERY_FINISHED -> {//接收到系统停止扫描设备广播mClient.onStopScan()}BluetoothDevice.ACTION_FOUND -> {//接收到系统扫描到周围的蓝牙设备mClient.onFind(intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)?: return)}BluetoothDevice.ACTION_BOND_STATE_CHANGED -> {//蓝牙配对状态回调val device =intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)?: returnmClient.bondStatus(device)}BluetoothDevice.ACTION_ACL_CONNECTED -> {//蓝牙连接状态回到val device =intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)?: returnif (device.bondState == 12) {//如果绑定状态为12 则判断蓝牙连接成功,可以发起回调通知UI层,前面通过协程任务我们已经知道//蓝牙连接是否成功,此处将不再回调,// mClient.onConnected(device)}}BluetoothDevice.ACTION_ACL_DISCONNECTED -> {//蓝牙设备断开val device =intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)?: return//由于我们这里连接的是打印机,所以判断如果不是蓝牙打印机设备,不做处理,这里的设备类型,查看了源码,直接写成了//1536 和 7936  具体类型可以前往系统源码Device查看if (!isPrinter(device.bluetoothClass.majorDeviceClass)) return//如果是人为,不做重连处理if (isArtifical) {//回调蓝牙断开状态mClient.onDisconnect(device)return}mClient.onReconnect()}}}}

使用

  • 初始化BlueClint对象
 BlueClientImpl.instance.init(context).enableDebug(true)
  • 开始操作蓝牙
  BlueClientImpl.instance.onStartScan(object : ScanCallback {override fun onStartScan(isOn: Boolean) {mViewModel.isScanning.set(true)}override fun onStopScan(deviceList: MutableList<BluetoothDevice>) {mViewModel.isScanning.set(false)}override fun onFindDevice(device: BluetoothDevice) {if (device.name == null) returnmSystemList.add(device)mAdapter.addData(BluetoothSimpleDevice(false, device.name, device.address, "89"))}})

注意事项

在扫描周围蓝牙设备的时候一定要开启定位权限,不然无法获取到周围的蓝牙设备(特别注意),6.0以上设备需要动态申请定位权限,manifest清单文件申请蓝牙操作必要的权限:

 <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /><uses-permission android:name="android.permission.BLUETOOTH" /><uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />

总结

通过以上几个重要的步骤,我们基本实现了蓝牙的开启,扫描周围设备,连接一台蓝牙设备,断开蓝牙设备等常用功能,当然其中仍然存在许多优化的细节,大体思路和方向我觉得还是正确和行的通,毕竟在项目中已经使用了此方案,代码我会在写完第二部分上传到github,代码我也将会持续更新,由于我自身技术水平受限,可能由于思考问题不全面,如果觉得写的不好,请勿喷,也欢迎指教和issue。

针对蓝牙相关问题,我已做了相关lib,如果想了解,请查阅Anddroid链接蓝牙打印机

android操作蓝牙打印机(上)相关推荐

  1. Android连接蓝牙打印机

    前言 在之前写过一篇Android操作蓝牙打印机(上),当时也是因为自己在工作中确实走了许多弯路,所以当时计划着用两篇文章详细阐述蓝牙打印的整个流程,也是对工作的一种总结,其次也可以给蓝牙打印方面感觉 ...

  2. android 蓝牙地址连接打印机,android 连接蓝牙打印机 BluetoothAdapter

    android 连接蓝牙打印机 BluetoothAdapter 源码下载地址:https://github.com/yylxy/BluetoothText.git public class Prin ...

  3. Android连接蓝牙打印机实现PDF文档的打印

    目前网上教程与Demo介绍的都是蓝牙连接热敏打印机(pos机大小的打印机),如果想通过蓝牙连接日常所见到的打印机,进行打印,这些教程或Demo是做不到的. 目前Android的蓝牙并不支持BPP(Ba ...

  4. Android蓝牙打印服务,Android 模拟蓝牙打印机

    1: 思路 百度百科的介绍 所谓蓝牙打印机,就是指在主机端用一单片机来仿真打印机进行工作,截取从主机并口传出的数据及控制信号,并通过蓝牙无线连接传送到打印机端.在打印机侧的单片机则根据所收到的蓝牙数据 ...

  5. mui android连接蓝牙打印机打印

    android设备连蓝牙打印机打印,代码如下:  mui.plusReady(function(){             main = plus.android.runtimeMainActivi ...

  6. Android调用蓝牙打印机

    首先需要一个jar包,bluesdk,请自行百度. 具体排版样式跟网络打印机打印排版样式实现一样,这里不多叙述,只贴一个实现方法代码.蓝牙打印机使用前需要先跟手机配对,可以保存在本地,记录下地址,这里 ...

  7. android 蓝牙打印代码,分享一个b4a下安卓操作蓝牙打印机的代码

    b4a的  用到 Serial的库 实测过能用 另外:有人有    zxing_b4a_1.3plus_lib_demo.zip 的库没有? 关键代码 Sub Process_Globals Dim ...

  8. Android 实现蓝牙打印的功能

    第一步:首先需要一个蓝牙打印工具类 import android.bluetooth.BluetoothSocket; import android.graphics.Bitmap; import a ...

  9. Android手机连接蓝牙打印机连接不上的问题

    目前碰到的情况(虽然可能是小情况,但是在解决的时候还是很费时间的): 问题描述:用Android机连接蓝牙打印机,发现华为P7可以连接,而其余的手机都连接失败,找了一上午资料也没解决这个问题. 接着我 ...

最新文章

  1. 【BZOJ 3879】SvT
  2. 平台还是代购?海外贸易之争趋近尾声
  3. 使用游标显示销售报表_协助报表开发之 MongoDB join mysql
  4. maven工程导入项目打开404_Maven依赖配置和依赖范围
  5. Linux操作系统load average过高,kworker占用较多cpu
  6. 利用openpyxl,Python对excel读写文件
  7. 计算机应用基础模块2客观题答案 文档,卓顶精文2019计算机应用基础网上形考答案模块2 Word 2010 文字处理系统客观题答案...
  8. sqlalchemy连接mysql数据库_史上超详细的flask_sqlalchemy连接mysql数据库
  9. Javascript ES6 理解Promise的then()
  10. HDU2999 Stone Game, Why are you always there?【SG函数】
  11. tomcat启动成功 未加载项目_智云CRM项目启动大会在深圳成功召开
  12. python购物车代码_(Python基础)简单购物车代码
  13. Crystal Ball—甲骨文水晶球风险管理软件(概念以及实战——基础案例篇)
  14. 接口测试用例设计思路_最全测试用例设计方法~思路分析
  15. Python运算符优先级和结合性
  16. 关于梯度下降与Momentum通俗易懂的解释
  17. Mysql如何保证原子性,一致性,持久性
  18. GYM CERC 16 K Key Knocking 构造
  19. Windows: Ctrl,Alt, Shift等快捷键的含义
  20. c语言程序设计 超市收银设计,C语言超市收银系统方案

热门文章

  1. 数据挖掘导论可视化部分总结
  2. java惰性计算原理_利用 Lambda 表达式实现 Java 中的惰性求值
  3. 椭圆积分函数和雅各比椭圆函数
  4. 计算机中guest用户是灰的,来宾帐户状态不适用呈灰色状
  5. 5.20爬虫结——Mu
  6. mac 不显示 外接屏幕_mac连接投影仪不显示怎么办-mac外接显示器设置教程 - 河东软件园...
  7. CentOS虚拟机网络连接失败
  8. 火红色枫叶背景《你好秋天》秋分节气 PPT模板
  9. 微信php echo换行,微信小程序文字显示换行问题
  10. mac系统学python_升级mac自带的python,学python拿mac还是win,使用系统自带Pyth