Android蓝牙开发全面总结

基本概念

安卓平台提供对蓝牙的通讯栈的支持,允许设别和其他的设备进行无线传输数据。应用程序层通过安卓API来调用蓝牙的相关功能,这些API使程序无线连接到蓝牙设备,并拥有P2P或者多端无线连接的特性。

蓝牙的功能:

1、扫描其他蓝牙设备

2、为可配对的蓝牙设备查询蓝牙适配器

3、建立RFCOMM通道(其实就是尼玛的认证)

4、通过服务搜索来链接其他的设备

5、与其他的设备进行数据传输

6、管理多个连接

蓝牙建立连接必须要求:

1、打开蓝牙

2、查找附近已配对或可用设备

3、连接设备

4、设备间数据交互

首先,要操作蓝牙,先要在AndroidManifest.xml里加入权限

1
2
< uses-permissionandroid:name = "android.permission.BLUETOOTH_ADMIN"  />
< uses-permissionandroid:name = "android.permission.BLUETOOTH"  />

Android所有关于蓝牙开发的类都在android.bluetooth包下,只有8个类

1.BluetoothAdapter 蓝牙适配器

直到我们建立bluetoothSocket连接之前,都要不断操作它BluetoothAdapter里的方法很多,常用的有以下几个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
cancelDiscovery() 根据字面意思,是取消发现,也就是说当我们正在搜索设备的时候调用这个方法将不再继续搜索
disable()关闭蓝牙
enable()打开蓝牙,这个方法打开蓝牙不会弹出提示,更多的时候我们需要问下用户是否打开,一下这两行代码同样是打开蓝牙,不过会提示用户:
Intemtenabler= new  Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enabler,reCode); //同startActivity(enabler);
getAddress()获取本地蓝牙地址
getDefaultAdapter()获取默认BluetoothAdapter,实际上,也只有这一种方法获取BluetoothAdapter
getName()获取本地蓝牙名称
getRemoteDevice(String address)根据蓝牙地址获取远程蓝牙设备
getState()获取本地蓝牙适配器当前状态(感觉可能调试的时候更需要)
isDiscovering()判断当前是否正在查找设备,是返回 true
isEnabled()判断蓝牙是否打开,已打开返回 true ,否则,返回 false
listenUsingRfcommWithServiceRecord(String name,UUID uuid)根据名称,UUID创建并返回BluetoothServerSocket,这是创建BluetoothSocket服务器端的*步
startDiscovery()开始搜索,这是搜索的*步
使用BluetoothAdapter的startDiscovery()方法来搜索蓝牙设备
startDiscovery()方法是一个异步方法,调用后会立即返回。该方法会进行对其他蓝牙设备的搜索,该过程会持续 12 秒。该方法调用后,搜索过程实际上是在一个System Service中进行的,所以可以调用cancelDiscovery()方法来停止搜索(该方法可以在未执行discovery请求时调用)。
请求Discovery后,系统开始搜索蓝牙设备,在这个过程中,系统会发送以下三个广播:
ACTION_DISCOVERY_START:开始搜索
ACTION_DISCOVERY_FINISHED:搜索结束
ACTION_FOUND:找到设备,这个Intent中包含两个extra fields:EXTRA_DEVICE和EXTRA_CLASS,分别包含BluetooDevice和BluetoothClass。
我们可以自己注册相应的BroadcastReceiver来接收响应的广播,以便实现某些功能

2.BluetoothDevice 描述了一个蓝牙设备

1
2
3
4
createRfcommSocketToServiceRecord(UUIDuuid)根据UUID创建并返回一个BluetoothSocket
getState() 蓝牙状态这里要说一下,只有在 BluetoothAdapter.STATE_ON 状态下才可以监听,具体可以看andrid api;
这个方法也是我们获取BluetoothDevice的目的――创建BluetoothSocket
这个类其他的方法,如getAddress(),getName(),同BluetoothAdapter

3.BluetoothServerSocket

这个类一种只有三个方法两个重载的accept(),accept(inttimeout)两者的区别在于后面的方法指定了过时时间,需要注意的是,执行这两个方法的时候,直到接收到了客户端的请求(或是过期之后),都会阻塞线程,应该放在新线程里运行!

还有一点需要注意的是,这两个方法都返回一个BluetoothSocket,*后的连接也是服务器端与客户端的两个BluetoothSocket的连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void    close()
Closes the object and release any system resources it holds.
void    connect()
Attempt to connect to a remote device.
InputStream getInputStream()
Get the input stream associated with  this  socket.
OutputStream    getOutputStream()
Get the output stream associated with  this  socket.
BluetoothDevice getRemoteDevice()
Get the remote device  this  socket is connecting, or connected, to.
获取远程设备,该套接字连接,或连接到---。
boolean isConnected()
Get the connection status of  this  socket, ie, whether there is an active connection with remote device.
判断当前的连接状态

4.BluetoothSocket

跟BluetoothServerSocket相对,是客户端一共5个方法,不出意外,都会用到

1
2
3
4
5
close(),关闭
connect()连接
getInptuStream()获取输入流
getOutputStream()获取输出流
getRemoteDevice()获取远程设备,这里指的是获取bluetoothSocket指定连接的那个远程蓝牙设备

5、BluetoothClass

代表一个描述了设备通用特性和功能的蓝牙类。比如,一个蓝牙类会指定皆如电话、计算机或耳机的通用设备类型,可以提供皆如音频或者电话的服务。

每个蓝牙类都是有0个或更多的服务类,以及一个设备类组成。设备类将被分解成主要和较小的设备类部分。

BluetoothClass 用作一个能粗略描述一个设备(比如关闭用户界面上一个图标的设备)的线索,但当蓝牙服务事实上是被一个设备所支撑的时候,BluetoothClass的 介绍则不那么可信任。精确的服务搜寻通过SDP请求来完成。当运用createRfcommSocketToServiceRecord(UUID) 和listenUsingRfcommWithServiceRecord(String, UUID)来创建RFCOMM端口的时候,SDP请求就会自动执行。

使用getBluetoothClass()方法来获取为远程设备所提供的类。

两个内部类:

class   BluetoothClass.Device

定义所有设备类的常量

class   BluetoothClass.Service

定义所有服务类的常量

公共方法:

public int describeContents ()

描述包含在可封装编组的表示中所有特殊对象的种类。

返回值 

一个指示被Parcelabel所排列的特殊对象类型集合的位掩码。

public boolean equals (Object o)

比较带有特定目标的常量。如果他们相等则标示出来。 为了保证其相等,o必须代表相同的对象,该对象作为这个使用类依赖比较的常量。通常约定,该比较既要可移植又需灵活。

当且仅当o是一个作为接收器(使用==操作符来做比较)的精确相同的对象是,这个对象的实现才返回true值。子类通常实现equals(Object)方法,这样它才会重视这两个对象的类型和状态。

通常约定,对于equals(Object)和hashCode() 方法,如果equals对于任意两个对象返回真值,那么hashCode()必须对这些对象返回相同的纸。这意味着对象的子类通常都覆盖或者都不覆盖这两个方法。

参数

o   需要对比常量的对象

返回值

如果特定的对象和该对象相等则返回true,否则返回false。

public int getDeviceClass ()

返回BluetoothClass中的设备类部分(主要的和较小的)

从函数中返回的值可以和在BluetoothClass.Device中的公共常量做比较,从而确定哪个设备类在这个蓝牙类中是被编码的。

返回值

设备类部分

public int getMajorDeviceClass ()

返回BluetoothClass中设备类的主要部分

从函数中返回的值可以和在BluetoothClass.Device.Major中的公共常量做比较,从而确定哪个主要类在这个蓝牙类中是被编码的。

返回值

主要设备类部分

public boolean hasService (int service)

如果该指定服务类被BluetoothClass所支持,则返回true

在BluetoothClass.Service中,合法的服务类是公共常量,比如AUDIO类。

参数

service 合法服务类

返回值

如果该服务类可被支持,则返回true

public int hashCode ()

返回这个对象的整型哈希码。按约定,任意两个在equals(Object)中返回true的对象必须返回相同的哈希码。这意味着对象的子类通常通常覆盖或者都不覆盖这两个方法。

注意:除非同等对比信息发生改变,否则哈希码不随时间改变而改变。

public String toString ()  

返回这个对象的字符串,该字符串包含精确且可读的介绍。系统鼓励子类去重写该方法,并且提供了能对该对象的类型和数据进行重视的实现方法。默认的实现方法只是简单地把类名、“@“符号和该对象hashCode()方法的16进制数连接起来(如下列所示的表达式):

1
2
3
4
5
public  void  writeToParcel (Parcel out,  int  flags)
将类的数据写入外部提供的Parcel中
参数
out     对象需要被写入的Parcel
flags  和对象需要如何被写入有关的附加标志。可能是 0 ,或者可能是PARCELABLE_WRITE_RETURN_VALUE。

以上是蓝牙主要操作的类。

基本用法:

1、获取本地蓝牙适配器

1
BluetoothAdapter mAdapter= BluetoothAdapter.getDefaultAdapter();

2、打开蓝牙

1
2
3
4
5
6
7
if (!mAdapter.isEnabled()){
//弹出对话框提示用户是后打开
Intent enabler =  new  Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enabler, REQUEST_ENABLE);
//不做提示,强行打开,此方法需要权限&lt;uses-permissionandroid:name="android.permission.BLUETOOTH_ADMIN" /&gt;
// mAdapter.enable();
}

3、搜索设备

1)mAdapter.startDiscovery() 是*步,可能你会发现没有返回的蓝牙设备

2)定义BroadcastReceiver,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 创建一个接收ACTION_FOUND广播的BroadcastReceiver
private  final  BroadcastReceiver mReceiver =  new  BroadcastReceiver() {
      public  void  onReceive(Context context, Intent intent) {
           String action = intent.getAction();
           // 发现设备
           if  (BluetoothDevice.ACTION_FOUND.equals(action)) {
                // 从Intent中获取设备对象
                BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
                // 将设备名称和地址放入array adapter,以便在ListView中显示
                mArrayAdapter.add(device.getName() +  "/n"  + device.getAddress());
           } else  if  (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) {
                if  (mNewDevicesAdapter.getCount() ==  0 ) {
                     Log.v(TAG,  "find over" );
                }
          }
     };
// 注册BroadcastReceiver
IntentFilter filter =  new  IntentFilter(BluetoothDevice.ACTION_FOUND);
registerReceiver(mReceiver, filter);  // 不要忘了之后解除绑定

4、建立连接

首先Android sdk(2.0以上版本)支持的蓝牙连接是通过BluetoothSocket建立连接,服务器端(BluetoothServerSocket)和客户端(BluetoothSocket)需指定同样的UUID,才能建立连接,因为建立连接的方法会阻塞线程,所以服务器端和客户端都应启动新线程连接
1)服务器端:

1
2
3
4
//UUID格式一般是"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"可到
//http://www.uuidgenerator.com 申请
BluetoothServerSocket serverSocket = mAdapter. listenUsingRfcommWithServiceRecord(serverSocketName,UUID);
serverSocket.accept();

2)客户端:

1
2
3
//通过BroadcastReceiver获取了BLuetoothDevice
BluetoothSocket clienSocket=dcvice. createRfcommSocketToServiceRecord(UUID);
clienSocket.connect();

5、数据传递

通过以上操作,就已经建立的BluetoothSocket连接了,数据传递是通过流

1)获取流

1
2
inputStream = socket.getInputStream();
     outputStream = socket.getOutputStream();

2)写出、读入(JAVA常规操作)

补充一下,使设备能够被搜索

1
2
Intent enabler =  new  Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
     startActivityForResult(enabler,REQUEST_DISCOVERABLE);

关于蓝牙连接:

可以直接用以下代码进行连接:

1
2
3
4
5
6
final  String SPP_UUID =  "00001101-0000-1000-8000-00805F9B34FB" ;
UUID uuid = UUID.fromString(SPP_UUID);
BluetoothSocket socket;
socket = device.createInsecureRfcommSocketToServiceRecord(uuid);
adapter.cancelDiscovery();
socket.connect();

1.startDiscovey有可能启动失败

一般程序中会有两步:开启蓝牙、开始寻找设备。顺序执行了开启蓝牙-寻找设备的步骤,但是由于蓝牙还没有完全打开,就开始寻找设备,导致寻找失败。

解决办法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
adapter = BluetoothAdapter.getDefaultAdapter();
if  (adapter ==  null )      {
// 设备不支持蓝牙
}
// 打开蓝牙
if  (!adapter.isEnabled()){
     adapter.enable();
     adapter.cancelDiscovery();
}
// 寻找蓝牙设备,android会将查找到的设备以广播形式发出去
while  (!adapter.startDiscovery()){
     Log.e( "BlueTooth"  "尝试失败" );
     try  {
         Thread.sleep( 100 );
      catch  (InterruptedException e) {
         e.printStackTrace();
     }
}

2.接收数据转换

使用socket.getInputStream接收到的数据是字节流,这样的数据是没法分析的,所以很多情况需要一个byte转十六进制String的函数:

1
2
3
4
5
6
7
8
9
public  static  String bytesToHex( byte [] bytes) {
     char [] hexChars =  new  char [bytes.length *  2 ];
     for   int  j =  0 ; j &lt; bytes.length; j++ ) {
         int  v = bytes[j] &amp;  0xFF ;
         hexChars[j *  2 ] = hexArray[v &gt;&gt;&gt;  4 ];
         hexChars[j *  2   1 ] = hexArray[v &amp;  0x0F ];
     }
     return  new  String(hexChars);
}

IOS APP 架构设计(一)

IOS APP 架构设计
一,APP架构概述
1. 应用架构
2.Model 和 View
3.App 的本质是反馈回路
4.架构技术
5.App 任务
6.IOS 架构的5中模式:
二,APP设计常用的5种模式概览
1. Model-View-Controller
2. Model-View-ViewModel+协调器
3. Model-View-Controller+ViewState
4. Model 适配器-View 绑定器 (MAVB)
5. Elm 架构 (TEA)
三,其他APP架构模式
1. Model-View-Presenter
2. VIPER,Riblets,和其他 “Clean” 架构
3. 基于组件的架构 (React Native)
一,APP架构概述
1. 应用架构
App 架构是软件设计的一个分支,它关心如何设计一个 app 的结构。具体来说,它关注于两个 方面:如何将 app 分解为不同的接口和概念层次部件,以及这些部件之间和自身的不同操作中 所使用的控制流和数据流路径。
我们通常使用简单的框图来解释 app 的架构。比如,Apple 的 MVC 模式可以通过 model、 view 和 controller 三层结构来描述。

%title插图%num
上面框图中的模块展示了这个模式中不同名字的三个层次。在一个 MVC 项目中,*大部分的代 码都会落到其中某个层上。箭头表示了这些层进行连接的方式。
但是,这种简单的框图几乎无法解释在实践中模式的操作方式。这是因为在实际的 app 架构中, 部件的构建有非常多的可能性。事件流在层中穿梭的方式是什么?部件之间是否应该在编译期 间或者运行时持有对方?要怎么读取和修改不同部件中的数据?以及状态的变更应该以哪条路 径在 app 中穿行?

2.Model 和 View
在*高的层级上,app 架构其实就是一套分类,app 中不同的部件会被归纳到某个类型中去。 在本书中,我们将这些不同的种类叫做层次:一个层次指的是,遵循一些基本规则并负责特定 功能的接口和其他代码的集合。
Model 层和 View 层是这些分类中*为常⻅的两个。
Model 层是 app 的内容,它不依赖于 (像 UIKit 那样的) 任何 app 框架。也就是说,程序员对 model 层有完全的控制。Model 层通常包括 model 对象 (在录音 app 中的例子是文件夹和录音对象) 和协调对象 (比如我们的 app 例子中的负责在磁盘上存储数据的 Store 类型)。被存储在 磁盘上的那部分 model 我们称之为文档 model (documentation model)。
View 层是依赖于 app 框架的部分,它使 model 层可⻅,并允许用戶进行交互,从而将 model 层转变为一个 app。当创建 iOS 应用时,view 层几乎总是直接使用 UIKit。不过,我们也会看 到在有些架构中,会使用 UIKit 的封装来实现不同的 view 层。另外,对一些其他的像是游戏这 样的自定义应用,view 层可以不是 UIKit 或者 AppKit,它可能是 SceneKit 或者 OpenGL 的某 种封装。
有时候,我们选择使用结构体或者枚举来表示 model 或者 view 的实例,而不使用类的对象。 在实践中,类型之间的区别非常重要,但是当我们在 model 层中谈到对象、结构体和枚举时, 我们会将三者统一地称为 model 对象。类似地,我们也会把 view 层的实例叫做 view 对象,实 际上它们也可能是对象、结构体或者枚举。
View 对象通常会构成一个单一的 view 层级,在这个层级中,所有的 view 对象通过树结构的方 式连接起来。在树的根部是整个屏幕,屏幕中存在若干窗口,接下来在树的分支和叶子上是更 多的小 view。类似地,view controller 也通常会形成 view controller 层级。不过,model 对 象却不需要有明确的层级关系,在程序中它们可以是互不关联的独立 model。
当我们提到 view 时,通常指的是像一个按钮或者一个文本 label 这样的单一 view 对象。当我 们提到 model 时,我们通常指的也是像一个 Recording 实例或者 Folder 实例这样的单个 model 对象。在该话题的大多数文献中,“model” 在不同上下文中指的可能是不同的事情。它 可以指代一个 model 层,model 层中的具体的若干对象,文档 model,或者是 model 层中不 关联的文档。虽然可能会显得啰嗦,我们还是会尝试在本书中尽量明确地区分这些不同含义。

为什么 Model 和 View 的分类会被认为是基础中的基础

当然啦,就算不区分 model 层和 view 层,写出一个 app 也是*对可能的。比如说,在一个简 单的对话框中,通常就没有独立的 model 数据。在用戶点击 OK 按钮的时候,我们可以直接从 用戶界面元素中读取状态。不过通常情况下,model 层的缺失,会让程序的行为缺乏对于清晰 规则的依据,这会使得代码难以维护。
定义一个 model 层的*重要的理由是,它为我们的程序提供一个表述事实的单一来源,这会让 逻辑清晰,行为正确。这样一来,我们的程序便不会被应用框架中的实现细节所支配。
应用框架为我们提供了构建 app 所需要的基础设施。在本书中,我们使用 Cocoa – 或者更精确 说,根据目标平台,使用 UIKit,AppKit 或者 WatchKit – 来作为应用框架。

如果 model 层能做到和应用框架分离,我们就可以完全在 app 的范围之外使用它。我们可以很 容易地在另外的测试套件中运行它,或者用一个完全不同的应用框架重写新的 view 层。这个 model 层将能够用于 Android,macOS 或者 Windows 版本的 app 中。

3.App 的本质是反馈回路
View 层和 model 层需要交流。所以,两者之间需要存在连接。假设 view 层和 model 层是被清
晰地分开,而且不存在无法解耦的联结的话,两者之间的通讯就需要一些形式的翻译:

%title插图%num
从根本上说,用戶界面是一个同时负责展示和输入功能的反馈设备,所以毫无疑问,这导致的 结果就是一个反馈回路。每个 app 设计模式所面临的挑战是如何处理这张图表中箭头所包含的 交流,依赖和变换。
在 model 层和 view 层之间不同的路径拥有不同的名字。用戶发起的事件会导致 view 的响应, 我们把由此引起的代码路径称为 view action,像是点击按钮或者选中 table view 中的某一行 就属于 view action。当一个 view action 被送到 model 层时,它会被转变为 model action (或 者说,让 model 对象执行一个 action 或者进行更新的命令)。这种命令也被叫做一个消息 (特别 在当 model 是被 reducer 改变时,我们会这么称呼它)。将 view action 转变为 model action 的操作,以及路径上的其他逻辑被叫做交互逻辑。

一个或者多个 model 对象上状态的改变叫做 model 变更。Model 的变更通常会触发一个
model 通知,比如说从 model 层发出一个可观测的通知,它描述 model 层中什么内容发生了 改变。当 view 依赖于 model 数据时,通知会触发一个 view 变更,来更改 view 层中的内容。 这些通知可以以多种形式存在:Foundation 中的 Noti?cation,代理,回调,或者是其他机制, 都是可以的。将 model 通知和数据转变为 view 更改的操作,以及路径上的其他逻辑被叫做表 现逻辑。
根据 app 模式的不同,有些状态可能是在文档 model 之外进行维护的,这样一来,更新这些状 态的行为就不会追随文档 model 的路径。在很多模式中的导航状态就这种行为的一个常⻅例 子,在 view 层级中的某个部分 (或者按照 Cocoa Storyboard 中使用的术语,将它称为 scene) 可能会被换出或者换入层级中。
在 app 中非文档 model 的状态被叫做 view state。在 Cocoa 里,大部分 view 对象都管理着它 们自己的 view state,controller 对象则管理剩余的 view state。在 Cocoa view state 的框图 中,通常会有加在反馈回路上的捷径,或者单个层自身进行循环。在有一些架构中,view state 不属于 controller 层,而是属于 model 层的部分 (不过,根据定义,view controller 并不是文 档 model 的一部分)。
当所有的状态都在 model 层中被维护,而且所有的变更都通过完整的反馈回路路径进行传递 时,我们就将它称为单向数据流。当任意的 view 对象或者中间层对象只能够通过 model 发出 的通知来进行创建和更新 (换句话说,view 或者中间层不能通过捷径来更新自身或者其他的 view) 时,这个模式通常就是单向的。

4.架构技术
Apple 平台的标准 Cocoa 框架提供了一些架构工具。Noti?cation 将值从单一源广播给若干个 收听者。键值观察 (KVO) 可以将某个对象上属性的改变报告给另一个对象。然而,Cocoa 中的 架构工具十分有限,我们将会使用到一些额外的框架。
本书中使用到的第三方技术中包含了响应式编程。响应式编程也是一种用来交流变更的工具, 不过和通知或者 KVO 不同的是,它专注于在源和目标之间进行变形,让逻辑可以在部件之间传 输信息的同时得以表达。
我们可以使用像是响应式编程或者 KVO 这样的技术创建属性绑定。绑定接受一个源和一个目 标,无论何时,只要源发生了变化,目标也将被更新。这和手动进行观察在语法上有着不同, 我们不再需要写观察的逻辑,而只需要指定源和目标,接下来框架将会为我们处理其余部分的 工作。

macOS 上的 Cocoa 包含有 Cocoa 绑定技术,它是一种双向绑定,所有的可观察对象同时也是 观察者,在一个方向上建立绑定连接,会在反方向也创建一个连接。不论是 (在MVVM-C 的章
节中用到的) RxCocoa,还是 (MAVB 章节 中用到的) CwlViews,都不是双向绑定的。所以,在
本书中,所有关于绑定的讨论都只涉及到单向绑定。

5.App 任务
要让程序正常工作,view 必须依赖于 model 数据来生成和存在,我们配置 view,让它可以对
model 进行更改,并且能在 model 更新时也得到更新。 所以,我们需要决定在 app 中如何执行下列任务:

构建—谁负责构建model和view,以及将两者连接起来?
更新model—如何处理viewaction?
改变view—如何将model的数据应用到view上去?
viewstate—如何处理导航和其他一些modelstate以外的状态?
测试—为了达到一定程度的测试覆盖,要采取怎样的测试策略?
对于上面五个问题的回答,是构成 app 设计模式的基础要件。在本书中,我们会逐一研究这些 设计模式。

6.IOS 架构的5中模式:
IOS 架构的5中模式:

标准的CocoaModel-View-Controller(MVC)是Apple在示例项目中所采用的设计模 式。它是 Cocoa app 中*为常⻅的架构,同时也是在 Cocoa 中讨论架构时所采用的基 准线。
Model-View-ViewModel+协调器(MVVM-C)是MVC的变种,它拥有单独的 “view-model” (视图模型) 和一个用来管理 view controller 的协调器。MVVM 使用数据 绑定 (通常会和响应式编程一起使用) 来建立 view-model 层和 view 层之间的连接。
Model-View-Controller+ViewState(MVC+VS)这种模式将所有的viewstate集中到 一个地方,而不是让它们散落在 view 和 view controller 中。这和 model 层所遵循的规 则相同。
Model适配器-View绑定器(ModelAdapter-ViewBinder,MAVB)是本书的一位作者所 使用的实验性质的架构。MAVB 专注于构建声明式的 view,并且抛弃 controller,采用 绑定的方式来在 model 和 view 之间进行通讯。
Elm架构(TEA)与MVC或者MVVM这样的常⻅架构完全背道而驰。它使用虚拟view 层级来构建 view,并使用 reducer 来在 model 和 view 之间进行交互。
二,APP设计常用的5种模式概览
1. Model-View-Controller
在 Cocoa MVC 中,一小部分 controller 对象负责处理 model 或者 view 层范畴之外的所有任 务。
这意味着,controller 层接收所有的 view action,处理所有的交互逻辑,发送所有的 model action,接收所有的 model 通知,对所有用来展示的数据进行准备,*后再将它们应用到 view 的变更上去。如果我们去看一下介绍一章中的 app 反馈回路的图,会发现在 model 和 view 之
间的箭头上,几乎每个标签都是 controller。而且要知道,在这幅图中,构建和导航任务并没有 标注出来,它们也会由 controller 来处理。
下面是 MVC 模式的框图,它展示了一个 MVC app 的主要通讯路径:

%title插图%num
图中的虚线部分代表运行时的引用,view 层和 model 层都不会直接在代码中引用 controller。 实线部分代表编译期间的引用,controller 实例知道自己所连接的 view 和 model 对象的接口。
如果我们在这个图标外部描上边界的话,就得到了一个 MVC 版本的 app 反馈回路。注意在图 表中其他的路径并不参与整个反馈回路的路径 (也就是 view 层和 controller 层上那些指向自身 的箭头)。

构建
App 对象负责创建*顶层的 view controller,这个 view controller 将加载 view,并且知道应 该从 model 中获取哪些数据,然后把它们显示出来。Controller 要么显式地创建和持有 model 层,要么通过一个延迟创建的 model 单例来获取 model。在多文档配置中,model 层由更低层 的像是 UIDocument 或 NSDocument 所拥有。那些和 view 相关的单个 model 对象,通常会 被 controller 所引用并缓存下来。
更改 Model
在 MVC 中,controller 主要通过 target/action 机制和 (由 storyboard 或者代码进行设置的) delegate 来接收 view 事件。Controller 知道自己所连接的 view,但是 view 在编译期间却没有 关于 controller 接口的信息。当一个 view 事件到达时,controller 有能力改变自身的内部状态, 更改 model,或者直接改变 view 层级。
更改 View
在我们所理解的 MVC 中,当一个更改 model 的 view action 发生时,controller 不应该直接去 操作 view 层级。正确的做法是,controller 去订阅 model 通知,并且在当通知到达时再更改 view 层级。这样一来,数据流就可以单向进行:view action 被转变为 model 变更,然后 model 发送通知,这个通知*后被转为 view 变更。
View State
View state 可以按需要被 store 在 view 或者 controller 的属性中。相对于影响 model 的 view action,那些只影响 view 或 controller 状态的 action 则不需要通过 model 进行传递。对于 view state 的存储,可以结合使用 storyboard 和 UIStateRestoring 来进行实现,storyboard 负责记录活跃的 controller 层级,而 UIStateRestoring 负责从 controller 和 view 中读取数据。
测试
在 MVC 中,view controller 与 app 的其他部件紧密相连。边界的缺失使得为 controller 编写 单元测试和接口测试十分困难,集成测试是余下的为数不多的可行测试手段之一。在集成测试 中,我们构建相连接的 view、model 和 controller 层,然后操作 model 或者 view,来测试是 否能得到我们想要的结果。
集成测试的书写非常复杂,而且它涵盖的范围太广了。它不仅仅测试逻辑,也测试部件是如何 连接的 (虽然在一些情况下和 UI 测试的⻆度并不相同)。不过,在 MVC 中通过集成测试,通常 达到 80% 左右的测试覆盖率是有可能的。
MVC 的重要性
因为 Apple 在所有的实例项目中都使用了这种模式,加上 Cocoa 本身就是针对这种模式设计 的,所以 Cocoa MVC 成为了 iOS,macOS,tvOS 和 watchOS 上官方认证的 app 架构模式。

历史
MVC 这个名字*次被提出是在 1979 年,Trygve Reenskaug 用它来描述 Smalltalk-76 上已 经存在的 “template pattern” 应用。在他和 Adele Goldberg 讨论了术语方面的问题后,MVC 的名字被确定下来 (之前的名字包括 Model-View-Editor 和 Model-View-Tool-Editor 等)。
在原本的构想中,view 是直接 “附着” 在 model 层上,并观察所有 model 变化的。Controller 存在的目的仅仅是捕捉用戶事件,并把它们转发给 model。这两个特性都是 Smalltalk 运行方 式的产物,它们并不是为了现代的 app 框架所设计的,所以今天这种 MVC 的原始构想已经几 乎*迹了。
Cocoa 中的 MVC 实现可以追溯到大约 1997 年的 NeXTSTEP 4 的年代。在那之前,所有现在 controller 所担当的⻆色,通常都由一个 (像是 NSWindow 那样的) 高层 view 类来扮演。之后, 从原始的 Smalltalk 的 MVC 实现中所发展出的理念是分离展示部分,也就是 view 层和 model 层应该被完全隔离开,这带来了一个强烈的需求,那就是要引入一个支持对象来辅助两者之间 的通讯。NeXTSTEP 中 controller 的概念和 Taligent 稍早的 Model-View-Presenter 中的 presenter (展示器) 很相似。不过,在现在 Model-View-Presenter 这个名字通常被用来指代那 些通过协议从 controller 中将 view 抽象出来的类似 MVC 的模式。

2. Model-View-ViewModel+协调器
MVVM 和 MVC 类似,也是通过基于场景 (scene,view 层级中可能会在导航发生改变时切入或者换出的子树) 进行的架构。相较于 MVC,MVVM 在每个场景中使用 view-model 来描述场景中的表现逻辑和交互逻辑。

View-model 在编译期间不包含对 view 或者 controller 的引用。它暴露出一系列属性,用来描 述每个 view 在显示时应有的值。把一系列变换运用到底层的 model 对象后,就能得到这些* 终可以直接设置到 view 上的值。实际将这些值设置到 view 上的工作,则由预先建立的绑定来 完成,绑定会保证当这些显示值发生变化时,把它设定到对应的 view 上去。响应式编程是用来 表达这类声明和变换关系的很好的工具,所以它天生就适合 (虽说不是严格必要) 被用来处理
view-model。在很多时候,整个 view-model 都可以用响应式编程绑定的方式,以声明式的形 式进行表达。

在理论上,因为 view-model 不包含对 view 层的引用,所以它是独立于 app 框架的,这让对于 view-model 的测试也可以独立于 app 框架。
由于 view-model 是和场景耦合的,我们还需要一个能够在场景间切换时提供逻辑的对象。在 MVVM-C 中,这个对象叫做协调器 (coordinator)。协调器持有对 model 层的引用,并且了解 view controller 树的结构,这样,它能够为每个场景的 view-model 提供所需要的 model 对象。
和 MVC 不同,MVVM-C 中的 view controller 从来都不会直接引用其他的 view controller (所 以,也不会引用其他的 view-model)。View controller 通过 delegate 的机制,将 view action 的信息告诉协调器。协调器据此显示新的 view controller 并设置它们的 model 数据。换句话 说,view controller 的层级是由协调器进行管理的,而不是由 view controller 来决定的。

这些特性所形成的架构的总体结构如下图所示:

%title插图%num

如果我们忽略掉协调器,那么这张图表就很像 MVC 了,只不过在 view controller 和 model 之 间加入了一个阶段。MVVM 将之前在 view controller 中的大部分工作转移到了 view-model 中,但是要注意,view-model 并不会在编译时拥有对 view controller 的引用。
View-model 可以从 view controller 和 view 中独立出来,也可以被单独测试。同样,view controller 也不再拥有内部的 view state,这些状态也被移动到了 view-model 中。在 MVC 中 view controller 的双重⻆色 (既作为 view 层级的一部分,又负责协调 view 和 model 之间的交 互),减少到了单一⻆色 (view controller 仅仅只是 view 层级的一部分)。
协调器模式的加入进一步减少了 view controller 所负责的部分:现在它不需要关心如何展示其 他的 view controller 了。因此,这实际上是以添加了一层 controller 接口为代价,降低了 view controller 之间的耦合。

构建
对于 model 的创建和 MVC 中的保持不变,通常它是一个顶层 controller 的职责。不过,单独
的 model 对象现在属于 view-model,而不属于 view controller。
初始的 view 层级的创建和 MVC 中的一样,通过 storyboard 或者代码来完成。和 MVC 不同的 是,view controller 不再直接为每个 view 获取和准备数据,它会把这项工作交给 view-model。 View controller 在创建的时候会一并创建 view-model,并且将每个 view 绑定到 view-model 所暴露出的相应属性上去。
更改 Model
在 MVVM 中,view controller 接收 view 事件的方式和 MVC 中一样 (在 view 和 view controller 之间建立连接的方式也相同)。不过,当一个 view 事件到达时,view controller 不会 去改变自身的内部状态、view state、或者是 model。相对地,它立即调用 view-model 上的方 法,再由 view-model 改变内部状态或者 model。
更改 View
和 MVC 不同,view controller 不监听 model。View-model 将负责观察 model,并将 model 的通知转变为 view controller 可以理解的形式。View controller 订阅 view-model 的变更,这 通常通过一个响应式编程框架来完成,但也可以使用任意其他的观察机制。当一个 view-model 事件来到时,由 view controller 去更改 view 层级。
为了实现单向数据流,view-model 总是应该将变更 model 的 view action 发送给 model,并 且仅仅在 model 变化实际发生之后再通知相关的观察者。
View State
View state 要么存在于 view 自身之中,要么存在于 view-model 里。和 MVC 不同,view controller 中不存在任何 view state。View-model 中的 view state 的变更,会被 controller 观 察到,不过 controller 无法区分 model 的通知和 view state 变更的通知。当使用协调器时, view controller 层级将由协调器进行管理。
测试
因为 view-model 和 view 层与 controller 层是解耦合的,所以可以使用接口测试来测试 view-model,而不需要像 MVC 里那样使用集成测试。接口测试要比集成测试简单得多,因为 不需要为它们建立完整的组件层次结构。
为了让接口测试尽可能覆盖更多的范围,view controller 应当尽可能简单,但是那些没有被移 出 view controller 的部分仍然需要单独进行测试。在我们的实现中,这部分内容包括与协调器 的交互,以及初始时负责创建工作的代码。
MVVM 的重要性
MVVM 是 iOS 上*流行的 MVC 的非直接变形的 app 设计模式。换言之,它和 MVC 相比,并没有非常大的不同;两者都是围绕 view controller 场景构建的,而且所使用的机制也大都相同。

*大的区别可能在于 view-model 中对响应式编程的使用了,它被用来描述一系列的转换和依 赖关系。通过使用响应式编程来清晰地描述 model 对象与显示值之间的关系,为我们从总体上 理解应用中的依赖关系提供了重要的指导。

iOS 中的协调器是一种很有用的模式,因为管理 view controller 层级是一件非常重要的事情。 协调器在本质上并没有和 MVVM 绑定,它也能被使用在 MVC 或者其他模式上。

历史
MVVM 由 Ken Cooper 和 Ted Peters 提出,他们当时在微软工作,负责后来变成 Windows Presentation Foundation (WPF) 的项目,这是微软.NET 的 app 框架,并于 2005 年正式发布。

WPF 使用一种基于 XML,被称为 XAML 的描述性语言来描述 view 所绑定的某个 view-model 上的属性。在 Cocoa 中,没有 XAML,我们必须使用像是 RxSwift 这样的框架和一些 (通常存 在于 controller 中的) 代码来完成 view-model 和 view 的绑定。

MVVM 和我们在 MVC 历史中提到的 MVP 模式非常类似. 不过,在 Cooper 和 Peters 的论述中, MVVM 中 view 和 view-model 的绑定需要明确的框架支持,但 presenter 是通过传统的手动 方式来传递变化。

iOS 中的协调器则是*近才 (重新) 流行起来的,Soroush Khanlou 在 2015 年时在他的网站上描述了这个想法。协调器基于 app controller 这样的更古老的模式,而它们在 Cocoa 和其他平台上已经存在了有数十年之久。

3. Model-View-Controller+ViewState
MVC+VS 是为标准的 MVC 带来单向数据流方式的一种尝试。在标准的 Cocoa MVC 中,view state 可以由两到三种不同的路径进行操作,MVC+VS 则试图避免这点,让 view state 的处理 更加易于管理。在 MVC+VS 中,我们明确地在一个新的 model 对象中,对所有的 view state 进行定义和表达,我们把这个对象叫做 view state model。

在 MVC+VS 中,我们不会忽略任何一次导航变更,列表选择,文本框编辑,开关变更,model 展示或者滚动位置变更 (或者其他任意的 view state 变化)。我们将这些变更发送给 view state model。每个 view controller 负责监听 view state model,这样变更的通讯会非常直接。在表现或者交互逻辑部分,我们不从 view 中去读取 view state ,而是从 view state model 中去获 取它们:

%title插图%num

结果所得到的图表和 MVC 类似,但 controller 的内部反馈回路的部分 (被用来更新 view state) 有所不同,现在它和 model 的回路类似,形成了一个独立的 view state 回路。

构建
和传统的 MVC 一样,将文档 model 数据应用到 view 上的工作依然是 view controller 的责任, view controller 还会使用和订阅 view state 。因为 view state model 和文档 model 都需要观 察,所以相比于典型的 MVC 来说,我们需要多得多的通过通知进行观察的函数。
更改 Model
当 view action 发生时,view controller 去变更文档 model (这和 MVC 保持不变) 或者变更 model state。我们不会去直接改变 view 层级,所有的 view 变更都要通过文档 model 和 view state model 的通知来进行。
更改 View
Controller 同时对文档 model 和 view state model 进行观察,并且只在变更发生的时候更新 view 层级。
View State
View State 被明确地从 view controller 中提取出来。处理的方法和 model 是一样的: controller 观察 view state model,并且对应地更改 view 层级。
测试
在 MVC+VS 中,我们使用和 MVC 里类似的集成测试,但是测试本身会非常不同。所有的测试 都从一个空的根 view controller 开始,然后通过设定文档 model 和 view state model,这个 根 view controller 可以构建出整个 view 层级和 view controller 层级。MVC 的集成测试中*困 难的部分 (设定所有的部件) 在 MVC+VS 中可以被自动完成。要测试另一个 view state 时,我 们可以重新设置全局 view state,所有的 view controller 都会调整自身。
一旦 view 层级被构建,我们可以编写两种测试。*种测试负责检查 view 层级是不是按照我 们的期望被建立起来,第二种测试检查 view action 有没有正确地改变 view state。
MVC+VS 的重要性
MVC+VS 主要是用来对 view state 进行教学的工具。
在一个非标准 MVC 的 app 中,添加一个 view state model,并且在每个 view controller 中 (在已经对 model 进行观察的基础上) 观察这些 view state model,提供了不少优点:任意的状 态恢复 (这种恢复不依赖于 storyboard 或者 UIStateRestoration),完整的用戶界面日志,以及 为了调试目的,在不同的 view state 间进行跳转的能力。

历史
这种特定的体系是 Matt Gallagher 在 2017 年开发的教学工具,它被用来展示单向数据流和用 戶界面的时间旅行等概念。这个模式的目标是,在传统的 Cocoa MVC app 上通过*小的改动, 实现对 view 的状态在每个 action 发生时都可以进行快照。

4. Model 适配器-View 绑定器 (MAVB)
MAVB 是一种以绑定为中心的实验模式。在这个模式中,有三个重要的概念:view 绑定器, model 适配器,以及绑定。

View 绑定器是 view (或者 view controller) 的封装类:它构建 view,并且为它暴露出一个绑定 列表。一些绑定为 view 提供数据 (比如,一个标签的文本),另一些从 view 中发出事件 (比如, 按钮点击或者导航变更)。

虽然 view 绑定器可以含有动态绑定,但是 view 绑定器本身是不可变的。这让 MAVB 也成为了 一种声明式的模式:你声明 view 绑定器和它们的 action,而不是随着时间去改变 view 绑定器。

Model 适配器是可变状态的封装,它是由所谓的 reducer 进行实现的。Model 适配器提供了一 个 (用于发送事件的) 输入绑定,以及一个 (用于接收更新的) 输出绑定。

在 MAVB 中,你不会去直接创建 view;相对地,你只会去创建 view 绑定器。同样地,你也从 来不会去处理 model 适配器以外的可变状态。在 view 绑定器和 model 适配器之间的 (两个方 向上的) 变换,是通过 (使用标准的响应式编程技术) 来对绑定进行变形而完成的。

MAVB 移除了对 controller 层的需求。创建逻辑通过 view 绑定器来表达,变换逻辑通过绑定来 表达,而状态变更则通过 model 适配器来表达。结果得到的框图如下:

%title插图%num

构建
Model 适配器 (用来封装主 model ) 和 view state 适配器 (封装顶层的 view state) 通常是在
main.swift 文件中进行创建的,这早于任何的 view。
View 绑定器使用普通的函数进行构建,这些函数接受必要的 model 适配器作为参数。实际的
Cocoa view 则由框架负责进行创建。 2. 更改 Model
当一个 view (或者 view controller) 可以发出 action 时,对应的 view 绑定允许我们指定一个 action 绑定。在这里,数据从 view 流向 action 绑定的输出端。典型情况下,输出端会与一个 model 适配器相连接,view 事件会通过绑定进行变形,成为 model 适配器可以理解的一条消 息。这条消息随后被 model 适配器的 reducer 使用,并改变状态。
更改 View
当 model 适配器的状态发生改变时,它会通过输出信号产生通知。在 view 绑定器中,我们可 以将 model 适配器的输出信号进行变形,并将它绑定到一个 view 属性上去。这样一来,view 属性就会在一个通知被发送时自动进行变更了。
View State
View state 被认为是 model 层的一部分。View state action 以及 view state 通知和 model action 以及 model 通知享有同样的路径。
测试
在 MAVB 中,我们通过测试 view 绑定器来测试代码。由于 view 绑定器是一组绑定的列表,我 们可以验证绑定包含了我们所期望的条目,而且它们的配置正确无误。我们可以和使用绑定来 测试初始构建以及发生变化时的情况。
在 MAVB 中进行的测试,与在 MVVM 中的测试很相似。不过,在 MVVM 中,view controller 有可能会包含逻辑,这导致在 view-model 和 view 之间有可能会存在没有测试到的代码。而 MAVB 中不存在 view controller,绑定代码是 model 适配器和 view 绑定器之间的唯一的代码, 这样一来,保证完整的测试覆盖要简单得多。
MAVB 的重要性
在我们所讨论的主要模式之中,MAVB 没有遵循某个直接的先例,它既不是从其他平台移植过 来的模式,也不是其他模式的变种。它自成一派,用于试验目的,而且一些奇怪。我们在这儿 介绍它的意义在于,它展示了一些很不一样的东西。不过,这并不是说这个模式没有从其他模 式中借鉴经验教训:像是绑定、响应式编程、领域专用语言以及 reducer 都是已经被熟知的想 法了。
历史
MAVB 是 Matt Gallagher 在 Cocoa with Love 网站上首先提出的。这个模式参照了 Cocoa 绑 定、函数式响应动画、ComponentKit、XAML、Redux 以及成千上万行的使用 Cocoa view controller 的经验。
本书中的实现使用了 CwlViews 框架来处理 view 构建、绑定器和适配器的实现等工作。
5. Elm 架构 (TEA)
TEA 和 MVC 有着根本上的不同。在 TEA 中,model 和所有的 view state 被集成为一个单个状 态对象,所有 app 中的变化都通过向状态对象发送消息来发生,一个叫做 reducer 的状态更新 函数负责处理这些消息。
在 TEA 中,每个状态的改变会生成一个新的虚拟 view 层级,它由轻量级的结构体组成,描述 了 view 层级应该看上去的形式。虚拟 view 层级让我们能够使用纯函数的方式来写 view 部分 的代码;虚拟 view 层级总是直接从状态进行计算,中间不会有任何副作用。当状态发生改变 时,我们使用同样的函数重新计算 view 层级,而不是直接去改变 view 层级。
Driver 类型 (这是 TEA 框架中的一部分,它负责持有对 TEA 中其他层的引用) 将对虚拟 view 层 级和 UIView 层级进行比较,并且对它进行必要的更改,让 view 和它们的虚拟版本相符合。这 个 TEA 框架中的 driver (驱动) 部件是随着我们 app 的启动而被初始化的,它自身并不知道要对 应哪个特定的 app。我们要在它的初始化方法中传入这些信息:包括 app 的初始状态,一个通 过消息更新状态的函数,一个根据给定状态渲染虚拟 view 层级的函数,以及一个根据给定状态 计算通知订阅的函数 (比如,我们可以订阅某个 model store 更改时所发出的通知)。
从框架的使用者的视⻆来看,TEA 的关于更改部分的框图是这样的:

%title插图%num
如果我们追踪这张图表的上面两层,我们会发现在 view 和 model 之间存在我们在本章开头是 就说过的反馈回路;这是一个从 view 到状态,然后再返回 view 的回路 (通过 TEA 框架进行协 调)。
下面的回路代表的是 TEA中处理副作用的方式 (比如将数据写入磁盘中):当在状态更新方法中 处理消息时,我们可以返回一个命令,这些命令会被 driver 所执行。在我们的例子中,*重要 的命令是更改 store 中的内容,store 反过来又被 driver 所持有的订阅者监听。这些订阅者可 以触发消息来改变状态,状态*终触发 view 的重新渲染作为响应。
这些事件回路的结构让 TEA 成为了遵守单向数据流原则的设计模式的另一个例子。

构建
状态在启动时被构建,并传递给运行时系统 (也就是 driver)。运行时系统拥有状态,store 是一 个单例。
初始的 view 层级和之后更新时的 view 层级是通过同样的路径构建的:通过当前的状态,计算 出虚拟 view 层级,运行时系统负责更新真实的 view 层级,让它与虚拟 view 层级相匹配。
更改 Model
虚拟 view 拥有与它们所关联的消息,这些消息在一个 view 事件发生时会被发送。Driver 可以 接收这些消息,并使用更新方法来改变状态。更新方法可以返回一个命令 (副作用),比如我们
想在 store 中进行的改动。Driver 会截获该命令并执行它。TEA 让 view 不可能直接对状态或者 store 进行更改。
更改 View
运行时系统负责这件事。改变 view 的唯一方式是改变状态。所以,初始化创建 view 层级和更
新 view 层级之间没有区别。 4. View State
View state 是包含在整体的状态之中的。由于 view 是直接从状态中计算出来的,导航和交互状 态也同样会被自动更新。
测试
在大多数架构中,让测试部件彼此相连往往要花费大量努力。在 TEA 中,我们不需要对此进行 测试,因为 driver 会自动处理这部分内容。类似地,我们不需要测试当状态变化时 view 会正确 随之变化。我们所需要测试的仅仅是对于给定的状态,虚拟 view 层级可以被正确计算。
要测试状态的变更,我们可以创建一个给定的状态,然后使用 update 方法和对应的消息来改 变状态。然后通过对比之前和之后的状态,我们就可以验证 update 是否对给定的状态和消息 返回了所期望的结果。在 TEA 中,我们还可以测试对应给定状态的订阅是不是正确。和 view 层级一样,update 函数和订阅也都是纯函数。
因为所有的部件 (计算虚拟 view 层级,更新函数和订阅) 都是纯函数,我们可以对它们进行完 全隔离的测试。任何框架部件的初始化都是不需要的,我们只用将参数传递进去,然后验证结 果就行了。我们 TEA 实现中的大多数测试都非常直截了当。
Elm 架构的重要性
TEA *早是在 Elm 这⻔函数式语言中被实现的。所以 TEA 是一种如何用函数式的方法表达 GUI 编程的尝试。TEA 同时也是*为古老的单向数据流架构。

历史
Elm 是 Evan Czaplicki 所设计的函数式编程语言,它*初的目的是为了构建前端 web app。 TEA 是归功于 Elm 社区的一个模式,它的出现是语言约束和目标环境相互作用的自然结果。它 背后的思想影响了很多其他的基于 web 的框架,其中包括 React、Redux 和 Flux 等。在 Swift 中,还没有 TEA 的权威实现,不过我们可以找到不少研究型的项目。在本书中,我们使用 Swift 按我们自己的理解实现了这个模式。主要的工作由 Chris Eidhof 于 2017 年完成。虽然我 们的这个实现还并不是 “产品级” 的,但是许多想法是可以用在生产代码中的。

三,其他APP架构模式
1. Model-View-Presenter
Model-View-Presenter (MVP) 是一种在 Android 上很流行的模式,在 iOS 中,也有相应的实 现。在总体结构和使用的技术上,它粗略来说是一种位于标准 MVC 和 MVVM 之间的模式。

MVP 使用单独的 presenter 对象,它和 MVVM 中 view-model 所扮演的⻆色一样。相对 view-model 而言,presenter 去除了响应式编程的部分,而是把要展示的值暴露为接口上的属 性。不过,每当这些值需要变更的时候,presenter 会立即将它们推送到下面的 view 中去 (view 将自己作为协议暴露给 presenter)。
从抽象的观点来看,MVP 和 MVC 很像。Cocoa 的 MVC,除了名字以外,就是一个 MVP – 它是 从上世纪九十年代 Taligent 的原始的 MVP 实现中派生出来的。View,状态和关联的逻辑在两 个模式中都是一样的。不同之处在于,现代的 MVP 中有一个分离的 presenter 实体,它使用协 议来在 presenter 和 view controller 之间进行界定,Cocoa 的 MVC 让 controller 能够直接引 用 view,而 MVP 中的 presenter 只能知道 view 的协议。

有些开发者认为协议的分离对于测试是必要的。当我们在讨论测试时,我们会看到标准的 MVC 在没有任何分离的情况下,也可以被完整测试。所以,我们感觉 MVP 并没有太大不同。如果我 们对测试一个完全解耦的展示层有强烈需求的话,我们认为 MVVM 的方式更简单一些:让 view controller 通过观察去从 view-model 中拉取值,而不是让 presenter 将值推送到一个协 议中去。

2. VIPER,Riblets,和其他 “Clean” 架构
VIPER,Riblets 和其他类似的模式尝试将 Robert Martin 的 “Clean Architecture” 从 web app 带到 iOS 开发中,它们主要把 controller 的职责分散到三到四个不同的类中,并用严格的顺序 将它们排列起来。在序列中的每个类都不允许直接引用序列中前面的类。

为了强制单方向的引用这一规则,这些模式需要非常多的协议,类,以及在不同层中传递数据 的方式。由于这个原因,很多使用这些模式的开发者会去使用代码生成器。我们的感觉是,这 些代码生成器,以及任何的繁杂到需要生成器的模式,都产生了一些误导。将 “Clean” 架构带 到 Cocoa 的尝试通常都宣称它们可以管理 view controller 的 “肥大化” 问题,但是让人啼笑皆 非的是,这么做往往让代码库变得更大。

虽然将接口分解是控制代码尺寸的一种有效手段,但是我们认为这应该按需进行,而不是教条 式地对每个 view controller 都这么操作。分解接口需要我们对数据以及所涉及到的任务有清楚 的认识,只有这样,我们才能达到*优的抽象,并在*大程度上降低代码的复杂度。

3. 基于组件的架构 (React Native)
如果你选择使用 JavaScript 而不是 Swift 编程,或者你的 app 重度依赖于 web API 的交互, JavaScript 会是更好的选择,这时你可能会考虑 React Native。不过,本书是专注于 Swift 和 Cocoa 的,所以我们将探索模式的界限定在了这些领域内。

如果你想要找一些类似 React Native,但是是基于 Swift 的东西的话,可以看看我们对 TEA 的 探索。MAVB 的实现也从 ComponentKit 中获得了一些启发,而 ComponentKit 本身又从 React 中获取灵感:它使用类 DSL 的语法来进行声明式和可变形的 view 构建,这和 React 中 Component 的 render 方法及其相似。

MAC/IOS app crash日志符号表还原方法调用

1. Crash日志收集平台简介
2. 手动还原符号表
2.1 准备资料
我们开发经常通过一些crash日志平台收集到一些crash日志,有时候这些日志并没有通过符号表来还原成我们想看到的具体是哪一行代码出错了,这个时候我们需要手动去还原符号表。

下面我以bugsplat平台为例,来讲解手动还原的过程:

首先我通过bugsplat平台日志分析到有一个crash问题,https://app.bugsplat.com/v2/crash?database=Wondershare_VideoEditor_Mac&id=807244 但是这个crash我能够看到的只有那些符号表,没有还原成对应的代码。我可以在网站上看到这么一行:

Explanation:
SIGABRT #0 at 0x7fff9b375f06 Application Specific Information: *** Terminating app due to uncaught exception ‘NSInvalidArgumentException’, reason: ‘-[NSTextView setAutomaticTextCompletionEnabled:]: unrecognized selector sent to instance 0x7fe8b929d420’

2.2 还原步骤
创建一个新的文件夹,mac下终端输入命令:
mkdir testcrash

导出symbolicatecrash 可执行文件:
执行下面这句命令后,可以打印出多个对应的文件路径:

find /Applications/Xcode.app -name symbolicatecrash -type f

输入命令的后的结果如下:

Crash find /Applications/Xcode.app -name symbolicatecrash -type f
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/iOSSupport/Library/PrivateFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash
/Applications/Xcode.app/Contents/Developer/Platforms/WatchSimulator.platform/Developer/Library/PrivateFrameworks/DVTFoundation.framework/symbolicatecrash
/Applications/Xcode.app/Contents/Developer/Platforms/AppleTVSimulator.platform/Developer/Library/PrivateFrameworks/DVTFoundation.framework/symbolicatecrash
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/PrivateFrameworks/DVTFoundation.framework/symbolicatecrash
/Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash

选择一个对应的平台即可,我这里是macos 选择*个即可。

然后,再执行下面的命令,把symbolicatecrash文件拷贝到当前目录

cp /Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash ./

把对应的.dSYM, .app, .crash, symbolicatecrash 文件放置在*步创建的文件夹同一目录中。

设置环境变量
终端输入:

export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer

还原符号表,导出崩溃日志
./symbolicatecrash plCrashReporter.crashlog test.dSYM > app.log

苹果框架学习(二) Metal

文章目录
苹果框架学习(二) Metal
Metal简介
1. Essentials
1.1 基本任务和概念
1.2 将OpenGL代码迁移到Metal
1.3 将您的Metal代码移植到苹果Arm芯片
2. GPUs
2.1 获取默认GPU
2.2 macOS中的GPU选择
2.3 protocol MTLDevice
2.4 GPU 特征
3. Command Setup
3.1 建立一个命令结构
3.2 准备你的Metal应用程序在后台运行
3.3 protocol MTLCommandQueue
3.4 protocol MTLCommandBuffer
3.5 protocol MTLCommandEncoder
3.6 计数器采样
4. 并行计算
5. 射线跟踪
6. 渲染
7. Presentation
8. Shaders
9. Resources
10. 对象大小调整和定位
11. 填充向量和矩阵
12. Time
13. Tools
14. GPU编程技术
15. Reference
16. Related Documentation
苹果框架学习(二) Metal
Metal简介
Metal是渲染高级3D图形和执行数据并行计算使用图形处理器。

图形处理器(gpu)被设计用来快速渲染图形和执行数据并行计算。当您需要直接与设备上可用的gpu通信时,请使用Metal框架。渲染复杂场景或执行高级科学计算的应用程序可以利用这种能力实现*大的性能。这些应用程序包括:

渲染复杂3D环境的游戏

视频处理应用程序,比如Final Cut Pro

数据处理应用程序,比如那些用于进行科学研究的应用程序

Metal与其他补充其功能的框架携手工作。使用MetalKit可以简化将Metal内容显示在屏幕上的任务。使用Metal Performance着色器来实现自定义渲染函数或利用现有函数的大型库。

许多高级Apple框架都构建在Metal之上,以利用其性能,包括Core Image、SpriteKit和SceneKit。使用这些高级框架之一可以避免您接触到GPU编程的细节,但是编写定制的Metal代码可以使您获得*高水平的性能。

1. Essentials
1.1 基本任务和概念
基本任务和概念
通过一系列示例代码项目熟悉Metal

1.2 将OpenGL代码迁移到Metal
将OpenGL代码迁移到Metal
用Metal替换应用程序中已弃用的OpenGL代码。

1.3 将您的Metal代码移植到苹果Arm芯片
将您的Metal代码移植到苹果Arm芯片
创建一个版本的Metal应用程序,运行在苹果硅和英特尔的Mac电脑上。

2. GPUs
在运行时访问GPU设备。图形处理器是Metal开发的基础。

2.1 获取默认GPU
获取默认GPU
选择要在其上运行Metal代码的系统默认GPU设备。

要使用Metal框架,首先要获得一个GPU设备。应用程序与Metal交互所需的所有对象都来自一个在运行时获得的MTLDevice。iOS和tvOS设备只有一个GPU可以通过调用MTLCreateSystemDefaultDevice()来访问:

2.2 macOS中的GPU选择
macOS中的GPU选择
通过考虑GPU能力、功率或性能特征,选择一个或多个GPU来运行您的Metal代码。

2.3 protocol MTLDevice
protocol MTLDevice
图形处理器的Metal接口,用于绘制图形或进行并行计算。

MTLDevice协议定义了到GPU的接口。您可以查询MTLDevice为您的Metal应用程序提供的独特功能,并使用MTLDevice发出所有的Metal命令。不要自己执行这个协议;相反,在iOS或tvOS中,在运行时使用MTLCreateSystemDefaultDevice() 从系统请求GPU;在macOS中,使用MTLCopyAllDevicesWithObserver(handler:)获取可用MTLDevice对象的列表。关于选择正确的GPU(s)的完整讨论,请参见获取默认GPU。

MTLDevice对象是在Metal中执行任何操作的go-to对象,因此应用程序与之交互的所有Metal对象都来自于运行时获得的MTLDevice实例。mtldevice创建的对象开销大但持久;其中许多对象被设计为初始化一次,并在应用程序的整个生命周期中重用。然而,这些对象特定于发出它们的MTLDevice。如果使用多个MTLDevice实例或希望从一个MTLDevice切换到另一个MTLDevice,则需要为每个MTLDevice创建一组单独的对象。

2.4 GPU 特征
GPU 特征
查找特定GPU家族的特征信息。

Metal Feature Sets

使用Metal特征设置表
根据Metal软件版本和GPU家族查找功能可用性。
Metal中功能的可用性是由GPU支持的Metal软件版本和家族功能集的组合决定的。Metal功能集表提供了功能的可用性,特定的数值限制,以及对不同GPU家族的像素格式支持。

Apple GPU Families
理解GPU家族4
了解A11的特性,包括光栅顺序组、平铺着色器和图像块。

GPU家族4描述了A11芯片和苹果设计的图形处理单元(GPU)架构带来的新特性和性能提升。

iOS和tvOS设备中的gpu实现了一种称为基于tile的延迟渲染(TBDR)的渲染技术,以优化性能和功耗。在传统的即时模式(IM)渲染器中,当一个三角形被提交给GPU处理时,它会立即被渲染到设备内存中。即使三角形被后来提交给GPU的其他原语遮挡,三角形也会被光栅化和片段函数处理。

基于Tile延迟渲染
TBDR对IM架构做了一些重要的改变,在所有的原语都提交之后处理场景。屏幕被分割成小块,分别进行处理。所有几何体的交叉块被同时处理,并且遮挡碎片被丢弃在光栅化和碎片着色阶段之前。块被渲染到GPU上的快速本地内存中,只有在渲染完成后才被写到设备内存中。

TBDR允许顶点和片段阶段异步运行——比IM提供了显著的性能改进。当运行渲染通道的片段阶段时,硬件并行地执行未来渲染通道的顶点阶段。顶点阶段通常大量使用固定功能硬件,而片段阶段使用数学和带宽。完全重叠它们允许设备同时使用GPU上的所有硬件块。

TBDR使用的平铺内存有三个重要特征。首先,着色器核心和瓷砖存储器之间的带宽比GPU和设备存储器之间的带宽高很多倍,并比例与着色器核心的数量。其次,访问瓷砖内存的内存访问延迟比访问设备内存的延迟低很多倍。*后,平铺内存比设备内存消耗更少的能量。

在基于A7到a10的设备上,Metal没有明确地描述这种基于瓷砖的架构;相反,您使用它来为底层实现提供提示。例如,加载和存储操作控制哪些数据被加载到本地内存以及哪些数据被写到设备内存中。类似地,无内存缓冲区指定仅在渲染过程中使用的每像素中间数据;在实践中,这些数据存储在GPU的快速本地内存中的一个tile中。

A11 GPU上的Metal2

在A11中,苹果设计的GPU提供了几个显著增强TBDR的功能。这些特性是通过额外的Metal 2 api提供的,并允许您的应用程序和游戏实现新的性能和功能级别。

这些特性包括imageblock、平铺阴影、光栅顺序组和imageblock样本覆盖率控制。A11 GPU上的Metal 2也提高了碎片丢弃的性能。

总的来说,这些特性提供了对内存布局的更好控制和对存储在tile中的数据的访问,并提供了更细粒度的同步以保持GPU上的更多工作。*终结果是,与以前相比,您可以在一次渲染传递中执行更广泛的计算,并将计算保持在快速的本地内存中。

在A11上的Metal 2还简化了技术的实现,如地下散射、顺序无关的透明度和基于瓷砖的照明算法。

3. Command Setup
建立基础设施,以执行您的自定义代码的GPU。

3.1 建立一个命令结构
建立一个命令结构
了解Metal如何在GPU上执行命令。

3.2 准备你的Metal应用程序在后台运行
准备你的Metal应用程序在后台运行
通过暂停未来的GPU使用和确保之前的工作安排,准备你的应用移到后台。

3.3 protocol MTLCommandQueue
MTLCommandQueue
一种为GPU执行组织命令缓冲区的队列。

3.4 protocol MTLCommandBuffer
MTLCommandBuffer
为GPU存储要执行的编码命令的容器。

3.5 protocol MTLCommandEncoder
MTLCommandEncoder
一种将GPU命令写入命令缓冲区的编码器。

3.6 计数器采样
计数器采样
检索关于GPU如何执行你的命令的信息。

4. 并行计算
Process arbitrary calculations in parallel on the GPU.

Processing a Texture in a Compute Function
Perform data-parallel computations on texture data.

Creating Threads and Threadgroups
Learn how Metal organizes compute-processing workloads.

Calculating Threadgroup and Grid Sizes
Calculate the optimum sizes for threadgroups and grids when dispatching compute-processing workloads.

class MTLComputePipelineDescriptor
An object used to customize how a new compute pipeline state object is compiled.

protocol MTLComputePipelineState
An object that contains a compiled compute pipeline.

class MTLComputePassDescriptor
A configuration for a compute pass, used to create a compute command encoder.

protocol MTLComputeCommandEncoder
An object used to encode commands in a compute pass.

5. 射线跟踪
Accelerating Ray Tracing Using Metal
Implement ray-traced rendering using GPU-based parallel processing

protocol MTLAccelerationStructure
A collection of model data, organized to allow for GPU-accelerated intersection of rays with the model.

class MTLAccelerationStructureDescriptor
A base class for classes that define the configuration for a new acceleraton structure.

class MTLAccelerationStructureGeometryDescriptor
A base class for descriptors that contain geometry data to convert into a ray-tracing acceleration structure.

class MTLAccelerationStructureBoundingBoxGeometryDescriptor
A description of a list of bounding boxes to turn into an acceleration structure.

class MTLAccelerationStructureTriangleGeometryDescriptor
A description of a list of triangle primitives to turn into an acceleration structure.

class MTLPrimitiveAccelerationStructureDescriptor
A description of an acceleration structure that contains geometry primitives.

class MTLInstanceAccelerationStructureDescriptor
A description of an acceleration structure built from instances of primitive acceleration structures.

struct MTLAccelerationStructureInstanceDescriptor
A description of an instance in an instanced geometry acceleration structure.

protocol MTLIntersectionFunctionTable
A table of visible functions that Metal calls to perform ray-tracing intersection tests.

class MTLIntersectionFunctionTableDescriptor
A description that describes how to create an intersection function table.

class MTLIntersectionFunctionDescriptor
A description of a visible function that performs an intersection test.

protocol MTLAccelerationStructureCommandEncoder
A object used to encode commands that build or refit acceleration structures.

6. 渲染
Render graphics by issuing draw calls, and choose a presentation object if you’re drawing to the screen.

Using a Render Pipeline to Render Primitives
Render a simple 2D triangle.

Creating and Sampling Textures
Load image data into a texture and apply it to a quadrangle.

Calculating Primitive Visibility Using Depth Testing
Determine which pixels are visible in a scene by using a depth texture.

Customizing Render Pass Setup
Render into an offscreen texture by creating a custom render pass.

Generating Multiple Output Vertex Streams from One Input Stream
Render efficiently to multiple layers or viewports.

Render Pipelines
Specify how graphics primitives should be rendered.

class MTLRenderPassDescriptor
A group of render targets that hold the results of a render pass.

protocol MTLRenderCommandEncoder
The object to use for encoding commands for a render pass.

protocol MTLParallelRenderCommandEncoder
An object that splits up a single render pass so that it can be simultaneously encoded from multiple threads.

Model I/O
Specify precise locations within the textures associated with graphics processing.

7. Presentation
Display Metal textures onscreen.

Drawable Objects
Obtain textures to draw into from drawable objects.

8. Shaders
Shader Authoring
Write your GPU code in the Metal Shading Language.

Libraries
Organize your shaders into libraries.

Functions
Retrieve information about rendering and compute functions.

9. Resources
Create objects to hold GPU data.

Setting Resource Storage Modes
Set a storage mode that defines the memory location and access permissions of a resource.

Copying Data to a Private Resource
Use a blit command encoder to copy buffer or texture data to a private resource.

Synchronizing a Managed Resource
Synchronize the contents of a managed resource for the CPU or GPU.

Transferring Data Between Connected GPUs
Use high-speed connections between GPUs to transfer data quickly.

Reducing the Memory Footprint of Metal Apps
Learn best practices for using memory efficiently in iOS and tvOS.

protocol MTLResource
An allocation of memory that is accessible to a GPU.

protocol MTLBlitCommandEncoder
An encoder that encodes memory copying, filtering, and fill commands.

class MTLBlitPassDescriptor
A configuration for a blit pass, used to create a blit command encoder.

protocol MTLResourceStateCommandEncoder
An encoder that encodes commands that modify resource configurations.

class MTLResourceStatePassDescriptor
A configuration for a resource state pass, used to create a resource state command encoder.

Buffers
Create and manipulate unstructured GPU resources.

Textures
Create and manipulate structured GPU resources.

Indirect Command Buffers
Recoup encoding time by reusing commands, or create a GPU-driven rendering pipeline by generating commands on the GPU.

Heaps
Create a single allocation of memory from which you can suballocate resources.

Synchronization
Manage access to resources in your app to avoid data hazards.

10. 对象大小调整和定位
Set the sizes and positions of many Metal objects.

struct MTLOrigin
The coordinates for the front upper-left corner of a region.

struct MTLRegion
The bounds for a subset of an object’s elements.

struct MTLSize
The dimensions of an object.

typealias MTLCoordinate2D
A coordinate in the viewport.

func MTLCoordinate2DMake(Float, Float) -> MTLCoordinate2D
Returns a new 2D point with the specified coordinates.

11. 填充向量和矩阵
typealias MTLPackedFloat3
A stucture that contains three 32-bit floating-point values with no additional padding.

typealias MTLPackedFloat4x3
A structure that contains the top three rows of a 4×4 matrix of 32-bit floating-point values, in column-major order.

12. Time
typealias MTLTimestamp
A timestamp, in Mach absolute time.

13. Tools
Diagnosing Metal Programming Issues Early
Identify Metal framework and shader programming errors during development using
Xcode’s diagnostic tools.

Developing Metal Apps that Run in Simulator
Prototype and test your Metal apps in Simulator.

Supporting Simulator in a Metal App
Modify Metal Apps to Run in Simulator.

Frame Capture Debugging Tools
Analyze and optimize your app performance at runtime.

Using Metal System Trace in Instruments to Profile Your App
Smooth out your frame rate by checking for issues in your app’s CPU and GPU utilization.

Optimizing Performance with the GPU Counters Instrument
Examine your app’s use of GPU resources in Instruments, and tune your app as needed.

14. GPU编程技术
Learn a variety of strategies for executing code efficiently on GPUs, and experiment with their companion sample code.

Rendering a Scene with Forward Plus Lighting Using Tile Shaders
Implement a forward plus renderer using the latest features on Apple GPUs.

Rendering a Scene with Deferred Lighting
Implement a deferred lighting renderer that takes advantage of unique Metal features.

Rendering Reflections with Fewer Render Passes
Use layer selection to reduce the number of render passes needed to generate an environment map.

Using Function Specialization to Build Pipeline Variants
Create pipelines for different levels of detail from a common shader source.

Rendering Terrain Dynamically with Argument Buffers
Use argument buffers to render terrain in real time with a GPU-driven pipeline.

Mixing Metal and OpenGL Rendering in a View
Draw with Metal and OpenGL in the same view using an interoperable texture.

Modern Rendering with Metal
Use advanced Metal features such as Indirect Command Buffers, Sparse Textures, and Variable Rate Rasterization to implement modern rendering algorithms.

15. Reference
Metal Structures

Metal Enumerations

Metal Type Aliases

16. Related Documentation
Metal Shading Language Guide

Metal Feature Sets

MetalKit
Build Metal apps quicker and easier, using far less code. Render graphics in a standard Metal view, load textures from many sources, and work efficiently with models provided by Model I/O.

Metal Performance Shaders
Optimize graphics and compute performance with kernels that are fine-tuned for the unique characteristics of each Metal GPU family.

Metal Programming Guide

Metal Best Practices Guide

IOS面试考察(四):系统基础,网络,操作系统相关问题

文章目录
1. IOS面试考察(四):系统基础,网络,操作系统相关问题
1.1 网络原理相关
1.2 操作系统原理相关
1.3 编译原理相关
1.4 数据结构相关
1.5 其他原理性相关
1.6 开发证书相关
1. IOS面试考察(四):系统基础,网络,操作系统相关问题
1.1 网络原理相关
进程和线程的区别
HTTPS的握手过程
什么是中间人攻击?怎么预防
TCP的握手过程?为什么进行三次握手,四次挥手
堆和栈区的区别?谁的占用内存空间大
加密算法:对称加密算法和非对称加密算法区别
常见的对称加密和非对称加密算法有哪些
MD5、Sha1、Sha256区别
charles抓包过程?不使用charles,4G网络如何抓包
1.2 操作系统原理相关
1.3 编译原理相关
1.4 数据结构相关
对于移动开发者来说,一般不会遇到非常难的算法,大多以数据结构为主,笔者列出一些必会的算法,当然有时间了可以去LeetCode上刷刷题

八大排序算法
栈&队列
字符串处理
链表
二叉树相关操作
深搜广搜
基本的动态规划题、贪心算法、二分查找
1.5 其他原理性相关
PerformSelector & NSInvocation优劣对比
oc怎么实现多继承?怎么面向切面(可以参考Aspects深度解析-iOS面向切面编程)
哪些bug会导致崩溃,如何防护崩溃
怎么监控崩溃
app的启动过程(考察LLVM编译过程、静态链接、动态链接、runtime初始化)
沙盒目录的每个文件夹划分的作用
简述下match-o文件结构
1.6 开发证书相关
苹果使用证书的目的是什么
AppStore安装app时的认证流程
开发者怎么在debug模式下把app安装到设备呢

IOS屏幕适配(四)*新系统IOS13适配

IOS屏幕适配(四)*新系统IOS13适配
3. IOS *新系统适配问题
3.1 IOS 13 适配
3.1.1 即将废弃的 LaunchImage
3.1.2 Sign in with Apple -提供第三方登录的注意啦
3.1.3 iOS 13 DeviceToken有变化
3.1.4 MPMoviePlayerController 在iOS 13已经不能用了
3.1.5 控制器的 modalPresentationStyle 默认值变了
3.1.6 UITextField 的私有属性 _placeholderLabel 被禁止访问了
3.1.7 UISearchBar显示问题
3.1.8 黑暗模式 Dark Mode
3.1.8.1 适配黑暗模式
3.1.8.1.1 模拟器调试
3.1.8.1.2 图片适配
3.1.8.1.3 颜色适配
3.1.8.1.4 状态栏适配
3.1.9 模态弹出默认交互改变
3.1.10 App启动过程中,部分View可能无法实时获取到frame
3. IOS *新系统适配问题
苹果官方资料:
WWDC19视频
Xcode 11 beta 下载
macOS Catalina 10.15 beta 下载
3.1 IOS 13 适配
3.1.1 即将废弃的 LaunchImage
从 iOS 8 的时候,苹果就引入了 LaunchScreen,我们可以设置 LaunchScreen来作为启动页。当然,现在你还可以使用LaunchImage来设置启动图。不过使用LaunchImage的话,要求我们必须提供各种屏幕尺寸的启动图,来适配各种设备,随着苹果设备尺寸越来越多,这种方式显然不够 Flexible。而使用 LaunchScreen的话,情况会变的很简单, LaunchScreen是支持AutoLayout+SizeClass的,所以适配各种屏幕都不在话下。

注意啦⚠️,从2020年4月开始,所有使⽤ iOS13 SDK的 App将必须提供 LaunchScreen,LaunchImage即将退出历史舞台*。
3.1.2 Sign in with Apple -提供第三方登录的注意啦
如果你的应用使用了第三方登录,那么你可能也需要加下 「Sign in with Apple」
Sign In with Apple will be available for beta testing this summer. It will be required as an option for users in apps that support third-party sign-in when it is commercially available later this year.

如何集成 可以参考这篇博客:Sign in with Apple
3.1.3 iOS 13 DeviceToken有变化
NSString *dt = [deviceToken description];
dt = [dt stringByReplacingOccurrencesOfString: @”<” withString: @””];
dt = [dt stringByReplacingOccurrencesOfString: @”>” withString: @””];
dt = [dt stringByReplacingOccurrencesOfString: @” ” withString: @””];
这段代码运行在 iOS 13 上已经无法获取到准确的DeviceToken字符串了,iOS 13 通过[deviceToken description]获取到的内容已经变了。

解决方案
– (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
if (![deviceToken isKindOfClass:[NSData class]]) return;
const unsigned *tokenBytes = [deviceToken bytes];
NSString *hexToken = [NSString stringWithFormat:@”%08x%08x%08x%08x%08x%08x%08x%08x”,
ntohl(tokenBytes[0]), ntohl(tokenBytes[1]), ntohl(tokenBytes[2]),
ntohl(tokenBytes[3]), ntohl(tokenBytes[4]), ntohl(tokenBytes[5]),
ntohl(tokenBytes[6]), ntohl(tokenBytes[7])];
NSLog(@”deviceToken:%@”,hexToken);
}

3.1.4 MPMoviePlayerController 在iOS 13已经不能用了
‘MPMoviePlayerController is no longer available. Use AVPlayerViewController in AVKit.’

解决方案:
既然不能再用了,那只能换掉了。替代方案就是AVKit里面的那套播放器。

3.1.5 控制器的 modalPresentationStyle 默认值变了
查阅了下 UIModalPresentationStyle枚举定义,赫然发现iOS 13新加了一个枚举值:

typedef NS_ENUM(NSInteger, UIModalPresentationStyle) {
UIModalPresentationFullScreen = 0,
UIModalPresentationPageSheet API_AVAILABLE(ios(3.2)) API_UNAVAILABLE(tvos),
UIModalPresentationFormSheet API_AVAILABLE(ios(3.2)) API_UNAVAILABLE(tvos),
UIModalPresentationCurrentContext API_AVAILABLE(ios(3.2)),
UIModalPresentationCustom API_AVAILABLE(ios(7.0)),
UIModalPresentationOverFullScreen API_AVAILABLE(ios(8.0)),
UIModalPresentationOverCurrentContext API_AVAILABLE(ios(8.0)),
UIModalPresentationPopover API_AVAILABLE(ios(8.0)) API_UNAVAILABLE(tvos),
UIModalPresentationBlurOverFullScreen API_AVAILABLE(tvos(11.0)) API_UNAVAILABLE(ios) API_UNAVAILABLE(watchos),
UIModalPresentationNone API_AVAILABLE(ios(7.0)) = -1,
UIModalPresentationAutomatic API_AVAILABLE(ios(13.0)) = -2,
};

解决方案
如果你完全接受苹果的这个默认效果,那就不需要去修改任何代码。
如果,你原来就比较细心,已经设置了modalPresentationStyle的值,那你也不会有这个影响。
对于想要找回原来默认交互的同学,直接设置如下即可:
self.modalPresentationStyle = UIModalPresentationOverFullScreen;
3.1.6 UITextField 的私有属性 _placeholderLabel 被禁止访问了
IOS 13下调用下面代码会导致闪退
[self.textField setValue:self.placeholderColor forKeyPath:@”_placeholderLabel.textColor”];
1
打印错误信息如下:

‘Access to UITextField’s _placeholderLabel ivar is prohibited. This is an application bug’

解决方案:
UITextField有个attributedPlaceholder的属性,我们可以自定义这个富文本来达到我们需要的结果。

NSMutableAttributedString *placeholderString = [[NSMutableAttributedString alloc] initWithString:placeholder attributes:@{NSForegroundColorAttributeName : self.placeholderColor}];
_textField.attributedPlaceholder = placeholderString;

iOS 13 通过 KVC 方式修改私有属性,有 Crash 风险,谨慎使用!并不是所有KVC都会Crash,要尝试!

3.1.7 UISearchBar显示问题
SearchBar的高度只有1px
升级到iOS13,UISearchController上的SearchBar显示异常,查看后发现对应的高度只有1px,目前没找到具体导致的原因,
解决办法是: 使用KVO监听frame值变化后设置去应该显示的高度
黑线处理crash
之前为了处理搜索框的黑线问题会遍历后删除UISearchBarBackground,在iOS13会导致UI渲染失败crash;
解决办法是: 设置UISearchBarBackground的layer.contents为nil
TabBar红点偏移
如果之前有通过TabBar上图片位置来设置红点位置,在iOS13上会发现显示位置都在*左边去了。遍历UITabBarButton的subViews发现只有在TabBar选中状态下才能取到UITabBarSwappableImageView,
解决办法是: 修改为通过UITabBarButton的位置来设置红点的frame

3.1.8 黑暗模式 Dark Mode
Apps on iOS 13 are expected to support dark mode
Use system colors and materials
Create your own dynamic colors and images Leverage flexible infrastructure

UI 需要出一套新交互

在iOS13,为UIViewController和UIView扩展了一个新的API-overrideUserInterfaceStyle,使用方法,官方文档大致是这么说的:
通过设置overrideUserInterfaceStyle属性以使该视图及其子视图具有特定的UIUserInterfaceStyle。但如果想要获取当前的UIUserInterfaceStyle,需要改用traitCollection.userInterfaceStyle。
尽可能使用UIViewController上的overrideUserInterfaceStyle属性。仅在以下时间使用此属性:
(1) 在单个视图或小视图层次结构上局部使用特定样式。
(2) 您希望在整个UIWindow及其视图控制器和模态弹出的ViewController上使用特定样式,且不希望强制更改整个应用程序具有样式。 (如果您确实希望整个应用程序具有某种样式,请不要使用它,而是在Info.plist中设置UIUserInterfaceStyle键。)
当设置在普通的UIView上时:
此属性仅影响此视图及其子视图的特征。
它不会影响任何视图控制器或其他视图控制器的子视图。
在UIWindow上设置时:
此属性会影响rootViewController,从而影响整个视图控制器和视图层次结构。
它还会影响该window模态出来的界面。
由此可见,overrideUserInterfaceStyle不仅会影响自己,还会影响自己的子视图,换做window就会影响整个window中的所有视图及视图控制器,包括模态跳转出来的视图控制器。而且,文档中也特别强调了,你可以设置整个应用程序只是用某种样式,具体方法可以通过代码,也可以通过info.plist配置键User Interface Style,对应的Value为Light/Dark。

if #available(iOS 13.0, *) {
window?.overrideUserInterfaceStyle = .light;
}

%title插图%num

3.1.8.1 适配黑暗模式
适配Dark 模式主要从这几个方面:
模拟器调试(simulator debug)
图片(assets)
颜色(color)
状态栏(status bar)
3.1.8.1.1 模拟器调试
运行项目,点击Xcode底部调试栏中Environment Overrides.
开启Interface Style,就可以切换了。如下图:

%title插图%num

%title插图%num
3.1.8.1.2 图片适配
图片适配,主要是我们本地图片资源适配,网络图片的话,还是比较繁琐。
图片适配比较方便的就是通过Assets.xcassets进行图片管理:
添加一个image set,重命名如”adaptimage”,选中该image set;
选中Attributes Inspector;
将Appearances由”None”改为”Any,Dark”;
不同模式下设置不同图片即可,mode 改变会自动选择不同的图片

%title插图%num

当然图片适配,你也可以直接使用判断当前系统mode的方式进行区分,就我个人而言不是很喜欢这种方式,因为还需要监听系统模式的变化,重写UITraitEnvironment协议方法traitCollectionDidChange(_:),我们先看下协议方法:
/** Trait environments expose a trait collection that describes their environment. */
public protocol UITraitEnvironment : NSObjectProtocol {

@available(iOS 8.0, *)
var traitCollection: UITraitCollection { get }

/** To be overridden as needed to provide custom behavior when the environment’s traits change. */
@available(iOS 8.0, *)
func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?)
}

*后,我们只需要在改变系统mode的时候,重写代理:
func updateImageView() {
let image = traitCollection.userInterfaceStyle == .light ? UIImage(named: “dark-ios”) : UIImage(named: “white-ios”)
imageView.image = image
}

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
updateImageView()
}

3.1.8.1.3 颜色适配
颜色适配有三种方式:
方法一:是通过Assets.xcassets添加一个Color Set,目前系统支持≥iOS11.0
extension UIColor {
@available(iOS 11.0, *)
public /*not inherited*/ init?(named name: String) // load from main bundle

@available(iOS 11.0, *)
public /*not inherited*/ init?(named name: String, in bundle: Bundle?, compatibleWith traitCollection: UITraitCollection?)
}

%title插图%num
方法二:代码创建动态颜色init(dynamicProvider: @escaping (UITraitCollection) -> UIColor),目前系统支持≥iOS 13.0
// 方法二
let titleColor = UIColor.init(dynamicProvider: { (trait) -> UIColor in
return trait.userInterfaceStyle == .light ? UIColor.black : UIColor.white
})
btn.setTitleColor(titleColor, for: .normal)

方法三:像图片一样,监听模式转变,重写traitCollectionDidChange(_:)方法,不推荐这种。
3.1.8.1.4 状态栏适配
目前状态栏也增加了一种模式,由之前的两种,变成了三种, 其中default由之前的黑色内容,变成了会根据系统模式,自动选择当前展示lightContent还是darkContent。
public enum UIStatusBarStyle : Int {
case `default` // Automatically chooses light or dark content based on the user interface style

@available(iOS 7.0, *)
case lightContent // Light content, for use on dark backgrounds

@available(iOS 13.0, *)
case darkContent // Dark content, for use on light backgrounds
}

我们在使用的时候,就可以重写preferredStatusBarStyle的get方法:
override var preferredStatusBarStyle: UIStatusBarStyle{
get{
return .lightContent
}
}

3.1.9 模态弹出默认交互改变
iOS 13 的 presentViewController 默认有视差效果,模态出来的界面现在默认都下滑返回。 一些页面必须要点确认才能消失的,需要适配。如果项目中页面高度全部是屏幕尺寸,那么多出来的导航高度会出现问题。

/*
Defines the presentation style that will be used for this view controller when it is presented modally. Set this property on the view controller to be presented, not the presenter.
If this property has been set to UIModalPresentationAutomatic, reading it will always return a concrete presentation style. By default UIViewController resolves UIModalPresentationAutomatic to UIModalPresentationPageSheet, but other system-provided view controllers may resolve UIModalPresentationAutomatic to other concrete presentation styles.
Defaults to UIModalPresentationAutomatic on iOS starting in iOS 13.0, and UIModalPresentationFullScreen on previous versions. Defaults to UIModalPresentationFullScreen on all other platforms.
*/
@property(nonatomic,assign) UIModalPresentationStyle modalPresentationStyle API_AVAILABLE(ios(3.2));

解决方案:
// Swift
self.modalPresentationStyle = .fullScreen

// Objective-C
self.modalPresentationStyle = UIModalPresentationFullScreen;

3.1.10 App启动过程中,部分View可能无法实时获取到frame
可能是为了优化启动速度,App 启动过程中,部分View可能无法实时获取到正确的frame

解决方案
// 只有等执行完 UIViewController 的 viewDidAppear 方法以后,才能获取到正确的值,在viewDidLoad等地方 frame Size 为 0,例如:
[[UIApplication sharedApplication] statusBarFrame];

iOS13适配:UIWindow窗口和导航控制器

文章目录
适配方案——不支持多场景窗口
适配方案——支持多场景窗口
同时兼容iOS13和iOS12及以下

这次iOS13的发布,其改动步子有点大了,尤其是是其多场景窗口(多任务)已经颠覆了老应用的设计基础了—-数据的协同共享处理机制(本文不谈,哈哈)
这里记录下一些界面层面的适配体会:
如果是Xcode 10及以下创建的老项目,用Xcode 11打开,老项目基本能正常运行。但是如果用Xcode 11创建新项目,还按照老项目思路写代码就会有坑了。

用Xcode 11创建一个Single View App项目,会多生成一些文件和代码

多了SceneDelegate代理
Info.plist里面多了Application Scene Manifest配置
多出来的这些文件和代码,影响*直观的是多场景窗口和导航控制器。

适配方案——不支持多场景窗口
这种适配方案*简单。
将多出来的文件和代码删除就好了

删除SceneDelegate代理文件 (可选)
删除 Info.plist里面的Application Scene Manifest配置(一定要删除)
删除 AppDelegate代理的两个方法:
application:configurationForConnectingSceneSession:options:
application: didDiscardSceneSessions:
这两个方法一定要删除,否则使用纯代码创建的Window和导航控制器UINavigationController不会生效。
适配方案——支持多场景窗口
先说我遇到的一些现象。
尽管我不会为每个应用自定义窗口和导航,但我我依然会使用纯代码创建UIWindow和UINavigationController,具体如下

//AppDelegate
– (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
//在Xcode11创建的项目中,需要自行给AppDelegate添加属性window
//自定义Window
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
//自定义导航控制器
UINavigationController *rootNavgationController = [[UINavigationController alloc] initWithRootViewController:[ViewController new]];
//导航控制是rootViewController
self.window.rootViewController = rootNavgationController;
//现实Window
[self.window makeKeyAndVisible];
return YES;
}

 

Xcode 11创建的项目中,写入上述代码运行App,结果发现这部分代码虽然执行了,但是通过UIViewController的self.navigationController获取的导航竟然是nil。
从现象反推过程,既然代码执行了,那么很大可能是self.window没有显示在屏幕上。
查看iOS13下UIWindow的定义,有这么一条

// If nil, window will not appear on any screen.
// changing the UIWindowScene may be an expensive operation and should not be done in performance-sensitive code
@property(nullable, nonatomic, weak) UIWindowScene *windowScene API_AVAILABLE(ios(13.0));

如果UIWindow的属性windowScene为nil,那么这个UIWindow则不会显示在任何屏幕上。

既然问题找到了,那么解决起来也就容易了,一番断点调试跟踪代码后,加单的解决办法是在SceneDelegate的方法scene:willConnectToSession:options:中创建UIWindow和UINavigationController

//SceneDelegate
– (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions {
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
self.window.windowScene = (UIWindowScene*)scene;
UINavigationController *rootNavgationController = [[UINavigationController alloc] initWithRootViewController:[ViewController new]];
self.window.rootViewController = rootNavgationController;
[self.window makeKeyAndVisible];
}

 

同时兼容iOS13和iOS12及以下
多场景窗口、SceneDelegate等只有在iOS13才可以,若要考虑iOS12及以下的运行环境,那么上述解决方案就要考虑环境版本匹配了,完整代码如下

AppDelegate部分代码

//AppDelegate.h

@interface AppDelegate : UIResponder <UIApplicationDelegate>

@property (strong, nonatomic) UIWindow *window;

@end

//AppDelegate.m
@implementation AppDelegate
– (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
if (@available(iOS 13,*)) {
return YES;
} else {
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
UINavigationController *rootNavgationController = [[UINavigationController alloc] initWithRootViewController:[ViewController new]];
self.window.rootViewController = rootNavgationController;
[self.window makeKeyAndVisible];
return YES;
}
}

#pragma mark – UISceneSession lifecycle

– (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options {
return [[UISceneConfiguration alloc] initWithName:@”Default Configuration” sessionRole:connectingSceneSession.role];
}

@end

SceneDelegate部分代码

//SceneDelegate.h
@interface SceneDelegate : UIResponder <UIWindowSceneDelegate>

@property (strong, nonatomic) UIWindow * window;

@end

//SceneDelegate.m
@implementation SceneDelegate
– (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions {

self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
self.window.windowScene = (UIWindowScene*)scene;
UINavigationController *rootNavgationController = [[UINavigationController alloc] initWithRootViewController:[ViewController new]];
self.window.rootViewController = rootNavgationController;
[self.window makeKeyAndVisible];
}

@end

Android 蓝牙开发模块详解 (含 demo)

1、简介
实现一下功能:
(1)扫描其他蓝牙设备
(2)查询本地蓝牙适配,用于配对蓝牙设备
(3)建立 RFCOMM 信道
(4)通过服务发现连接其他设备
(5)数据通信
(6)管理多个连接

%title插图%num

2、基本功能简介
1)、设置权限
<uses-permission android:name=”android.permission.BLUETOOTH”/>
<uses-permission android:name=”android.permission.BLUETOOTH_ADMIN”/>
1
2
2)开启蓝牙
//启动蓝牙
private void enableBluetooh() {
BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); //获取蓝牙适配器
if (bluetoothAdapter == null) {
Log.i(TAG,”设备不支持蓝牙功能”);
return;
}

if (bluetoothAdapter.isEnabled()) { //如果蓝牙没有打开
Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableIntent,100);
}
}

public void onActivityResult(int requestCode,int resultCode,Intent data) {
if (requestCode == 100) {
if (resultCode == RESULT_OK) {
Log.i(TAG,”蓝牙开启成功”);
} else {
Log.i(TAG,”蓝牙开启失败”);
}
}
}

3)、发现蓝牙设备
(1)使本机蓝牙处于可见状态,会被其他蓝牙设备搜索

//是自身可以被发现
private void ensureDiscoverable() {
if(bluetoothAdapter.getScanMode() != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
Intent discoverableIntent = new Intent (BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION,300); //设置本机蓝牙在 300 秒之内可以被 搜索
startActivity(discoverableIntent);
}
}

(2)查找已经配对的设备,及以前已经配对过的设备

//查找已经配对过的设备,及已经配对过的设备
private void pairedDevices() {
Set<BluetoothDevice> pairedDevices = bluetoothAdapter.getBondedDevices();
if (pairedDevices.size() > 0) {
for (BluetoothDevice devices : pairedDevices) {
Log.i(TAG ,”device name: ” + devices.getName() + “\n” +
“device address: ” + devices.getAddress());
}
} else {
Log.i(TAG,”没有找到已匹配的设备”);
}
}

(3)搜索设备

//搜索新的蓝牙设备 //先注册一个 广播 来获取搜索结果
private void searchDevices() {
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND); //当一个设备被发现时 ,调用广播
this.registerReceiver(mReceiver,filter);

//当搜索结束后,调用 广播
filter = new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
this.registerReceiver(mReceiver,filter);

bluetoothAdapter.startDiscovery();// 用这个方法来搜索设备
}

private BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (BluetoothDevice.ACTION_FOUND.equals(action)) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
//已经配对过的跳过
if (device.getBondState() != BluetoothDevice.BOND_BONDED) {
Log.i(TAG,”device name: ” + device.getName() + ” device address: ” + device.getAddress());
}
} else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) {
//搜索结束
Log.i(TAG,”设备搜索结束”);
}
}
};

4、建立连接
当查找到蓝牙设备之后,接下来建立连接,本机可以作为一个服务端,来接收其他设备的连接。

private static final UUID MY_UUID = UUID.fromString(“fa87c0d0-afac-11de-8a39-0800200c9a66″);
private String NAME_INSECURE;
public class AcceptThread extends Thread{
//像一个服务器一样时刻监听是否有连接
private BluetoothServerSocket serverSocket;
public AcceptThread(boolean secure) {
BluetoothServerSocket temp = null;
try {
temp = bluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME_INSECURE,MY_UUID);
} catch (IOException e) {
e.printStackTrace();
}

serverSocket = temp;
}

public void run(){
BluetoothSocket socket = null;
while (true) {
try {
socket = serverSocket.accept();
} catch (IOException e) {
e.printStackTrace();
Log.e(TAG,”accept socket fail”);
break;
}
}

if (socket != null) {
//此时可以做一个数据交换线程 把socket 传进去
}
}

//取消监听
public void cancel() {
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

5、交换数据
当搜索到蓝牙设备之后,接下来就会获取到设备地址,通过地址获取BluetoothDeviced对象,可以将其看作是一个客户端。通过对象。device.createRfcommSocketToServiceRecord(MY_UUID) 同一个 UUID 可与服务器建立连接 获取另一个 socket 对象。

//另一个设备连接本机 , 相当于客户端
private class ConnectThread extends Thread {
private BluetoothSocket socket;
private BluetoothDevice device;

public ConnectThread(BluetoothDevice device, boolean secure){
this.device = device;
BluetoothSocket tmp = null;
try {
tmp = device.createInsecureRfcommSocketToServiceRecord(MY_UUID);
} catch (IOException e) {
e.printStackTrace();
}
}

public void run(){
bluetoothAdapter.cancelDiscovery(); //取消设备查找

try {
socket.connect();
} catch (IOException e) {
e.printStackTrace();
try {
socket.close(); //连接失败
} catch (IOException e1) {
e1.printStackTrace();
}
}
//此时可以创建一个线程 把 socket 传进去
}

public void cancel () {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

6、建立数据通信线程
接下来 我们读取通信数据

private class ConnectedThread extends Thread{
private BluetoothSocket socket;
private InputStream inputStream;
private OutputStream outputStream;

public ConnectedThread(BluetoothSocket socket) {
this.socket = socket;

//获取输入输出流
try {
inputStream = socket.getInputStream();
outputStream = socket.getOutputStream();
} catch (IOException e) {
e.printStackTrace();
}
}

public void run() {
byte [] buff = new byte [1024];
int len = 0;

//读数据需要不断监听 ,写 不需要

while (true) {
try {
len = inputStream.read(buff);
//获取到的数据 长度
} catch (IOException e) {
e.printStackTrace();
start();//重新启动服务
}
}
}

public void write(byte[] buffer) {
try {
outputStream.write(buffer);
} catch (IOException e) {
e.printStackTrace();
}
}

public void cancel() {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

6、实例练习
6.1、代码结构

6.2 、 xml 文档
<?xml version=”1.0″ encoding=”utf-8″?>
<LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:app=”http://schemas.android.com/apk/res-auto”
xmlns:tools=”http://schemas.android.com/tools”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:orientation=”horizontal”
tools:context=”.MainActivity”>

<ScrollView
android:layout_width=”match_parent”
android:layout_height=”match_parent”>

<LinearLayout
android:layout_width=”match_parent”
android:layout_height=”match_parent”>
<LinearLayout
android:orientation=”vertical”
android:layout_width=”match_parent”
android:layout_weight=”1″
android:layout_height=”match_parent”>
<Button
android:layout_width=”match_parent”
android:layout_height=”60dp”
android:id=”@+id/bt_op_id”
android:textSize=”30dp”
android:text=”打开蓝牙”/>

<Button
android:layout_width=”match_parent”
android:layout_height=”60dp”
android:id=”@+id/bt_close_id”
android:textSize=”30dp”
android:text=”关闭蓝牙”/>

<Button
android:layout_width=”match_parent”
android:layout_height=”60dp”
android:id=”@+id/bt_discovered_id”
android:textSize=”30dp”
android:text=”允许搜索”/>

<Button
android:layout_width=”match_parent”
android:layout_height=”60dp”
android:id=”@+id/bt_search_id”
android:textSize=”30dp”
android:text=”开始搜索”/>

<Button
android:layout_width=”match_parent”
android:layout_height=”60dp”
android:id=”@+id/bt_paired_id”
android:textSize=”30dp”
android:text=”已经配对”/>

<Button
android:layout_width=”match_parent”
android:layout_height=”60dp”
android:id=”@+id/bt_pair_id”
android:textSize=”30dp”
android:text=”蓝牙配对”/>

<Button
android:layout_width=”match_parent”
android:layout_height=”60dp”
android:id=”@+id/bt_unPair_id”
android:textSize=”30dp”
android:text=”取消配对”/>

<Button
android:layout_width=”match_parent”
android:layout_height=”60dp”
android:id=”@+id/bt_server_id”
android:textSize=”30dp”
android:text=”开启服务”/>

</LinearLayout>

<LinearLayout
android:orientation=”vertical”
android:layout_weight=”1″
android:layout_width=”match_parent”
android:layout_height=”match_parent”>

<Button
android:layout_width=”match_parent”
android:layout_height=”60dp”
android:id=”@+id/bt_serverUnconnect_id”
android:textSize=”30dp”
android:text=”服务断开”/>

<Button
android:layout_width=”match_parent”
android:layout_height=”60dp”
android:id=”@+id/bt_connect_id”
android:textSize=”30dp”
android:text=”蓝牙连接”/>

<Button
android:layout_width=”match_parent”
android:layout_height=”60dp”
android:id=”@+id/bt_unConnect_id”
android:textSize=”30dp”
android:text=”取消连接”/>

<Button
android:layout_width=”match_parent”
android:layout_height=”60dp”
android:id=”@+id/bt_clientRev_id”
android:textSize=”30dp”
android:text=”客户端收”/>

<Button
android:layout_width=”match_parent”
android:layout_height=”60dp”
android:id=”@+id/bt_clientSend_id”
android:textSize=”30dp”
android:text=”客户端发”/>

<Button
android:layout_width=”match_parent”
android:layout_height=”60dp”
android:id=”@+id/bt_serverRev_id”
android:textSize=”30dp”
android:text=”服务端收”/>

<Button
android:layout_width=”match_parent”
android:layout_height=”60dp”
android:id=”@+id/bt_ServerSend_id”
android:textSize=”30dp”
android:text=”服务端发”/>

</LinearLayout>
</LinearLayout>

</ScrollView>

</LinearLayout>

6.3、添加权限
<uses-permission android:name=”android.permission.BLUETOOTH”/>
<uses-permission android:name=”android.permission.BLUETOOTH_ADMIN”/>
<!–模糊定位权限,仅作用于6.0+–>
<uses-permission android:name=”android.permission.ACCESS_COARSE_LOCATION” />
<!–精准定位权限,仅作用于6.0+–>
<uses-permission android:name=”android.permission.ACCESS_FINE_LOCATION” />

6.4 功能代码
package myapplication.lum.com.mybluetoothapplication;

import android.Manifest;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothServerSocket;
import android.bluetooth.BluetoothSocket;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.location.LocationManager;
import android.os.Build;
import android.provider.Settings;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.UUID;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private String TAG = “MainActivity lum: “;
private BluetoothAdapter mbluetoothAdapter;
private Button buttonOpen,buttonClose,buttonDiscover,buttonSearch,buttonPaired, buttonPair,
buttonUnpair,buttonServer,buttonServerUnconnect, buttonConnect,buttonUnconnect,
buttonClientRev,buttonClientSend,buttonServRev,buttonServSend;

private static final int REQUEST_ENABLE = 0x1; //请求能够打开蓝牙
private static final int REQUEST_CODE_PERMISSION_LOCATION = 0x2; //权限请求

private static final UUID MY_UUID = UUID.fromString(“fa87c0d0-afac-11de-8a39-0800200c9a66″);
private BluetoothDevice bluetoothDevice; //我们将要连接配对的设备
private BluetoothSocket bluetoothSocket; //蓝牙配对客户端的 socket
private BluetoothServerSocket serverSocket; //服务端的 socket
private BluetoothSocket serverClientSocket;//服务端接受的 客户端socket

private AcceptThread acceptThread; //服务端线程
private ClientThread clientThread ; //客户端线程

private OutputStream outputStream ; //输出流
private InputStream inputStream ; //输入流

private Byte[] buffer ;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

checkPermissions();
checkBLEFeature();
initView();
}

/**
* 检查BLE是否起作用
*/
private void checkBLEFeature() {
//判断是否支持蓝牙4.0
if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
Log.i(TAG,”设备支持BLE”);
} else {
Log.i(TAG,”设备不支持BLE”);
}
}

private void initView(){
mbluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); //获取默认的蓝牙适配器

//蓝牙搜索需要注册
IntentFilter filter = new IntentFilter();
filter.addAction(BluetoothDevice.ACTION_FOUND); //蓝牙搜索
filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED); //蓝牙搜索结束
filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); //蓝牙设备状态改变
registerReceiver(mReceiver,filter);

buttonOpen = (Button) findViewById(R.id.bt_op_id);
buttonClose = (Button) findViewById(R.id.bt_close_id);
buttonDiscover = (Button) findViewById(R.id.bt_discovered_id);
buttonSearch = (Button) findViewById(R.id.bt_search_id);
buttonPaired = (Button) findViewById(R.id.bt_paired_id);
buttonPair = (Button) findViewById(R.id.bt_pair_id);
buttonUnpair = (Button) findViewById(R.id.bt_unPair_id);
buttonServer = (Button) findViewById(R.id.bt_server_id);
buttonServerUnconnect = (Button) findViewById(R.id.bt_unConnect_id);
buttonConnect = (Button) findViewById(R.id.bt_connect_id);
buttonUnconnect = (Button) findViewById(R.id.bt_unConnect_id);
buttonClientRev = (Button) findViewById(R.id.bt_clientRev_id);
buttonClientSend = (Button) findViewById(R.id.bt_clientSend_id);
buttonServRev = (Button) findViewById(R.id.bt_serverRev_id);
buttonServSend = (Button) findViewById(R.id.bt_ServerSend_id);

buttonOpen.setOnClickListener(this);
buttonClose.setOnClickListener(this);
buttonDiscover.setOnClickListener(this);
buttonSearch.setOnClickListener(this);
buttonPaired.setOnClickListener(this);
buttonPair.setOnClickListener(this);
buttonUnpair.setOnClickListener(this);
buttonServer.setOnClickListener(this);
buttonServerUnconnect.setOnClickListener(this);
buttonConnect.setOnClickListener(this);
buttonUnconnect.setOnClickListener(this);
buttonClientRev.setOnClickListener(this);
buttonClientSend.setOnClickListener(this);
buttonServRev.setOnClickListener(this);
buttonServSend.setOnClickListener(this);

}

@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.bt_op_id:
enableBluetooh(); //打开蓝牙
break;
case R.id.bt_close_id:
closeBluetooh();//关闭蓝牙
break;
case R.id.bt_discovered_id:
disCoveredEnable();//允许被搜索
break;
case R.id.bt_paired_id:
pairedConnect();//已经配对的设备
break;
case R.id.bt_search_id:
mbluetoothAdapter.startDiscovery();//搜索设备
Log.i(TAG,”开始进行搜索”);
break;
case R.id.bt_pair_id: //蓝牙配对
paireDevice();
break;
case R.id.bt_unPair_id:
unPairDevices();//取消蓝牙配对
break;
case R.id.bt_server_id:
serverOpen();//打开服务端
break;
case R.id.bt_serverUnconnect_id: //服务端断开连接
serverUnconnect();//服务端断开连接
break;
case R.id.bt_connect_id:
clientConnectDevices();//蓝牙连接
break;
case R.id.bt_unConnect_id:
clientUnConnectDevices();//取消蓝牙连接
break;
case R.id.bt_clientRev_id:
clientReadRev();//客户端接收数据
break;
case R.id.bt_clientSend_id:
clientSendData();//客户端发送数据
break;
case R.id.bt_serverRev_id:
serverReadRev();//服务端接收数据
break;
case R.id.bt_ServerSend_id:
serverSendData();//服务端发送数据
break;
default:
break;
}
}

//服务端发送数据
private void serverSendData() {
sendData(“服务端发送数据”.getBytes(),serverClientSocket);
Log.i(TAG,”服务端发送数据”);
}

//服务端 接收数据
private void serverReadRev() {
ReadReceiveThread clientReadThread = new ReadReceiveThread(serverClientSocket);
clientReadThread.start();
}

//客户端发送数据
private void clientSendData() {
sendData(“客户端发送数据”.getBytes(),bluetoothSocket);
Log.i(TAG,”客户端发送数据”);
}

//客户端接收数据
private void clientReadRev() {
ReadReceiveThread clientReadThread = new ReadReceiveThread(bluetoothSocket);
clientReadThread.start();
}

//发送数据
public void sendData(byte[] bytStr,BluetoothSocket socket){
try {
if (outputStream == null) {
outputStream = socket.getOutputStream();
}
outputStream.write(bytStr);
Log.i(TAG,”发送的数据是: ” + new String(bytStr));
} catch (IOException e) {
e.printStackTrace();
Log.e(TAG,”发送数据错误”);
}
}

// 取数据线程
private class ReadReceiveThread extends Thread {

private BluetoothSocket socket ;

public ReadReceiveThread(BluetoothSocket bluetoothSocket) {
socket = bluetoothSocket;
}
public void run() {
byte[] buffer = new byte[1024];
int bytes;

try {

Log.i(TAG,”is socket connect: ” + socket.isConnected() + ” socket: ” + socket );
if (inputStream == null) {
inputStream = socket.getInputStream();
}

while (true) {
if ((bytes = inputStream.read(buffer)) > 0) {
byte[] buf_data = new byte[bytes];
for (int i = 0; i < bytes; i++) {
buf_data[i] = buffer[i];
}
String str = new String(buf_data);

Log.i(TAG,”接收的数据是: ” + str);
}
}
} catch (IOException e1) {
e1.printStackTrace();
Log.e(TAG,”接收数据错误”);
} finally {
try {
inputStream.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}

}
}

//服务端断开连接
private void serverUnconnect() {
acceptThread.cancel();
}

//打开服务端
private void serverOpen() {
acceptThread = new AcceptThread();
acceptThread.start(); //开启服务线程
}

//开启服务端线程 接收 客户端连接
private class AcceptThread extends Thread{

public AcceptThread () {
BluetoothServerSocket temp = null;

try {
temp = mbluetoothAdapter.listenUsingRfcommWithServiceRecord(“BLUE_TEST”,MY_UUID);
} catch (IOException e) {
e.printStackTrace();
Log.i(TAG,”创建服务器失败”);
}

serverSocket = temp;
Log.i(TAG,”server socket 创建成功: ” + serverSocket);
}

public void run(){
BluetoothSocket socket = null;

while (true) {
try {
socket = serverSocket.accept();
if (socket != null) {
serverClientSocket = socket;
Log.i(TAG,”服务端连接 socket: ” + socket);
}
} catch (IOException e) {
e.printStackTrace();
Log.e(TAG,”accept() fail”);
break;
}
}

}

public void cancel(){
try {
if (serverSocket != null) {
serverSocket.close();
serverSocket = null;
Log.i(TAG,”服务端取消连接”);
}
} catch (IOException e) {
e.printStackTrace();
}
;
}
}

/**
* 检查权限
*/
private void checkPermissions() {
String[] permissions = {Manifest.permission.ACCESS_FINE_LOCATION};
List<String> permissionDeniedList = new ArrayList<>();
for (String permission : permissions) {
int permissionCheck = ContextCompat.checkSelfPermission(this, permission);
if (permissionCheck == PackageManager.PERMISSION_GRANTED) {
onPermissionGranted(permission);
} else {
permissionDeniedList.add(permission);
}
}
if (!permissionDeniedList.isEmpty()) {
String[] deniedPermissions = permissionDeniedList.toArray(new String[permissionDeniedList.size()]);
ActivityCompat.requestPermissions(this, deniedPermissions, REQUEST_CODE_PERMISSION_LOCATION);
}
}

/**
* 权限回调
* @param requestCode
* @param permissions
* @param grantResults
*/
@Override
public final void onRequestPermissionsResult(int requestCode,
@NonNull String[] permissions,
@NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode) {
case REQUEST_CODE_PERMISSION_LOCATION:
if (grantResults.length > 0) {
for (int i = 0; i < grantResults.length; i++) {
if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
onPermissionGranted(permissions[i]);
}
}
}
break;
}
}

/**
* 开启GPS
* @param permission
*/
private void onPermissionGranted(String permission) {
switch (permission) {
case Manifest.permission.ACCESS_FINE_LOCATION:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !checkGPSIsOpen()) {
new AlertDialog.Builder(this)
.setTitle(“提示”)
.setMessage(“当前手机扫描蓝牙需要打开定位功能。”)
.setNegativeButton(“取消”,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
finish();
}
})
.setPositiveButton(“前往设置”,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
startActivity(intent);
}
})

.setCancelable(false)
.show();
} else {
//GPS已经开启了
}
break;
}
}

/**
* 检查GPS是否打开
* @return
*/
private boolean checkGPSIsOpen() {
LocationManager locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE);
if (locationManager == null)
return false;
return locationManager.isProviderEnabled(android.location.LocationManager.GPS_PROVIDER);
}

//取消蓝牙连接
private void clientUnConnectDevices() {
clientThread.cancel();
}

//蓝牙连接
private void clientConnectDevices() {
clientThread = new ClientThread();
clientThread.start();
}

//开启服务端线程 接收 客户端连接
private class ClientThread extends Thread{
public ClientThread () {
BluetoothSocket temp = null;
//配对之前把扫描关闭
if (mbluetoothAdapter.isDiscovering()){
mbluetoothAdapter.cancelDiscovery();
}
try {
temp = bluetoothDevice.createInsecureRfcommSocketToServiceRecord(MY_UUID);
} catch (IOException e) {
e.printStackTrace();
Log.i(TAG,”连接失败”);
}

bluetoothSocket = temp; //把配对时 反射获取的 获取的socket 赋值
Log.i(TAG,”客户端 配对 socket 初始化:” + bluetoothSocket);
}

public void run(){
try {
bluetoothSocket.connect();
Log.i(TAG,”连接设备”);
} catch (IOException e) {
e.printStackTrace();
try {
bluetoothSocket.close();
Log.e(TAG,”设备连接失败”);
} catch (IOException e1) {
e1.printStackTrace();
}
}
}

public void cancel(){
if (bluetoothSocket != null && bluetoothSocket.isConnected()){
try {
bluetoothSocket.close();
bluetoothSocket = null;
Log.i(TAG,”取消设备连接”);
} catch (IOException e) {
e.printStackTrace();
Log.i(TAG,”取消设备连接失败”);
}
}
}
}

//取消蓝牙配对
private void unPairDevices() {
Method method = null;
//配对之前把扫描关闭
if (mbluetoothAdapter.isDiscovering()){
mbluetoothAdapter.cancelDiscovery();
}
try {
method = bluetoothDevice.getClass().getMethod(“removeBond”, (Class[]) null);
method.invoke(bluetoothDevice, (Object[]) null);
Log.i(TAG,”取消蓝牙配对”);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}

}

//配对蓝牙设备
private void paireDevice() {

//配对之前把扫描关闭
if (mbluetoothAdapter.isDiscovering()){
mbluetoothAdapter.cancelDiscovery();
}
try {
Method method = bluetoothDevice.getClass().getMethod(“createRfcommSocket”, new Class[] {int.class});
method.invoke(bluetoothDevice, 1);
Log.i(TAG,”配对成功”);
} catch (NoSuchMethodException e) {
e.printStackTrace();
Log.i(TAG,”配对失败”);
} catch (IllegalAccessException e) {
e.printStackTrace();
Log.i(TAG,”配对失败”);
} catch (InvocationTargetException e) {
e.printStackTrace();
Log.i(TAG,”配对失败”);
}

}

//搜索蓝牙 需要进行广播接收 搜索到一个设备 接收到一个广播
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (BluetoothDevice.ACTION_FOUND.equals(action)) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);

if (device.getBondState() == BluetoothDevice.BOND_BONDED) { //搜索蓝牙设备
Log.i(TAG,”搜索到 已经配对的设备; device name: ” + device.getName() + ” device address: ” + device.getAddress());
} else {
Log.i(TAG,”搜索到 没有配对的设备; device name: ” + device.getName() + ” device address: ” + device.getAddress());
if(device.getAddress().equals(“00:20:00:76:45:39″)) { //指定一个蓝牙设备,根据自己设备
bluetoothDevice = device;
Log.i(TAG,”指定配对连接的device”);
}
}
} else if (action.equals(BluetoothDevice.ACTION_BOND_STATE_CHANGED)) { // 更新蓝牙设备的绑定状态
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if (device.getBondState() == BluetoothDevice.BOND_BONDING) {
Log.i(TAG,”正在配对 device name: ” + device.getName() + ” device address: ” + device.getAddress() + ” devices uuid: ” + device.getUuids());
} else if (device.getBondState() == BluetoothDevice.BOND_BONDED) {
Log.i(TAG,”完成配对 device name: ” + device.getName() + ” device address: ” + device.getAddress() + ” devices uuid: ” + device.getUuids());
} else if (device.getBondState() == BluetoothDevice.BOND_NONE) {
Log.i(TAG,”取消配对 device name: ” + device.getName() + ” device address: ” + device.getAddress() + ” devices uuid: ” + device.getUuids());
}
} else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) { //搜索结束
//搜索结束
Log.i(TAG,”设备搜索结束”);
}
}
};

//已经配对的设备
private void pairedConnect() {
Set<BluetoothDevice> pairedDevices = mbluetoothAdapter.getBondedDevices();
if (pairedDevices.size() > 0) {
for (BluetoothDevice devices : pairedDevices) {
Log.i(TAG ,”device name: ” + devices.getName() +
” device address: ” + devices.getAddress());
}
} else {
Log.i(TAG,”没有找到已匹配的设备”);
}

}

//允许蓝牙被搜索
private void disCoveredEnable() {
if(mbluetoothAdapter.getScanMode() != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
Intent discoverableIntent = new Intent (BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION,300); //设置本机蓝牙在 300 秒之内可以被 搜索
startActivity(discoverableIntent);
Log.i(TAG,”设置蓝牙被搜索模式”);
} else {
Log.i(TAG,”蓝牙模式是: ” + mbluetoothAdapter.getScanMode());
}
}

//关闭蓝牙
private void closeBluetooh() {
if (mbluetoothAdapter.isEnabled()) {
mbluetoothAdapter.disable();//关闭蓝牙
Log.i(TAG,”蓝牙关闭”);
}
}

//启动蓝牙
private void enableBluetooh() {

if (mbluetoothAdapter == null) {
Log.i(TAG,”设备不支持蓝牙功能”);
return;
}

if (!mbluetoothAdapter.isEnabled()) { //如果蓝牙没有打开
Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableIntent,REQUEST_ENABLE);

//bluetoothAdapter.enable();//隐式打开蓝牙
} else {
Log.i(TAG,”蓝牙已经打开”);
}
}

public void onActivityResult(int requestCode,int resultCode,Intent data) {
if (requestCode == REQUEST_ENABLE) {
if (resultCode == RESULT_OK) {
Log.i(TAG,”蓝牙开启成功”);
} else {
Log.i(TAG,”蓝牙开启失败”);
}
}
}

public void onDestroy() {
super.onDestroy();
//解除注册
unregisterReceiver(mReceiver);
Log.e(TAG,”解除注册”);
if (mbluetoothAdapter != null)
mbluetoothAdapter.cancelDiscovery(); //取消搜索
}
}

6.5 log 展示

6.6 心得
蓝牙 有 经典蓝牙 和 BLE 之分,本案例是经典蓝牙
2)蓝牙 搜索 配对 连接
蓝牙连接时 需要服务端先启动
4)服务端 客户端 数据传输 使用的不是同一 socket

Android 蓝牙开发基本流程

此例子基于 android demo

 

对于一般的软件开发人员来说,蓝牙是很少用到的,尤其是Android的蓝牙开发,国内的例子很少     Android对于蓝牙开发从2.0版本的sdk才开始支持,而且模拟器不支持,测试至少需要两部手机,所以制约了很多技术人员的开发;
鉴于很多开发人员现在也有蓝牙开发的需求,也为了大家少走些弯路,先将我积攒的一点点在Android蓝牙开发经验与大家分享一下!

首先,要操作蓝牙,先要在AndroidManifest.xml里加入权限

<uses-permissionandroid:name=”android.permission.BLUETOOTH_ADMIN” />

<uses-permissionandroid:name=”android.permission.BLUETOOTH” />

 

 

注:Android 6.0后需要加上

<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" 

这个权限

然后,看下api,Android所有关于蓝牙开发的类都在android.bluetooth包下,如下图,只有8个类


而我们需要用到了就只有几个而已:

    1.BluetoothAdapter 

顾名思义,蓝牙适配器,直到我们建立bluetoothSocket连接之前,都要不断操作它BluetoothAdapter里的方法很多,常用的有以下几个:

      cancelDiscovery() 根据字面意思,是取消发现,也就是说当我们正在搜索设备的时候调用这个方法将不再继续搜索

disable()关闭蓝牙

enable()打开蓝牙,这个方法打开蓝牙不会弹出提示,更多的时候我们需要问下用户是否打开,一下这两行代码同样是打开蓝牙,不过会提示用户

Intemtenabler=new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);

startActivityForResult(enabler,reCode);//同startActivity(enabler);

getAddress()获取本地蓝牙地址

getDefaultAdapter()获取默认BluetoothAdapter,实际上,也只有这一种方法获取BluetoothAdapter

getName()获取本地蓝牙名称

getRemoteDevice(String address)根据蓝牙地址获取远程蓝牙设备

getState()获取本地蓝牙适配器当前状态(感觉可能调试的时候更需要)

isDiscovering()判断当前是否正在查找设备,是返回true

isEnabled()判断蓝牙是否打开,已打开返回true,否则,返回false

listenUsingRfcommWithServiceRecord(String name,UUID uuid)根据名称,UUID创建并返回BluetoothServerSocket,这是创建BluetoothSocket服务器端的*步

startDiscovery()开始搜索,这是搜索的*步

2.BluetoothDevice

看名字就知道,这个类描述了一个蓝牙设备

createRfcommSocketToServiceRecord(UUIDuuid)根据UUID创建并返回一个BluetoothSocket

getState() 蓝牙状态这里要说一下,只有在 BluetoothAdapter.STATE_ON 状态下才可以监听,具体可以看andrid api;

这个方法也是我们获取BluetoothDevice的目的——创建BluetoothSocket
这个类其他的方法,如getAddress(),getName(),同BluetoothAdapter

    3.BluetoothServerSocket

如果去除了Bluetooth相信大家一定再熟悉不过了,既然是Socket,方法就应该都差不多,这个类一种只有三个方法两个重载的accept(),accept(inttimeout)两者的区别在于后面的方法指定了过时时间,需要注意的是,执行这两个方法的时候,直到接收到了客户端的请求(或是过期之后),都会阻塞线程,应该放在新线程里运行!

还有一点需要注意的是,这两个方法都返回一个BluetoothSocket,*后的连接也是服务器端与客户端的两个BluetoothSocket的连接

close()这个就不用说了吧,翻译一下——关闭!

 4.BluetoothSocket

 跟BluetoothServerSocket相对,是客户端一共5个方法,不出意外,都会用到

close(),关闭

connect()连接

getInptuStream()获取输入流

getOutputStream()获取输出流

getRemoteDevice()获取远程设备,这里指的是获取bluetoothSocket指定连接的那个远程蓝牙设备

 

 

1、获取本地蓝牙适配器

BluetoothAdapter
mAdapter= BluetoothAdapter.getDefaultAdapter();

2、打开蓝牙

if(!mAdapter.isEnabled()){

//弹出对话框提示用户是后打开

Intent enabler = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);

startActivityForResult(enabler, REQUEST_ENABLE);

//不做提示,强行打开

// mAdapter.enable();

}

3、搜索设备
1)刚才说过了mAdapter.startDiscovery()

是*步,可以你会发现没有返回的蓝牙设备,怎么知道查找到了呢?向下看,不要急

2)定义BroadcastReceiver,关于BroadcastReceiver不多讲了,不是今天的讨论内容,代码如下

 

  1. BroadcastReceiver mReceiver = new BroadcastReceiver() {
  2. public void onReceive(Context context, Intent intent) {
  3. String action = intent.getAction();
  4. //找到设备
  5. if (BluetoothDevice.ACTION_FOUND.equals(action)) {
  6. BluetoothDevice device = intent
  7. .getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
  8. if (device.getBondState() != BluetoothDevice.BOND_BONDED) {
  9. Log.v(TAG, “find device:” + device.getName()
  10. + device.getAddress());
  11. }
  12. }
  13. //搜索完成
  14. else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED
  15. .equals(action)) {
  16. setTitle(“搜索完成”);
  17. if (mNewDevicesAdapter.getCount() == 0) {
  18. Log.v(TAG, “find over”);
  19. }
  20. }
  21. }
  22. };

这样,没当查找到新设备或是搜索完成,相应的操作都在上段代码的两个if里执行了,不过前提是你要先注册

 

BroadcastReceiver,具体代码如下

 

  1. IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
  2. registerReceiver(mReceiver, filter);
  3. filter = new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
  4. registerReceiver(mReceiver, filter)<span style=“font-family:SimSun;”>;</span>

(这段代码,一般写在onCreate()里..)
4、建立连接,首先Android sdk(2.0以上版本)支持的蓝牙连接是通过BluetoothSocket建立连接(说的不对请高人指正),服务器端(BluetoothServerSocket)和客户端(BluetoothSocket)需指定同样的UUID,才能建立连接,因为建立连接的方法会阻塞线程,所以服务器端和客户端都应启动新线程连接

 

1)服务器端:

//UUID格式一般是”xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx”可到

//http://www.uuidgenerator.com 申请

BluetoothServerSocket serverSocket = mAdapter. listenUsingRfcommWithServiceRecord(serverSocketName,UUID);
serverSocket.accept();

2)客户端:
//还记得我们刚才在BroadcastReceiver获取了BLuetoothDevice么?
BluetoothSocket clienSocket=dcvice. createRfcommSocketToServiceRecord(UUID);
clienSocket.connect();

5、数据传递,通过以上操作,就已经建立的BluetoothSocket连接了,数据传递无非是通过流的形式
1)获取流
inputStream = socket.getInputStream();
outputStream = socket.getOutputStream();
2)写出、读入
这是基础的东西,在这就不多赘述了
终于写完了,这是我这两天的学习经验,希望对有蓝牙需求的朋友有所帮助!另外,之前我们提过

android.bluetooth下有8个类,还有4个类没有用到,那4个类里定义的都是常量,我也没用到它们..

 

*后把我找到的几个蓝牙的例子附在后面,希望从事软件开发,尤其是Android开发的朋友以后多沟通、多分享!

补充一下,使设备能够被搜索

Intent enabler = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);

startActivityForResult(enabler,REQUEST_DISCOVERABLE);

 

demo是包括客户端和服务端;分别放到两个手机上就可以通信;大家改改应该就可以用;

http://download.csdn.net/detail/q610098308/8681065
这个是官方的demo:
http://download.csdn.net/detail/q610098308/8628675

安卓微信8.0 炸弹爆炸了~

新增内容:

上线10周年整的微信,在1月21日迎来了自己的微信8.0版本,距离上次7.0大版本更新,已有两年多。

8.0版本进入页面
相比之前版本,8.0版本变动多达十余项,大部分功能张小龙都在1月19日的“微信之夜”演讲中提前“剧透”,比如他特别喜欢的给好友丢炸弹的功能,把“狗皮膏药”一样的浮窗“收纳”进一个可以快速进入快速切出的页面等。
目前,iPhone客户端均可更新微信,安卓版本尚未完全开放。
小编已经在21日晚更新了新版本,整理了以下你可能感兴趣的新功能:
黄脸表情会动了,真的可以“扔炸弹”
微信8.0版本中的黄脸表情图标更新,部分重新设计,全部加入动态效果。

表情部分重新设计
其中有3个表情比较特殊,“炸弹”表情发送后,收发双方屏幕均会震动,并有爆炸视觉效果,烟花、庆祝等表情包也有聊天页面动态视觉效果。

%title插图%num

“炸弹”表情的爆炸效果
此外,裂开、苦涩、叹气等前不久新加入的表情,也更新了拟人化效果。
新增状态栏,“摸鱼”很内涵
8.0版本中,在“我”Tab栏的微信号下方,新增“状态”,进入之后可选择心情想法、工作学习、活动、休息等多个类目的状态,并添加描述,选择话题,也可以加入图片、位置和公开范围。

我的状态
张小龙此前在演讲中表示,社交的本质是寻找同类,状态,是用来给人看到的,*好还是给同类的人看到。
所以当用户选择某一状态时,页面同时会显示和自己正处于同一“状态”的好友。
浮窗不“浮”,主页下拉显示*近看过内容
更新后微信的消息页面左上角新增了一个“··”按钮,里面收纳之前浮窗中的内容,并且取消此前*多5个的数量限制,形式为每行两项内容,规整排布。

%title插图%num

新浮窗
此前版本微信主页下拉仅显示“用过的小程序”,现在*近看过的直播、未看完的视频、文章等都可以在此处找到。

微信8.0状态视频怎么设置

1、打开微信,进入到“我”界面,点击“状态”。

2、选择一个你现在的状态,然后再点击界面左下角的“拍摄”。

3、长按界面底部的拍摄图标即拍视频,也可以点击右下角的图库,从图库中选择视频。(抖音上有一些拍摄好的视频,搜索“微信8.0状态背景视频”,下载即可)

4、设置文案、是否公开、是否显示位置等(这里怎么选都可以,不会影响*后的效果),接着点击“就这样”。

5、回到“我”界面,向下滑动界面即可查看状态里的视频。

%title插图%num

2021年1月20日,微信ios发布8.0版本,更新一波又一波,咱细品一下:

①浮窗这个烂功能终于改了
之前的浮窗贴边简直是不忍直视,无数次干扰视觉,触发拖动位置的操作。怪不得龙哥说当时草草上线像个狗皮膏药。新版右滑负一屏竖向排列展示方框浮窗,仍然是短时间打开不会刷新并且保留当前位置,长时间会强制刷新。

痛苦还有:
以前浮窗的场景是对话界面需要关闭和打开复用,也就是全局浮窗。现在统一放到了会话list页面才可以右滑,所以微信产品经理以为,我只是为了把在读内容暂时保留一个tab吗?用户只是在内容阅读过程被会话打断吗?显然忽略了,内容-会话-内容-会话的多个循环动作。@腾讯微信团队 考虑会话屏右滑手势不

②增加个人状态,会话页昵称微信icon小尾巴
会话页、联系人list页、个人资料页设置个人状态后,都会在昵称后有个微信icon图标。会话页点击昵称小尾巴,可以看到对方设置的状态,状态按照固定二级分类(心情想法/工作学习/活动/休息/其他)+自定义描述的结构展示。
a 个人推测,后续是否除了icon继续开放展示二级分类
b 状态功能,对于降噪会有所效果;过往一段时间7.0大版本中微信噪音越来越多
c 个人状态仍然是情绪的一种社交表达,就像打完篮球我完全不介意给别人表现出我刚运动了的状态去社交,甚至在特定群体中我们还想去找到这种同类的趋同感

③表情包刷新
8.0把表情包强制F5了一遍,更加鲜活,有了动效。部分表情可以图层全屏动效。在沟通过场景中,表情包的演化是信息的升维,静态-动态-拟真,是逐步贴合真实世界真实用户的过程,同时也超越了语言限制、实现了情绪的抽象、超越了现实沟通的不可描述

④大招应该是:视频号-小商店-交易货币微信豆-直播-SBS
a 微信沉淀了熟人关系,视频号/小商品/直播统统开放给用户,以前利用朋友圈做交易场景的,现在完全可以用视频内容+直播+小商店+微信豆完成;

b 公众号内容消费太细分和垂直了,除了内容很多服务的功能性要依托公众号,就造成功能性、内容消费的冲突,我使用你的功能不一定是因为要消费内容。朋友圈信息消费形式的局限,满足不了信息爆炸时代大众对信息饲料的渴望。

c 用户更大的内容消费需求,自然需要新的口径,视频号提供了天然场地,抖音之类的短视频前期也是要基于你的好友关系因素加入到分发机制中,就是为了形成信息场(这点微信具有先天优势,之前视奸抖音时候 推测调用好友关系之后 真实社交圈竟然大家会有话题共鸣,就说明抖音和真实场景形成了依存关系)。而类似于钉钉的组织在线,微信视频号的好友关系在线啊,推荐、同城视频、直播、朋友♥都是分发机制的一种,你在微信生态有更完整的数据啊:位置、消费、好友、朋友圈、互动、甚至聊天,社交推荐机制自然出现(虽然龙哥说不会分析你们的聊天数据,但是一些结构化的低资源消耗且有益的还是可能会被标记到你的用户画像上面)

⑤视频池信息流改版,半屏改全屏
首先微信对自己的算法有了一定积累,拿到了数据喂养之后,变得命中率更高了。抖音和快手一直有个争论点,抖音是全屏上滑信息流,完全靠强大算法喂给用户想消费的喜欢消费的内容,同时也意味着用户没有选择内容的权力(只有搜索,大明用户,但这类用户时间有限);快手之前有个内部分享说他们采用双列展示信息,给了用户选择信息的权力,即我看的是我想看的,而抖音我看的是系统想让我看的;一点微妙的差异,也印证了快手宿华说快手要当阳光而不是聚光灯。

半屏时代意味着算法精准度上有待提高,需要结合用户自主选择消费;全屏进入推荐消费时代。

⑥关于歌曲,龙哥的畅想简直入木三分,对现实社会人内心的挖掘五体投地。人们听歌是因为歌曲内容和内心世界的共鸣,内心世界怎么被勾起?是现实世界勾起的内心世界,此刻的心情会转化为你看世界的视角。视角集合,直呼太*了

⑦视频号和直播,又一个激发和链接个体的大招,想象空间太大了……

微信,十年磨一剑,社交之王的宝座,真不是盖的。
如今,微信也真正成为了一种生活方式。
%title插图%num