Light's Blog

The best or nothing.

iOS知识小集-201912

| Comments

2019.12.04

ASR service 增加 enable remote silence。

2019.12.06

present 同一个VC会crash。
UI操作要放到主线程。

2019.12.09

iOS 启动优化

启动过程

冷启动:不在内存,也不存在进程。
热启动:APP结束后再启动,有部分在内存但没有进程存在。
resume:APP没结束,全在内存内存中,进程也存在。

iOS启动过程:
• 根据 info.plist 里的设置加载闪屏,建立沙箱,对权限进行检查等;
• 加载 Mach-O;
• 执行 attribute 的 constructor 函数;
• 加载类扩展;
• 加载 C++静态对象;
• 调用+load 函数;
• 执行 main 函数;
• Application 初始化,到 applicationDidFinishLaunchingWithOptions 执行完;
• 初始化帧渲染,到 viewDidAppear 执行完,用户可见可操作。

分析工具

Instruments,App launch。

分析纬度

线程是计算机资源调度和分配的基本单位,CPU 的使用情况会体现到线程上。

主线程耗时:

​ 1、使用 Messier 生成 trace json 进行分析。
​ 2、手动 hook objc_msgSend 生成 Objective-C 方法耗时数据分析。GCDFetchFeed/SMCallTraceCore.c at master · ming1016/GCDFetchFeed · GitHub

CPU:

​ 1、使用 Energy Log 检查 CPU 耗电,前台三分钟或者后台一分钟 CPU 线程连续占用80%以上判定为耗电,同事记录耗电线程堆栈供分析。
​ 2、查看线程情况,task_theadsact_list 数组包含所有线程,使用 thread_info 的接口可以返回线程的基本信息 thread_basic_info_t

内存:

​ 1、查看 APP 内存真实使用情况,webkit/MemoryFootprintCocoa.cpp
​ 2、JetSam 会判断 APP 内存使用情况,超出阈值就会杀死APP。
​ 3、NSProcessInfophysicalMemory 表示设备物理内存大小。

网络:

​ 1、使用 Fishhook hook 网络底层库 CFNetwork
​ 2、指标:DNS 时间,SSL 时间,首包时间,响应时间。

I/O:

​ 1、使用 Frida,动态二进制插桩技术,在程序运行时区插入自定义代码获取 I/O 的耗时和处理数据大小等数据。
​ 2、WWDC。

延后任务管理

对于分析后,没必要在启动阶段执行的方法,可以延后执行。一般创建四个队列:
• 异步串行队列:执行有依赖关系的任务。
• 异步并行队列:分组执行独立任务,限制任务数量,避免CPU和内存瞬时激增影响主线程用户操作。
• 闲时主线程串行队列:监听主线程 runloop 状态,在 kCFRunLoopBeforeWaiting 时开始执行闲时队列里的任务,在 kCFRunLoopAfterWaiting 是停止。
• 闲时异步串行队列:同上。

优化后如何保持

监控启动阶段方法耗时,出现异常快速定位。
通过 JSON 表示监控数据,自动化分析和定位。

2019.12.10

CLLocationManager 请求权限需要被强引用。

2019.12.19

iOS 项目架构

小团队

MVC
MVP
MVVM
MVCS

大团队

需要考虑如何划分模块粒度、如何分层、如何团队协作。

划分模块粒度

首先,模块划分需要遵循规范、清晰的原则。
其次,SOLID 原则。

  1. 单一功能原则:对象功能要单一,不要在一个对象里添加很多功能。
  2. 开闭原则:扩展是开放的,修改是封闭的。
  3. 里氏替换原则:子类对象是可以替代基类对象的。
  4. 接口隔离原则:接口的用途要单一,不要在一个接口上根据不同的参数实现多个功能。
  5. 依赖反转原则:方法应该依赖抽象,不要依赖实例。iOS 开发就是高层业务方法依赖于协议。 最后,选择合适的粒度。iOS 组件,应该是包含 UI 控件,相关多个小功能的合集,是一种粒度适中的模块。

先按物理划分为不同的pod库,然后梳理组件之间的逻辑关系,进行改造。

组件分层:
1. 底层是与业务无关的基础组件,比如网络和存储等; 2. 中间层是通用的业务组件,比如账号、埋点、支付、购物车等; 3. 最上层是迭代业务组件,更新频率最高。

被多个业务或团队使用的功能模块才需要做成组件。

多团队分工

首先,基建团队,负责业务无关的基础功能组件和业务相关的通用业务组件。
然后,业务团队,耦合度高的业务可以划分成单独的业务团队。
最后,人员相互流动,相互了解。

理想的架构

协议式架构:采用协议式编程思路,在编译层使用协议定义规范,实现可在不同地方,从而达到分布管理和维护组件的目的。
缺点:缺少统一调度层,导致难于集中管理;协议过于规范,不够灵活。

中间者架构:采用中间者统一管理的方式,来控制APP的整个生命周期中组件的调用关系。组件接口设计要保持一致。
CTMediator,运行时解耦。

2019.12.31

使用 lame 库,实现 pcm 流 转 mp3 流。

iOS知识小集-201911

| Comments

2019.11.10

xcode 11.2 无法上传APP至商店,需要使用 xcode 11.2.1 GM 版本上传。

2019.11.11

使用GCDAsyncSocket实现APP间通信,需要server端先开启,然后client端去连接。
可以在后台接收socket消息,但是如果被挂起后,将无法接收。
可以通过静默推送唤醒后台挂起应用,然后处理socket消息。

GATT 协议栈。

2019.11.12

iOS 可以实现多个APP作为中心设备与同一个周边设备建立BLE连接。
APP 可以获取当前正在连接的设备(retrieve),然后直接与之建立BLE连接。

2019.11.13

Transporter 通过非命令行上传APP。

Google/SignIn:3.0版本
GoogleSignIn:4.0版本

iOS知识小集-201910

| Comments

2019.10.09

同一个ViewController不能连续present两个ViewController。
苹果的思维非同凡响,其实你只需要解散一个Modal View Controller就可以了。即处于最底层的View Controller,这样处于这个层之上的ModalView Controller统统会被解散。

2019.10.21

MPRemoteCommandCenterAVAudioSession 有关系。
如果增加AVAudioSessionCategoryOptionMixWithOthers,远程控制会消失。
I faced the same related issue. AVAudioSessionCategory should be mixwithothers, and you will no longer can update Lock Screen Control Center with info.

2019.10.23

以16进制查看文件
1、vim:%!xxd转换为十六进制查看,:%!xxd -r转换为文本查看。
2、hexdump -C file | more

2019.10.31

制定协议需要添加版本号。

当找不出问题时,考虑是否会受上下文影响。

BLE通知消息有Notify和Indicate两种。

iPhone 7及以下机型蓝牙版本为4.2,iPhone 8及以上机型蓝牙版本为5.0。

iPhone 6 BLE 发送数据发送间隔有最小限制30ms。

2019.10.31

Symbolic Breakpoint 查找参数为nil的情况。

iOS知识小集-201909

| Comments

2019.09.03

适配 iOS 13

1、需要适配 dark mode。现强制设为非 dark mode;

1
2
3
if (@available(iOS 13.0, *)) {
    self.window.overrideUserInterfaceStyle = UIUserInterfaceStyleLight;
}

2、present 默认状态变为 卡片弹出,可下滑退出,可设置 self.modalPresentationStyle = UIModalPresentationFullScreen;
3、device token 解析方法改变:

1
2
3
4
5
6
7
8
9
10
11
#include <arpa/inet.h>
- (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);
}

4、导航栏 label 需要设置frame,否则颜色会异常;
5、label add layer 会遮盖文字;
6、获取 keyWindow;
7、Privacy - Bluetooth Always Usage Description:增加该权限配置;
8、UITextfield placeholder 颜色;
9、NSData to HexString

1
2
3
4
5
6
7
8
9
10
11
12
@implementation NSData (HexRepresentation)

- (NSString *)hexString {
    const unsigned char *bytes = (const unsigned char *)self.bytes;
    NSMutableString *hex = [NSMutableString new];
    for (NSInteger i = 0; i < self.length; i++) {
        [hex appendFormat:@"%02x", bytes[i]];
    }
    return [hex copy];
}

@end

2019.09.04

Wireless debug

1
2
3
4
5
6
7
8
@startuml
WWLoginController -> WWThirdPartyLoginManager : press apple sign in button
WWThirdPartyLoginManager --> WWLoginController : apple sigin and send (userId, accessToken) back
WWLoginController -> WWThirdPartyAccountManager : send (loginType, userId, accessToken) to third party account manager
WWThirdPartyAccountManager --> WWLoginController : send (loginType, userId, accessToken) to server and send login status back
WWLoginController -> WWAccountRelatedManager : login succeeded, save (loginType, userId, responseObject)
WWLoginController -> WWRegisterInputNumberController : login failed, register phone number
@enduml

2019.09.05

Apple Sign In

操作未完成:需要删除APP重新安装才生效;

2019.09.06

Unit Test

添加 Unit Test:创建工程时添加;添加新的 Target,选择 Test;
添加 测试文件:新建文件时选择测试文件;
can’t find import files:运行一次就好了;

如何测试代理
如何测试block

2019.09.09

Failed to connect to github.com port 443: Operation timed outhttps://www.githubstatus.com/

2019.09.11

生产者 - 消费者问题:cache 缓存数据,queue 读取数据,当cache为空时,queue应该等待,不能结束。 mark down mathjax:写数学公式。
音频流播放

2019.09.16

TTS中断问题:先缓存0.5秒数据再开始播放。
AVAudioSessionErrorCodeCannotStartRecording(561145187):无法后台语音识别。
AVAudioSessionErrorCodeCannotStartPlaying(561015905):无法后台语音识别。

2019.09.17

Error: Multiple commands produce:切换为 legacy build system。
(Enterprise) Sign In with Apple:去掉企业版capability中 sign in with apple 选项。

1
2
3
4
5
6
7
8
9
10
11
12
@startwbs
* OTA
** BLE Connection
*** CSRConnectionManager \n -connectPeripheral
*** CSRConnectionManager \n -addDelegate:
*** CSRConnectionManager \n -stopScan
** Gaia Connection(-discoveredPripheralDetails)
*** CSRGaia \n -connectPeripheral:
*** CSRGaiaManager \n -setDataEndPointMode:
** BLE Disconnection
*** CSRConnectionManager \n -removeDelegate: \n disconnectPeripheral
@end

Error Domain=NSOSStatusErrorDomain Code=1954115647 “(null)”:音频数据不完整导致初始化音频播放器出错。

2019.09.18

Gaia VA

封装、解析 Gaia 数据包 Done

CSRGaiaGattCommand
- initWithLength:
- initWithNSData:

封装 command + vendor_id + data:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (NSData *)gaiaPacketWithCommand:(GaiaCommandType)command
                           vendor:(uint16_t)vendor_id
                             data:(NSData *)params{
  CSRGaiaGattCommand *cmd = [[CSRGaiaGattCommand alloc] initWithLength:GAIA_GATT_HEADER_SIZE];        
  if (cmd) {
    [cmd setCommandId:command];
    [cmd setVendorId:vendor_id];

    if (params) {
      [cmd addPayload:params];
    }

    NSData *packet = [cmd getPacket];

    DLog(@"Outgoing packet: %@", packet);
    return packet;
  }
  return nil;
}

解析

1
2
3
4
5
6
7
8
9
10
11
- (CSRGaiaGattCommand *)gaiaGattCommandWithGaiaPacket:(NSData *)packet{
  return [[CSRGaiaGattCommand alloc] initWithNSData: packet];
}

- (NSData *)payloadWithGaiaGattCommand:(CSRGaiaGattCommand *)command{
  //  command type
  GaiaCommandType cmdType = [command getCommandId];
  //  payload
  NSData *payload = [command getPayload];
  return payload;
}

接收 Gaia 数据包 Done

CSRGaiaManager CSRUpdateManagerDelegate - didReceiveGaiaGattResponse:
目前在WWTicPodBTManager中有监听。

发送 Gaia 数据包 Todo

CSRGaiaManager - getBattery CSRGaia - getBattery - sendCommand:vendor:data:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)getBattery {    
    [self sendCommand:GaiaCommand_GetCurrentBatteryLevel
               vendor:CSR_GAIA_VENDOR_ID
                 data:nil];
}

- (void)setVolume:(NSInteger)value {
    NSMutableData *payload = [[NSMutableData alloc] init];
    uint8_t payload_event = (uint8_t)value;

    [payload appendBytes:&payload_event length:1];
    [self sendCommand:GaiaCommand_Volume
               vendor:CSR_GAIA_VENDOR_ID
                 data:payload];
}

Todo:
1、在 CSRGaiaManager 中添加 VA 相关命令;
2、在 CSRGaia 中添加 VA 相关命令;

VA 状态管理 Todo

消息确认,发送、接收、解析等。

2019.09.19

测定 BLE 传输 速率;

2019.09.23

运行时间

1
2
3
4
5
6
7
CFAbsoluteTime startTime =CFAbsoluteTimeGetCurrent();

//在这写入要计算时间的代码

CFAbsoluteTime linkTime = (CFAbsoluteTimeGetCurrent() - startTime);

NSLog(@"Linked in %f ms", linkTime *1000.0);

音频解码是连续的,解码过程中初始化条件不能被重置。

2019.09.24

Voice Assistant State

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
@startuml
[*] --> VA.CheckVersion

VA.CheckVersion --> VA.Idle : GaiaCheckVersion

VA.Idle --> VA.VoiceRequesting : GaiaStart
VA.Idle -left-> VA.MobvoiPaused : GaiaMobvoiPaused
VA.Idle -right-> VA.Idle : GaiaMobvoiFeatureEvent

VA.MobvoiPaused --> VA.Idle : GaiaMobvoiResumed

state VA.Idle {
  [*] --> VA.Session.Normal
  [*] --> VA.Session.PushToTalk
  [*] --> VA.Session.Secretary
  [*] --> VA.Session.Joke

  VA.Session.Normal : normal voice query, auto finish
  VA.Session.PushToTalk : longpress voice query, ticpods finish
  VA.Session.Secretary : voice query with special params
  VA.Session.Joke : text query joke
}

VA.VoiceRequesting --> VA.VoiceRequested : GaiaVoiceData

VA.VoiceRequested --> VA.VoiceRequested : GaiaVoiceDataRequest ack
VA.VoiceRequested --> VA.VoiceRequested : GaiaVoiceData
VA.VoiceRequested --> VA.Idle : GaiaVoiceEnd
VA.VoiceRequested --> VA.Idle : GaiaCancel
VA.VoiceRequested --> VA.HostCancelOrEnd : GaiaMobvoiHostVoiceEnd
VA.VoiceRequested --> VA.HostCancelOrEnd : GaiaMobvoiHostCancel

VA.HostCancelOrEnd --> VA.Idle : GaiaVoiceEnd ack
VA.HostCancelOrEnd --> VA.Idle : GaiaCancel ack
VA.HostCancelOrEnd --> VA.Idle : GaiaStart

VA.CheckVersion : GaiaCheckVersion:
VA.CheckVersion : 1. ack GaiaCheckVersion
VA.CheckVersion : 2. check protocol version
VA.CheckVersion : 3. get G722 decoder parameters
VA.CheckVersion : 4. initialize G722 decoder with parameters

VA.Idle : GaiaStart:
VA.Idle : 1. ack GaiaStart
VA.Idle : 2. get session type\n

VA.Idle : GaiaMobvoiPaused:
VA.Idle : 1. no GaiaMobvoiPaused ack
VA.Idle : 2. no GaiaMobvoiFeatureEvent ack\n

VA.Idle : GaiaMobvoiFeatureEvent:
VA.Idle : 1. no GaiaMobvoiFeatureEvent ack
VA.Idle : 2. do special action

VA.MobvoiPaused : GaiaMobvoiResumed:
VA.MobvoiPaused : 1. no GaiaMobvoiResumed ack
VA.MobvoiPaused : 2. ignore voice assistant command

VA.VoiceRequesting : GaiaVoiceData:
VA.VoiceRequesting : 1. no GaiaVoiceData ack
VA.VoiceRequesting : 2. send GaiaVoiceDataRequest

VA.VoiceRequested : GaiaVoiceDataRequest ack:
VA.VoiceRequested : 1. do nothing\n

VA.VoiceRequested : GaiaData:
VA.VoiceRequested : 1. no GaiaData ack
VA.VoiceRequested : 2. decode G722 packet
VA.VoiceRequested : 3. send decoded voice data to speech sdk\n

VA.VoiceRequested : GaiaVoiceEnd:
VA.VoiceRequested : 1. ack GaiaVoiceEnd
VA.VoiceRequested : 2. finish recognition\n

VA.VoiceRequested : GaiaCancel:
VA.VoiceRequested : 1. ack GaiaCancel
VA.VoiceRequested : 2. cancel recognition\n

VA.VoiceRequested : GaiaMobvoiHostVoiceEnd:
VA.VoiceRequested : 1. send GaiaVoiceEnd
VA.VoiceRequested : 2. finish recognition\n

VA.VoiceRequested : GaiaMobvoiHostCancel:
VA.VoiceRequested : 1. send GaiaCancel
VA.VoiceRequested : 2. cancel recognition

VA.HostCancelOrEnd : GaiaVoiceEnd ack:
VA.HostCancelOrEnd : 1. do nothing\n

VA.HostCancelOrEnd : GaiaCancel ack:
VA.HostCancelOrEnd : 1. do nothing\n

VA.HostCancelOrEnd : GaiaStart:
VA.HostCancelOrEnd : 1. VA.Idle -> VA.VoiceRequesting
@enduml

知识小集-201908

| Comments

2019.08.06

RPC failed; curl 56 LibreSSL SSL_read: SSL_ERROR_SYSCALL, errno 60:拉取大文件时换成不够出错。
增大缓存空间:git config https.postBuffer 524288000git config http.postBuffer 524288000

2019.08.07

SpeechSDK

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@startwbs
* SpeechSDK
** Buf
*** set address
*** get address
*** set size
*** get size
** CallBackBase
*** call back with parameter
** Value
** SpeechSDS
*** make instance
*** set parameter
*** get parameter
*** get service
*** release service
*** clear up
*** destroy instance
** Service
*** invoke parameter
** Parameter
*** set parameter
*** get parameter
*** has parameter
*** drop parameter
*** clear parameter
@endwbs

Speech Development Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@startwbs
* Speech Development Service
** Hotword Service
*** SDS get Hotword service
*** invoke Hotword parameter
*** invoke start parameter
*** start recorder
*** invoke audio buf parameter
*** process Hotword call back
*** stop audio recorder
*** invoke stop parameter
*** SDS release Hotword service
*** SDS destroy SDS
** ASR Service
*** sds get ASR service
*** invoke ASR parameter
*** invoke start parameter
*** start recorder
*** invoke audio buf parameter
*** process ASR call back
*** stop audio recorder
*** invoke stop parameter
*** SDS release ASR service
*** SDS destroy SDS
** TTS Service
*** sds get TTS service
*** invoke TTS parameter
*** invoke start parameter
*** invoke text parameter
*** invok read parameter
*** append audio data and play
*** stop audio player
*** invoke stop parameter
*** SDS release TTS service
*** SDS destroy SDS
@endwbs

AssistantEngin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@startwbs
* AssitantEngine
** Hotword
*** load Hotword model
*** start Hotword
*** stop Hotword
** ASR
*** start ASR
**** manual
**** auto
*** cancel ASR
*** stop ASR
** Text Query
** TTS
*** start TTS
**** text
**** type
**** url
*** stop TTS
@endwbs

2019.08.15

pod 私有库问题
pod 私有库问题
通过 subspec 实现目录结构分级;

无法找到引用的第三方库:framework search path 有问题,设置pod_target_xcconfig时导致的。

Undefined symbols for architecture arm64:在配置中增加 other linker flags。

dyld: Library not loaded: @rpath:pod文件名与依赖库的名字相同,导致异常,更换pod名称。

dynamic: Dynamic frameworks and libraries are only supported on iOS 8.0 and onwards:platform = :ios, ‘9.0’

vendored framework not foundhttps://github.com/CocoaPods/CocoaPods/issues/1824 改用'POD/file.h'

Cocoapods wrapping around a static library without i386 architecture:s.pod_target_xcconfig = { ‘VALID_ARCHS[sdk=iphonesimulator*]’ => ‘’ }

source 中不要添加tag

2019.08.20

注意线程问题。

HFP 状态下 AudioQueueNewOutput 无法正常播放音频 改为 A2DP 播放音频;

iOS知识小集-201907

| Comments

2019.07.01

一般情况下,APP退到后台 30秒 后会被挂起。
申请后台任务,可以有至多 3分钟 时间处理任务,之后会被挂起。
由APP 触发timer 改为 蓝牙设备定期触发 timer,唤醒APP,执行操作。

2019.07.02

颈部操后台操作采用同样的逻辑。

2019.07.05

颈部操音乐处理,改为耳机状态下mix & duck。
结束后需要setActive = NO。恢复其他音乐正常播放。

2019.07.10

监听AVAudioSessionInterruptionNotification获取打断开始和结束的通知,做相应的处理。

pod update error: RPC failed; curl 56 LibreSSL SSL_read: SSL_ERROR_SYSCALL, errno 60, 增加buffer大小,git config --global http.postBuffer 114288000

2019.07.11

#import 'file',预处理后会递归复制头文件至当前文件;
@import Module,优化文件体积和编译速度;
[[UIApplication sharedApplication] setIdleTimerDisabled:YES];,防止灭屏;
gem sources -l,查看ruby gem镜像地址;
gem source -r url,删除ruby镜像地址;
gem source -a url,添加ruby镜像地址;

2019.07.12

检索特定字符串
1. 在工程目录下,grep -r string .
2. 在ipa包内,strings - -a -arch armv7 "AppName" | grep stringstrings - -a -arch armv7 "AppName" > AppName.txt

企业版打包,普通Team member只能安装developer版本,需要由管理员导出企业证书并安装,才可以导出distribution版本。

2019.07.23

无耳机条件下,A2DP不能少。
setActive:NO 可以恢复其他APP音乐。
开启 background task 来执行后台操作。

2019.07.24

Invalid virtual filesystem overlay file,clean,删除diriveddata,重启xcode。

制作git patch:git format-patch commitID
应用git patch:git am patchName
查看修改:git apply patchName --reject
对照修改解决问题删除rej文件之后:git add . git am --continue

2019.07.29

Core Bluetooth

Devices that implement the central role in Bluetooth low energy communication perform a number of common tasks—for example,
discovering and connecting to available peripherals, and exploring and interacting with the data that peripherals have to offer. Devices that implement the peripheral role also perform a number of common, but different, tasks—for example,
publishing and advertising services, and responding to read, write, and subscription requests from connected centrals.

A peripheral typically has data that is needed by other devices.
A central typically uses the information served up by a peripheral to accomplish some task.

Peripherals make their presence known by advertising the data(services、characteristics) they have over the air.
Centrals scan for nearby peripherals that might have data they’re interested in, or interact with a peripheral’s service by reading or writing the value of that service’s characteristic.

When a central discovers such a peripheral, the central requests to connect to the peripheral and begins exploring and interacting with the peripheral’s data.
The peripheral is responsible for responding to the central in appropriate ways.
You can retrieve the value of a characteristic by reading it directly or by subscribing to it.

As Centrals

CBCentralManager

CBPeripheral
CBService
CBCharacteristic CBCharacteristic CBService
CBCharacteristic CBCharacteristic

As Peripherals

CBPeripheralManager

CBMutableService CBMutableCharacteristic CBMutableCharacteristic

CBCentral

Centrals

Start up a central manager object

1
myCentralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil options:nil];

Discover and connect to peripheral devices that are advertising

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//  specify services CBUUID to discover peripherials you are interested in
[myCentralManager scanForPeripheralsWithServices:nil options:nil];

- (void)centralManager:(CBCentralManager *)central
 didDiscoverPeripheral:(CBPeripheral *)peripheral
     advertisementData:(NSDictionary *)advertisementData
                  RSSI:(NSNumber *)RSSI {
    //  If you plan to connect to the discovered peripheral, keep a strong reference to it so the system does not deallocate it.
    NSLog(@"Discovered %@", peripheral.name);
    self.discoveredPeripheral = peripheral;
    ...
}

//  save power
[myCentralManager stopScan];

Connecting to a Peripheral Device After You’ve Discovered It

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[myCentralManager connectPeripheral:peripheral options:nil];

- (void)centralManager:(CBCentralManager *)central
  didConnectPeripheral:(CBPeripheral *)peripheral {

    NSLog(@"Peripheral connected");

    //  set peripheral delegate
    peripheral.delegate = self;

    //  specify services CBUUID to discover services you are interested in
    [peripheral discoverServices:nil];

    ...
}

Discovering the Services of a Peripheral That You’re Connected To

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)peripheral:(CBPeripheral *)peripheral
didDiscoverServices:(NSError *)error {

    for (CBService *service in peripheral.services) {
        NSLog(@"Discovered service %@", service);

        NSLog(@"Discovering characteristics for service %@", service);
        [peripheral discoverCharacteristics:nil forService:service];

        ...
    }
    ...
}

Discovering the Characteristics of a Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)peripheral:(CBPeripheral *)peripheral
didDiscoverCharacteristicsForService:(CBService *)service
             error:(NSError *)error {

    for (CBCharacteristic *characteristic in service.characteristics) {
        NSLog(@"Discovered characteristic %@", characteristic);

        //  read characteristic
        [peripheral readValueForCharacteristic:characteristic];

        //  subscribe characteristic
        [peripheral setNotifyValue:YES forCharacteristic:interestingCharacteristic];
        ...
    }
    ...
}

Retrieving the Value of a Characteristic

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)peripheral:(CBPeripheral *)peripheral
didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic
             error:(NSError *)error {

    NSData *data = characteristic.value;
    // parse the data as needed
    ...
}

- (void)peripheral:(CBPeripheral *)peripheral
didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic
             error:(NSError *)error {

    if (error) {
        NSLog(@"Error changing notification state: %@",
           [error localizedDescription]);
    }
    ...
}

Writing the Value of a Characteristic

1
2
3
4
5
6
7
8
9
10
11
12
13
NSLog(@"Writing value for characteristic %@", interestingCharacteristic);
[peripheral writeValue:dataToWrite forCharacteristic:interestingCharacteristic type:CBCharacteristicWriteWithResponse];

- (void)peripheral:(CBPeripheral *)peripheral
didWriteValueForCharacteristic:(CBCharacteristic *)characteristic
             error:(NSError *)error {

    if (error) {
        NSLog(@"Error writing characteristic value: %@",
            [error localizedDescription]);
    }
    ...
}

Peripherals

Starting Up a Peripheral Manager

1
myPeripheralManager = [[CBPeripheralManager alloc] initWithDelegate:self queue:nil options:nil];

Setting Up Your Services and Characteristics

1
2
3
4
5
6
7
8
9
10
//  value != nil 只可读,value == nil 可写
myCharacteristic = [[CBMutableCharacteristic alloc] initWithType:myCharacteristicUUID
                                                      properties:CBCharacteristicPropertyRead
                                                           value:myValue
                                                     permissions:CBAttributePermissionsReadable];
//  primary service && secondary service
myService = [[CBMutableService alloc] initWithType:myServiceUUID primary:YES];

//  add characteristics
myService.characteristics = @[myCharacteristic];

Publishing Your Services and Characteristics

1
2
3
4
5
6
7
8
9
10
11
12
//  add service
[myPeripheralManager addService:myService];

- (void)peripheralManager:(CBPeripheralManager *)peripheral
            didAddService:(CBService *)service
                    error:(NSError *)error {

    if (error) {
        NSLog(@"Error publishing service: %@", [error localizedDescription]);
    }
    ...
}

Advertising Your Services

1
2
3
4
5
6
7
8
9
10
[myPeripheralManager startAdvertising:@{ CBAdvertisementDataServiceUUIDsKey : @[myFirstService.UUID, mySecondService.UUID] }];

- (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral
                                       error:(NSError *)error {

    if (error) {
        NSLog(@"Error advertising: %@", [error localizedDescription]);
    }
    ...
}

Responding to Read and Write Requests from a Central

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveReadRequest:(CBATTRequest *)request {
    if ([request.characteristic.UUID isEqual:myCharacteristic.UUID]) {
        if (request.offset > myCharacteristic.value.length) {
            [myPeripheralManager respondToRequest:request
                withResult:CBATTErrorInvalidOffset];
            return;
        }

        request.value = [myCharacteristic.value subdataWithRange:NSMakeRange(request.offset, myCharacteristic.value.length - request.offset)];

        [myPeripheralManager respondToRequest:request withResult:CBATTErrorSuccess];
    }
}

- (void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveWriteRequests:(NSArray<CBATTRequest *> *)requests{
    if ([request.characteristic.UUID isEqual:myCharacteristic.UUID]) {
        myCharacteristic.value = request.value;

        [myPeripheralManager respondToRequest:[requests objectAtIndex:0] withResult:CBATTErrorSuccess];
    }
  }

Sending Updated Characteristic Values to Subscribed Centrals

1
2
3
4
5
6
7
8
9
10
- (void)peripheralManager:(CBPeripheralManager *)peripheral central:(CBCentral *)central didSubscribeToCharacteristic:(CBCharacteristic *)characteristic {
    NSLog(@"Central subscribed to characteristic %@", characteristic);

    NSData *updatedValue = // fetch the characteristic's new value
    BOOL didSendValue = [myPeripheralManager updateValue:updatedValue forCharacteristic:characteristic onSubscribedCentrals:nil];
}

- (void)peripherialManagerIsReadyToUpdateSubscribers:(CBPeripheralManager *)peripheral {

}

Background Processing

That said, you can declare your app to support the Core Bluetooth background execution modes to allow your app to be woken up from a suspended state to process certain Bluetooth-related events. Even if your app doesn’t need the full range of background processing support, it can still ask to be alerted by the system when important events occur.

All Bluetooth-related events that occur while a foreground-only app is in the suspended state are queued by the system and delivered to the app only when it resumes to the foreground. That said, Core Bluetooth provides a way to alert the user when certain central role events occur. The user can then use these alerts to decide whether a particular event warrants bringing the app back to the foreground.

CBConnectPeripheralOptionNotifyOnConnectionKey
CBConnectPeripheralOptionNotifyOnDisconnectionKey CBConnectPeripheralOptionNotifyOnNotificationKey

bluetooth-central
bluetooth-peripheral
While your app is in the background you can still discover and connect to peripherals, and explore and interact with peripheral data. In addition, the system wakes up your app when any of the CBCentralManagerDelegate or CBPeripheralDelegate delegate methods are invoked, allowing your app to handle important central role events, such as when a connection is established or torn down, when a peripheral sends updated characteristic values, and when a central manager’s state changes.

Upon being woken up, an app has around 10 seconds to complete a task. Ideally, it should complete the task as fast as possible and allow itself to be suspended again. Apps that spend too much time executing in the background can be throttled back by the system or killed.

Adding Support for State Preservation and Restoration

Opt In to State Preservation and Restoration

1
2
3
myCentralManager = [[CBCentralManager alloc] initWithDelegate:self
                                                        queue:nil
                                                      options:@{ CBCentralManagerOptionRestoreIdentifierKey: @"myCentralManagerIdentifier" }];

Reinstantiate Your Central and Peripheral Managers

1
2
3
4
5
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    NSArray *centralManagerIdentifiers =
        launchOptions[UIApplicationLaunchOptionsBluetoothCentralsKey];
    ...
}

Implement the Appropriate Restoration Delegate Method
Update Your Initialization Process

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)centralManager:(CBCentralManager *)central
      willRestoreState:(NSDictionary *)state {

    NSArray *peripherals =
        state[CBCentralManagerRestoredStatePeripheralsKey];
    ...

    NSUInteger serviceUUIDIndex =
        [peripheral.services indexOfObjectPassingTest:^BOOL(CBService *obj,
        NSUInteger index, BOOL *stop) {
            return [obj.UUID isEqual:myServiceUUIDString];
        }];

    if (serviceUUIDIndex == NSNotFound) {
        [peripheral discoverServices:@[myServiceUUIDString]];
        ...
    }
}

Best Practices as Central

Scan for Devices Only When You Need To Unless you need to discover more devices, stop scanning for other devices after you have found one you want to connect to.

Explore a Peripheral’s Data Wisely Therefore, you should look for and discover only the services and associated characteristics your app needs.

Subscribe to Characteristic Values That Change Often It is best practice to subscribe to a characteristic’s value when possible, especially for characteristic values that change often.

Disconnect from a Device When You Have All the Data You Need

Reconnecting to Peripherals
Retrieving a List of Known Peripherals

1
knownPeripherals = [myCentralManager retrievePeripheralsWithIdentifiers:savedIdentifiers];

Retrieving a List of Connected Peripherals

Best Practices as Peripheral

Respect the Limits of Advertising Data When your app is in the foreground, it can use up to 28 bytes of space in the initial advertisement data for any combination of the two supported advertising data keys.

Configure Your Characteristics to Support Notifications

1
2
3
4
myCharacteristic = [[CBMutableCharacteristic alloc] initWithType:myCharacteristicUUID
                                                      properties:CBCharacteristicPropertyRead | CBCharacteristicPropertyNotify
                                                           value:nil
                                                     permissions:CBAttributePermissionsReadable];

Require a Paired Connection to Access Sensitive Data

1
2
3
4
emailCharacteristic = [[CBMutableCharacteristic alloc] initWithType:emailCharacteristicUUID
                                                         properties:CBCharacteristicPropertyRead | CBCharacteristicPropertyNotifyEncryptionRequired
                                                              value:nil
                                                        permissions:CBAttributePermissionsReadEncryptionRequired];

iOS知识小集-201906

| Comments

2019.06.06

音频基础

音频文件:即声音的数字信号。由原声音信息采样、量化和编码产生。
奈奎塞特理论:采样频率高于声音最高频率的两倍时,才能将数字信号还原为原来的声音。一般为40~50kHZ。
脉冲编码调制:对声音进行采样、量化的过程,简称 PCM。PCM数据为原始音频数据,完全无损,体积庞大。
音频格式:无损压缩(ALAC、APE、FLAC),有损压缩(MP3、AAC、OGG、WMA)。
MP3码率:代表压缩质量,码率越高、质量越好。固定码率(CBR),可变码率(VBR)。
MP3数据结构:ID3 + 音频数据。音频数据由帧(帧头4B + 附加信息32B + 声音数据)构成。

iOS音频播放

只播放音频文件:AVFoudation。
播放并存储音频文件:AudioFileStreamer + AudioQueue。边下载音频数据边用NSFileHandler读取本地音频文件给AudioFileStreamer解析分离音频帧,之后给AudioQueue进行解码和播放。
专业音乐播放器:使用AudioConverter把音频数据转换为PCM数据,再由AudioUnit + AUGraph进行音效处理和播放。

AudioSession

AudioSession作用:

1、定义APP如何使用音频(是录音还是播放);
2、选择音频输入输出设备(手机麦克风、手机扬声器、耳机麦克风、耳机扬声器);
3、协调其他APP音频和系统音频(电话打断,是否混响);

AudioSession使用:

1、设置category;
2、设置options;
3、setActive;

AudioFileStream

AudioFileStream作用:

1、读取采样率、码率、时长等基本信息;
2、分离音频帧;

AudioQueue

AudioQueue作用:

1、播放PCM数据、MP3数据;
2、录音;

AudioQueue使用:

1、创建AudioQueue;
2、创建AudioQueueBufferRef;
3、将数据写入AudioQueueBufferRef,然后插入到AudioQueue中;
4、调用AudioQueueStart开始播放;
5、在回调中将使用过的AudioQueueBufferRef放回重复使用;

20919.06.11

AVAudioSessionCategoryOptionAllowBluetooth

采用 HFP 协议将 蓝牙设备作为音频输入设备,且同时自动变为输出设备。通常使用蓝牙设备录音时使用,此协议下蓝牙设备既可录音又可播放音频,但 音质较差
只在 category 为 AVAudioSessionCategoryPlayAndRecord、AVAudioSessionCategoryRecord 时使用。

AVAudioSessionCategoryOptionAllowBluetoothA2DP

采用 A2DP 协议将 蓝牙设备作为音频输出设备。通常用来作为高质量音频蓝牙输出,比如播放音乐,此协议下蓝牙设备 无法作为音频输入设备,音频输入需要使用手机麦克风。
category 为 AVAudioSessionCategoryAmbient、AVAudioSessionCategorySoloAmbient、AVAudioSessionCategoryPlayback 时,默认采用 A2DP。
category 为 AVAudioSessionCategoryPlayAndRecord 时,如果需要使用 A2DP,需显示设置。
category 为 AVAudioSessionCategoryMultiRoute、AVAudioSessionCategoryRecord 时,A2DP 默认无效。
AVAudioSessionCategoryOptionAllowBluetooth 和 AVAudioSessionCategoryOptionAllowBluetoothA2DP 同时设置时,优先采用前者。
使用原则:
1、播放音频时,为了保证音频输出质量,采用 AVAudioSessionCategoryOptionAllowBluetoothA2DP;
2、既需要录音又需要播放音频时,需采用 AVAudioSessionCategoryOptionAllowBluetooth;

2019.06.12

后台开启AVAudioSession需要设置为MixWithOthers,否则无法激活。

2019.06.13

Could not find a ios simulator。
gem env 找到ruby路径查看 fourflusher 中 的 find.rb 文件。

2019.06.14

打包机器company账号过期,需重新登录。
Error:CodeSign /Users/Library/Developer/Xcode/DerivedData/Build/Intermediates.noindex/ArchiveIntermediates
解决办法:
unlock keychain security unlock-keychain -p mobvoi login.keychainsecurity set-key-partition-list -S apple: -s -k mobvoi login.keychain
删除过期provisioningprofile。~/Library/MobileDevice/Provisioning Profiles
清除~/Library/Developer/Xcode/DerivedData/
修改证书信任。
参考资料:
Code signing is required for product type ‘Application’ in SDK ‘iOS 10.0’
code sign failed
Sh 打包报错

2019.06.17

打包机器无法拉取gerrit代码。
Error:gerrit_jenkins@gerrit: Permission denied (publickey)

参考解决方法
生成秘钥:ssh-keygen
查看秘钥:cd ~/.ssh
查看公钥:cat ~/.ssh/id_rsa.pub
查看私钥:cat ~/.ssh/id_rsa
查看已发布公钥:cat ~/.ssh/known_hosts

2019.06.20

PCM 转 WAV

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
- (NSData*)stripAndAddWavHeader:(NSData*) wav {
  unsigned long wavDataSize = [wav length] - 44;
  NSData *WaveFile= [NSMutableData dataWithData:[wav subdataWithRange:NSMakeRange(44, wavDataSize)]];
  NSMutableData *newWavData;
  newWavData = [self addWavHeader:WaveFile];
  return newWavData;
}

- (NSMutableData *)addWavHeader:(NSData *)wavNoheader {
  int headerSize = 44;
  long totalAudioLen = [wavNoheader length];
  long totalDataLen = [wavNoheader length] + headerSize-8;
  long longSampleRate = 8000.0;
  int channels = 1;
  long byteRate = 8 * 16000.0 * channels/8;

  Byte *header = (Byte*)malloc(44);
  header[0] = 'R';  // RIFF/WAVE header
  header[1] = 'I';
  header[2] = 'F';
  header[3] = 'F';
  header[4] = (Byte) (totalDataLen & 0xff);
  header[5] = (Byte) ((totalDataLen >> 8) & 0xff);
  header[6] = (Byte) ((totalDataLen >> 16) & 0xff);
  header[7] = (Byte) ((totalDataLen >> 24) & 0xff);
  header[8] = 'W';
  header[9] = 'A';
  header[10] = 'V';
  header[11] = 'E';
  header[12] = 'f';  // 'fmt ' chunk
  header[13] = 'm';
  header[14] = 't';
  header[15] = ' ';
  header[16] = 16;  // 4 bytes: size of 'fmt ' chunk
  header[17] = 0;
  header[18] = 0;
  header[19] = 0;
  header[20] = 1;  // format = 1
  header[21] = 0;
  header[22] = (Byte) channels;
  header[23] = 0;
  header[24] = (Byte) (longSampleRate & 0xff);
  header[25] = (Byte) ((longSampleRate >> 8) & 0xff);
  header[26] = (Byte) ((longSampleRate >> 16) & 0xff);
  header[27] = (Byte) ((longSampleRate >> 24) & 0xff);
  header[28] = (Byte) (byteRate & 0xff);
  header[29] = (Byte) ((byteRate >> 8) & 0xff);
  header[30] = (Byte) ((byteRate >> 16) & 0xff);
  header[31] = (Byte) ((byteRate >> 24) & 0xff);
  header[32] = (Byte) (2 * 8 / 8);  // block align
  header[33] = 0;
  header[34] = 16;  // bits per sample
  header[35] = 0;
  header[36] = 'd';
  header[37] = 'a';
  header[38] = 't';
  header[39] = 'a';
  header[40] = (Byte) (totalAudioLen & 0xff);
  header[41] = (Byte) ((totalAudioLen >> 8) & 0xff);
  header[42] = (Byte) ((totalAudioLen >> 16) & 0xff);
  header[43] = (Byte) ((totalAudioLen >> 24) & 0xff);

  NSMutableData *newWavData = [NSMutableData dataWithBytes:header length:44];
  [newWavData appendBytes:[wavNoheader bytes] length:[wavNoheader length]];
  return newWavData;
}

multipart/form-data 上传 NSData
multipart/form-data upload

2019.06.21

NSFileManager、NSFileHandle

1
2
3
4
5
6
7
8
9
10
- (void)writeFile:(NSData*)data toPath:(NSString*)path{
  //  创建文件
  NSFileManager *fileManager = [NSFileManager defaultManager];
  [fileManager removeItemAtPath:path error:nil];
  [fileManager createFileAtPath:path contents:nil attributes:nil];
  //  写入文件
  NSFileHandle *fileHandle = [NSFileHandle fileHandleForWritingAtPath:path];
  [fileHandle writeData:data];
  [fileHandle closeFile];
}

convert CMSampleBufferRef to NSData

1
2
3
4
5
6
7
8
- (NSData*)dataFrom:(CMSampleBufferRef)sampleBuffer{
  CMBlockBufferRef blockBufferRef = CMSampleBufferGetDataBuffer(sampleBuffer);
  size_t length = CMBlockBufferGetDataLength(blockBufferRef);
  Byte buffer[length];
  CMBlockBufferCopyDataBytes(blockBufferRef, 0, length, buffer);
  NSData *data = [NSData dataWithBytes:buffer length:length];
  return data;
}

PBXCp Error: No such file or directory

bitcode_strip exited with 1
In additional to your answer, there is mush simpler method. As I see, XCode uses bitcode-strip only when enviroment variable STRIP_BITCODE_FROM_COPIED_FILES is set to YES. It seems that it’s set to default when enable_bitcode is switched on.
Add User-Defined Setting STRIP_BITCODE_FROM_COPIED_FILES=NO to your Target and it will compile fine, XCode will use standard strip.

2019.06.27

however, it’s generally preferable to deactivate your audio session before changing the category or other session properties. Making these changes while the session is deactivated prevents unnecessary reconfigurations of the audio system.

Ensure that the audio session for an app using a recording category is active only while recording. Before recording starts and when it stops, ensure that your session is inactive to allow other sounds, such as incoming message alerts, to play.

If an app supports background audio playback or recording, deactivate its audio session when entering the background if the app is not actively using audio (or preparing to use audio). Doing so allows the system to free up audio resources so that they may be used by other processes. It also prevents the app’s audio session from being deactivated when the app process is suspended by the operating system.

An audio interruption is the deactivation of your app’s audio session—which immediately stops your audio. Interruptions happen when a competing audio session from an app is activated and that session is not categorized by the system to mix with yours. After your session goes inactive, the system sends a “you were interrupted” message that you can respond to by saving state, updating the user interface, and so on.

2019.06.28

如何后台运行timer:通过蓝牙设备触发。

iOS知识小集-201905

| Comments

2019.05.05

Flutter 状态管理
Flutter的很多灵感来自于React,它的设计思想是数据与视图分离,由数据映射渲染视图。所以在Flutter中,它的Widget是immutable的,而它的动态部分全部放到了状态(State)中。
Scoped Model Redux
BLoC
BLoC

2019.05.06

Widget - State - BuildContext - InheritedWidget

  1. Widgets: related to layout;
  2. Widgets Tree: widgets are organized in tree structures;
  3. BuildContext: a reference to the location of a widget with in the widgets tree. a BuildContext only belongs to one widget.
  4. Stateless Widget: 创建之后就无法再改变的控件,比如Text,Row,Container,不会rebuild;
  5. Stateful Widget: 创建之后可以根据状态发生变化;widget.color;
  6. State: 状态的改变会影响控件的行为和布局,触发rebuild;State会和BuildContext绑定,绑定之后称为mounted,只有mounted之后,才可以setState;State mounted之后,不能直接被其他BuildContext访问;
  7. Stateful Widget Life Circle: constructor() -> createState() -> constructor() -> initState(){ controllers, animations, … } -> mounted -> didChangeDependencies(){ listeners } -> build() { build widgets } -> dispose() { controllers, listeners, …} ;
  8. 在 widget 的生命周期中,是否需要根据变量的变化来 rebuild widget;
  9. Widget unique identity - key:widgets的唯一标识,可以通过key来获取widget;
  10. State -> BuildContext -> Widget Instance:通过key来访问 state;
  11. InheritedWidget:实现在widget tree中高效O(1)共享信息;

Reactive Programming - Streams - BLoC

  1. BlocBase
  2. BlocProvider
  3. ApplicationBloc
  4. LoginBloc
  5. RxDart

dependencies:
bloc:^0.8.0
flutter_bloc:^0.5.0
equatable:^0.1.6

2019.05.07

bloc

2019.05.13

构造方法:
构造方法不会被子类继承;
convenient constructors;
named constructors;
initializer list;
initializer list -> superclass’s no-arg constructor -> main class’s no-arg constructor;
redirecting constructors;
factory constructors;

2019.05.14

使用SingleChildScrollView避免超出边界。
本地图片路径名称。
shared_preference 引入问题,增加pod xcconfig,编码问题。

iOS知识小集-201904

| Comments

2019.04.01

企业版本打包流程:
1、code sign identity,创建、或者导出p12;
2、创建APP ID;
3、创建ProvisioningProfile,下载安装;
4、archive、inhouse;

build can’t find simulator, need to update cocoapod.

2019.04.03

iOS 打包

xcodebuild archive -configuration $configuration VERBOSE_SCRIPT_LOGGING=YES -workspace ios/$scheme.xcworkspace -scheme $scheme -archivePath ./out/"$build_type".xcarchive

xcodebuild -exportArchive \ -allowProvisioningUpdates \ -archivePath ./out/"$build_type".xcarchive \ -exportOptionsPlist ios/$ExportOptions.plist \ -exportPath ./out/

添加 configuration 并且创建对应的 .xcconfig 文件需要添加到 Flutter 目录下
命令打包、导出成功;
需要配置相应的编译配置;
生成 ExportOptions.plis
配置 $scheme $configuration $build_type
注意 archive路径问题
忽略已经在git中的文件 .gitignore 需要删除对应文件才生效 git rm -r file
服务器安装 ProvisioningProfile 否则无法打包
jenkins workspace 仿照 vpa_release 创建 jenkins 工程
Xcode Command /usr/bin/codesign failed with exit code 1 : errSecInternalComponent 需要打开 keychain

2019.04.11

Android 打包

添加 安卓模拟器

initializing gradle 巨慢无比 替换为本地gradle版本
distributionUrl=https://services.gradle.org/distributions/gradle-4.6-all.zip

Error Domain=DVTPortalServiceErrorDomain Code=1100 “Your session has expired. Please log in.” 重新登录账号

生成 key.jks 文件 keytool -genkey -v -keystore ~/key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias key
添加 /android/key.properties 不要加入到git中

1
2
3
4
storePassword=<password from previous step>
keyPassword=<password from previous step>
keyAlias=key
storeFile=<location of the key store file, e.g. /Users/<user name>/key.jks>

修改 /android/app/build.gradle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def keystorePropertiesFile = rootProject.file("key.properties")
def keystoreProperties = new Properties()
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))

signingConfigs {
    release {
        keyAlias keystoreProperties['keyAlias']
        keyPassword keystoreProperties['keyPassword']
        storeFile file(keystoreProperties['storeFile'])
        storePassword keystoreProperties['storePassword']
    }
}
buildTypes {
    release {
        signingConfig signingConfigs.release
    }
}

运行 flutter build apk 生成 apk 文件,运行 flutter install 安装至手机。

2019.04.09

耳机颈部操
耳机配对:先配对经典蓝牙,再配对对应BLE
耳机通信:先注册服务,再接收耳机通知

2019.04.22

flutter
1、stateful widget、stateless widget;
2、animation;
3、paint;
4、opacity;
5、layout;
6、navigator & route;
7、network request;
8、ProgressIndicator;
9、asset、AssetBundle;
10、localizable、flutter_localizations(itl);
11、life circle;
12、listview、ListView.Builder;
13、gesture、GestureDetector;
14、theme、font;
15、text filed;
16、GPS(location)、camera(image_picker)、userdefault(SharePreferences)、database(SQFlite)、notification(firebase_messaging);
17、SharePreferences;
18、平台 UI 定制
19、json

2019.04.24

flutter_go

Application:

​ router:路由
​ tabbarController:tab bar
​ sharedPeference:共享存储

Router:

​ 路由管理:fluro
​ 定义路由名称:routers.dart
​ 定义路由事件:routers_handler.dart

TabBarController:

​ 创建TabController:controll = TabController(initialIndex: 0, vsync: this, length: 4)
​ 绑定TabController:TabBarView(controller: controller, children: <Widget>[Page1(), Page2(), Page3()])
​ 创建bottomNavigationBar:Material( child: TabBar(controller: controller, tabs: myTabs))

Widgets:

ThemeData:

​ 颜色:primaryColor,backgroundColor,accentColor;
​ 字体:textTheme;
​ 图标:iconTheme;

Color:

​ 定义全局 ThemeColor:const int ThemeColor = 0xFFC91B3A
​ 通过颜色值创建颜色:Color(ThemeColor)
​ 获取ThemeColor:Theme.of(context).primaryColor

PageController:

​ 创建controller:controller = PageController(initialPage: index)
​ 销毁controller:controller.dispose()
​ 操作controller:controller.animateToPage(1, duration: Duration(milliseconds: 300), curve: Curves.linear)
​ 绑定controller:PageView(controller: controller, onPageChanged: _onPageChanged, children: _buildItems)
​ indicator:Row of circles;

ListRefresh:

​ 异步方法,当mounted = true时,才能调用setState
​ 分页数据请求:requestApi
​ cell:renderItem
​ headerView:headerView

NetUtils

​ 网络请求:dio ​ get
​ post
​ json
​ protobuf

Utils

​ 时间、日期

Timer:

​ 创建Timer:timer = Timer.periodic(Duration(seconds: 5), (timer) { /// do something })
​ 销毁Timer:timer.cancel()

iOS知识小集-201903

| Comments

2019.03.08

Flutter开发流程搭建

目标:
实现Flutter对原来Native工程的非侵入式集成 —— 通过pod集成;
保证Flutter编译环境的唯一性 —— 通过服务器编译产生构建产物;
实现了Flutter的持续集成 —— 自动编译flutter代码并同步flutter构建产物;

  1. 在打包机器安装 flutter 开发环境;√

下载 Flutter SDK

配置环境变量:export PATH=[FLUTTER_INSTALL_PATH]/flutter/bin:$PATH

安装Xcode、Android Studio;

flutter doctor
iOS:
brew update
brew install –HEAD usbmuxd
brew link usbmuxd
brew install –HEAD libimobiledevice
brew install ideviceinstaller
brew install ios-deploy
Android:
update Android SDK 28
Plugins:
Flutter
Dart

  1. 新建 gerrit 测试项目 flutter_test、flutter_sdk_test、flutter_pod_test;√

  2. 新建 jenkins 打包项目 flutter_test;√

  3. 添加 Flutter 自动编译脚本;√

    jenkins源码配置:Refspec:refs/changes/:refs/changes/
    添加环境变量:export PATH=/Users/Mobvoi/Library/flutter/bin:$PATH
    自动使用company签名:修改源工程配置
    修改项目 Bundle ID,使用公司证书签名,需要区分证书;
    flutter build ios 编译 release 版本;
    flutter build ios --debug 编译 debug 版本;
    编译产物在 project_dir/ios/Flutter 目录下;
    将编译产物导出至 project_dir/FlutterSDK 目录下;
    build.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#!/bin/sh

# keychain 访问权限
security unlock-keychain -p mobvoi login.keychain

# 添加环境变量
export PATH=/Users/Mobvoi/Library/flutter/bin:$PATH

//  获取当前目录
project_dir=$(cd "$(dirname "$0")";pwd)
echo ${project_dir}
//  release产物路径
release_sdk_dir=${project_dir}/FlutterSDK/Release
echo ${release_sdk_dir}
//  debug产物路径
debug_sdk_dir=${project_dir}/FlutterSDK/Debug
echo ${debug_sdk_dir}

//  清空上次编译产物
rm -rf ${release_sdk_dir}
rm -rf ${debug_sdk_dir}

//  创建产物目录
mkdir -p ${release_sdk_dir}
mkdir -p ${debug_sdk_dir}

//  使用release证书编译
#flutter build ios

//  导出release编译产物
echo "Start exporting flutter release sdk done!"
cp -rf ${project_dir}/ios/Flutter/*.framework ${release_sdk_dir}
cp -rf ${project_dir}/ios/Flutter/flutter_assets ${release_sdk_dir}
echo "Export flutter release sdk done!"

//  使用debug证书编译
#flutter build ios --debug

//  导出debug编译产物
echo "Start exporting flutter release sdk done!"
cp -rf ${project_dir}/ios/Flutter/*.framework ${debug_sdk_dir}
cp -rf ${project_dir}/ios/Flutter/flutter_assets ${debug_sdk_dir}
echo "Export flutter debug sdk done!"
  1. 创建 pod 包含 flutter 工程 √
    创建pod:pod spec create flutter_sdk
    修改配置文件:
1
2
3
s.prepare_command = "build.sh"
s.vendored_frameworks = 'Framework/Debug/*.framework'
s.resources = 'Framework/Debug/flutter_assets'

server 编译生成产物
pod 下载产物

检查依赖 pod lib lint --verbose --no-clean --allow-warnings --sources=MobvoiPods,master 推送到仓库 pod repo push MobvoiPods flutter_test.podspec --allow-warnings --sources=MobvoiPods,master

  1. flutter_test 编译成功后将编译产物上传至 flutter_sdk_test,区分 iOS 和 Android; export.sh

  2. 构建 flutter_pod_test 自动从 flutter_sdk_test 下载iOS编译产物,区分 release 和 debug; download.sh

  3. one 工程依赖 flutter_pod_test,打包时区分 release 和 debug;

  4. 时序图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@startuml
actor Light
participant local_FlutterSDK
participant gerrit_FlutterSDK #99FF99
participant jenkins_FlutterSDK #FF9999
participant local_One

== Flutter ==

Light -> local_FlutterSDK : Develop
local_FlutterSDK -> gerrit_FlutterSDK : Push to gerrit
gerrit_FlutterSDK -> jenkins_FlutterSDK : Trigger compiling
note right #99FF99
  build_debug.sh
  build flutter debug SDK
end note
jenkins_FlutterSDK --> gerrit_FlutterSDK : Success
gerrit_FlutterSDK --> Light : Success
Light -> gerrit_FlutterSDK : Merge code
gerrit_FlutterSDK -> jenkins_FlutterSDK : Trigger compiling
note right #99FF99
  export_debug.sh
  build flutter debug SDK
  export flutter debug SDK
  update pod version
end note
jenkins_FlutterSDK --> gerrit_FlutterSDK : Success
gerrit_FlutterSDK --> Light : Success

== iOS ==

Light -> local_One : Pod update
local_One -> jenkins_FlutterSDK : Fetch flutter SDK
note right #99FF99
  FlutterSDK.podspec
  s.vendored_frameworks = 'Framework/*.framework'
  s.resources = 'Framework/flutter_assets'
end note
jenkins_FlutterSDK --> local_One : Success
@enduml

学习资料:https://github.com/alibaba/flutter-go

2019.03.22

  1. 运行flutter release 版本;
  2. 从网上下载的flutter工程需要配置dart 和 flutter SDK路径;
  3. podspec需要指定系统版本;
  4. pod lib lint 和 pod spec lint;
  5. flutter 版本需保持一致;
  6. flutter upgrade 升级;
  7. 将 App.framework 和 Flutter.framework 上传;
  8. 添加flutter编译时脚本;
  9. framework 和 static library 有什么区别;

PodFile

Root Options

install!:指定pod install时执行的方法和选项。

Dependencies

pod:指定工程的依赖。

  • :configurations => ['Debug','Release']:根据编译配置选择性安装依赖。
  • :source => 'https://github.com/CocoaPods/Specs.git':指定pod库的来源。
  • :subspecs => ['Subspec1', 'Subspec2']:指定需要安装的Subspec。
  • :path => '~/Documents/LocalPod':指定安装本地Pod。
  • :git => 'https://github.com/gowalla/AFNetworking.git', :branch => 'dev', :tag => '0.1.0':指定安装的库地址及分支或标签。
  • :podspec => https://example.com/JSONKit.podspec:指定podspec的来源。
  • :inhibit_warnings => true:屏蔽Pod中语法警告。

podspec:指定podspec文件的来源。

target 'ProjectTargetName' do ... end:指定Target的依赖。

script_phase :name => 'HelloWorldScript', :script => 'echo "Hello World"', :shell_path => '/usr/bin/ruby':指定运行脚本。

abstract_target 'AbstractTargetName' do ... end:指定抽象Target的依赖,方便多个Target继承。

inherit!:指定依赖继承。

  • :complete:完全继承父亲行为。
  • :none:不继承父亲行为。
  • :search_paths:只继承父亲库的搜索路径。

Target Configuration

platform :ios, '9.0':设置系统版本。

project 'TestProject', 'App Store' => :release, 'Test' => :debug:根据工程build configuration选择debug或release版本。

inhibit_all_warnings!:屏蔽所有Pod中语法警告。

use_modular_headers!:指定静态库使用modular headers。

use_frameworks!:指定使用frameworks而不是静态库。

Workspace

workspace 'MyWorkspace':指定workspace。

generate_bridge_support!:指定生成BridgeSupport文件。

set_arc_compatibility_flag!:指定需要添加-fobjc-arcflag。

Source

source 'https://github.com/CocoaPods/Specs.git':指定全局仓库地址。

Hooks

plugin 'cocoapods-keys', :keyring => 'Eidolon':指定安装时需要的插件。

pre_install do |installer| ... end:Pod下载好,安装前,执行。

post_install od |installer| ... end:Pod安装好,生成Xcode project时执行。

supports_swift_versions:指定所需要的Swift版本支持。

Podspec

Root specification

spec.source:指定Pod库的源地址。

  • :git => :tag, :branch, :commit, :submodules
  • :svn => :folder, :tag, :revision
  • :hg => :revision
  • :http => :flatten, :type, :sha256, :sha1

spec.prepare_command:当Pod下载好之后执行的脚本,可以用来创建、删除、修改下载好的文件。脚本会在Pod清除前和Pod工程创建前执行,目录是Pod的根目录。

Platform

spec.platform = :ios, '9.0':指定Pod支持的系统平台和版本。
spec.ios.deployment_target = '9.0':指定支持的系统平台的最低版本。

Build Settings

spec.dependency 'AFNetworking', '~> 1.0':指定依赖的第三方库。
spec.requires_arc = true:指定需要支持ARC。
spec.framework = 'QuartzCore':指定Target需要链接的系统库。
spec.library = 'xml2':指定Target需要链接的系统库。
spec.weak_framework = 'Twitter':指定Target需要weak链接的库。
spec.compiler_flags = '-DOS_OBJECT_USE_OBJC=0':传递给编译器的标识列表。
spec.prefix_header_file = false:指定是否引入公共头文件。
spec.script_phase = {}:当Pod编译时执行的脚本。

File patterns

*:匹配任何文件。
**:递归的匹配目录。
?:匹配任何一个字符。
[set]:匹配set中的任何一个字符。
{p,q}:匹配字符p或者字符q。

spec.source_files = 'Classes/**/*.{h,m}':指定Pod的源文件。
spec.public_header_files = 'Headers/Public/*.h':指定公共头文件。
spec.private_header_files = 'Headers/Private/*.h':指定私有头文件。
spec.vendored_frameworks = 'Frameworks/MyFramework.framework':与Pod一起绑定的framework。
spec.vendored_libraries = 'libProj4.a':与Pod一起绑定的静态库。
spec.resources = '[Images/*.png]':需要拷贝到Target中的资源文件。
spec.exclude_files = 'Classes/osx':需要忽略的文件。
spec.preserve_paths = 'Important.text':下载后不应该被删除的文件。