本文介绍如何使用无影云手机的iOS SDK。
1. 快速开始
1.1 获取SDK和DEMO
获取方式
下载和使用即表示您认可《无影云电脑SDK隐私权政策》。
本平台所有文档、SDK、客户端程序仅限于本人或本企业使用,未经阿里云同意不会转发给第三方个人或企业。
集成环境要求
最低支持iOS版本:10.0
由于iOS系统相关接口的开放,外设鼠标和键盘功能,要求iOS最低版本:13.4
SDK集成
将SDK解压,拷贝到自己的工程中。
关联 SDK库。
target --> Build Phases --> Link Binary With Libraries --> + --> Add Other -- Add Files -->选择对应的framework
设置Embed。
target --> General --> Frameworks, Libraries, and Embedded Content --> 找到ASPEngineSDK.framework --> Embed Without Signing
SDK权限
工程info.plist配置ASPEngineSDK需要的权限。
1. 录音权限
Privacy - Microphone Usage Description
2. 相机权限
Privacy - Camera Usage Description
SDK签名
SDK库是动态库,真机调试或发布App Store时,需要签名。
KEY文件签名
参考demo工程目录下的KEY配置进行签名,默认KEY文件是空的,可在KEY文件里配置一个签名ID,修改方法:
1. 命令修改
在命令窗口,cd工程目录,然后执行下面命令进行修改:
echo "8AEF20DC6E0CBFF4FA852118BCDD6D507F5764ED" > KEY
2. 手动修改
在工程里,找到KEY文件,以文本编辑器打开并修改。
如何获取签名ID
通过以下命令,查找自己本机安装证书的签名ID:
security find-identity -v -p codesigning
若没有本机安装证书,可以登录Apple开发者账号,下载对应证书的授权文件Profiles,下载后,选中文件右键,右键菜单选择显示简介,在预览处会显示此授权文件关联的所有证书信息,找到对应证书的信息,里面SHA-1对应的值就是我们需要签名ID。
Signing & Capabilities签名
需清空KEY文件配置的签名ID,然后在工程target里正确配置Signing & Capabilities里的证书即可。
添加签名Shell(模拟器不需要,真机需要)
target --> Build Phases --> + --> New Run Script Phase --> 填写sh内容,参考签名示例 --> 添加KEY文件,在Input Files处,点击+,填写$(SRCROOT)/KEY
注意:此Run Script必现在Embed Frameworks下面。
签名Shell示例
set -e
SHELL_PATH="${PROJECT_DIR}/ASPDemo/Lib/ASPEngineSDK/ASPEngineSDK.framework/link_and_sign.sh"
while read -r line; do
exit 0
done < ${SCRIPT_INPUT_FILE_0}
echo "===> Signing using $line"
/bin/bash -c "${SHELL_PATH}"\ "${TARGET_BUILD_DIR}"\ "${FRAMEWORKS_FOLDER_PATH}"\ "${line}"
工程配置注意事项
关闭Bitcode。
target --> Build Settings --> Build Options --> Enable Bitcode --> 设置No
1.2 对接流程
1.3 最佳实践
方案详见无影云手机快速集成最佳实践。 云手机集成的总体方案如下图所示
有多种登录方式,获取集成SDK所需要的连接云手机Ticket凭证,流程图为:
具体集成代码可以参考生命周期接口的参考代码。
2. 生命周期接口
2.1 初始化实例
+ (instancetype)buildStreamView;
2.2 建立连接
- (void)startWithTicket:(ASPConnTicket*) ticketParam;
ASPConnTicket参数说明:
属性 | 说明 |
connTicket | 连接鉴权Ticket,通过GetConnectionTicket - 获取应用连接凭证接口获取。 |
caFilePath | CA文件的绝对路径,用于TLS通信加密 |
desktopId | 请传入应用实例ID(格式:ai-0cc7s3n1iagyq****)。应用实例ID可通过调用云手机的查询实例详细信息接口获取。 |
useVpc | 是否通过企业专网进入 |
enableTls | 是否进行加密传输(建议设为YES) |
enableStatistics | 是否开启埋点(建议设为YES) |
preferRtcTransport | 是否使用rtc通道(建议设为YES) |
2.3 断开连接
- (int)stop;
2.4 暂停
暂停云端下发流和端侧流渲染
- (void)pause;
2.5 恢复
恢复云端下发流和端侧流渲染
- (void)dispose;
2.6 销毁实例
- (void)dispose;
2.7 回调说明
//云手机连接相关回调
@property (nonatomic, weak) id<ASPEngineDelegate> engineDelegate;
//分辨率发生变化回调
@property (nonatomic, weak) id<ASPEngineResolutionUpdateDelegate> resolutionUpdateDelegate;
//鼠标光标相关回调
@property (nonatomic, weak) id<ASPEngineCursorDelegate> cursorDelegate;
//云手机方向发生变化回调
@property (nonatomic, weak) id<ASPEngineOrientationUpdateDelegate> orientationUpdateDelegate;
//埋点相关回调
@property (nonatomic, weak) id<ASPEngineStatisticsDelegate> statisticsDelegate;
//输入法相关回调 云手机暂不支持
@property (nonatomic, weak) id<ASPEngineIMEDelegate> imeDelegate;
//LOG相关回调,delegate建议使用单例
+ (void)setASPEngineLogDelegate:(id<ASPEngineLogDelegate>)delegate;
+ (void)unsetASPEngineLogDelegate;
云手机连接相关回调ASPEngineDelegate
接口 | 描述 |
onConnectionSuccess:(int)connectId | 连接云手机成功回调,返回连接的标识 |
onConnectionFailureWithErrCode:(int)errCode errMsg:(NSString*)errMsg | 连接云手机失败回调,返回错误码和错误信息 |
onEngineErrorWithErrCode:(int)errCode errMsg:(NSString*)errMsg | SDK内部发生异常回调,返回错误码和错误信息 |
onDisconnected:(int)reason | 云手机连接被断开,返回被断开原因 |
onFirstFrameRendered:(long)timeCostMS | 云手机显示第一帧画面回调,返回耗时情况 |
onReconnect:(int)errorCode | 云手机连接发生重连动作,返回导致重连的错误码 |
onPolicyUpdate:(NSString *)policy | 云手机策略回调,返回策略配置 |
onUpdateNetworkQos:(AspNetworkQoS)qos | 网络服务质量情况 |
onSessionSuccess | 云手机连接会话创建成功回调 |
分辨率发生变化回调ASPEngineResolutionUpdateDelegate
接口 | 描述 |
onResolutionUpdateWithOldWidth:(int)oldWidth oldHeight:(int)oldHeight width:(int)width height:(int)height | 分辨率发生变化回调,返回旧分辨率和新分辨率 |
onMonitorsDpiConfig:(int)dpi maxSupportDpi:(int)maxSupportDpi | dpi配置和支持的最大dpi,云手机暂不支持 |
鼠标光标相关回调ASPEngineCursorDelegate
接口 | 描述 |
onCursorBitmapUpdateWithHotX:(int)hotX hotY:(int)hotY width:(int)width height:(int)height rgba:(char*)rgba | 鼠标光标数据回调,返回位置、大小和数据 |
onCursorReset | 鼠标光标发生重置 |
onCursorHide | 鼠标光标被隐藏 |
onCursorMoveWithX:(int)x y:(int)y | 鼠标光标移动位置 |
埋点相关回调ASPEngineStatisticsDelegate
接口 | 描述 |
onStatisticsInfoUpdate:(ASPStatisticsInfo *)info | 云手机性能数据回调,返回性能数据对象 |
LOG相关回调,ASPEngineLogDelegate建议使用单例
接口 | 描述 |
onLogMessage:(NSString*)msg tag:(NSString*)tag level:(AspLogLevel)level | SDK日志回调,返回日志信息、tag和级别 |
3. 业务接口
接口 | 说明 |
- (BOOL)enableStatistics:(BOOL) enabled enableGuestInfo:(BOOL)enableGuestInfo | 是否开启埋点和获取镜像cpu使用率。 |
- (BOOL)enableMouseMode:(BOOL)enabled | 是否开启鼠标模式 |
@property (nonatomic, assign) BOOL enableDump; | 是否开启dump,此功能会消耗性能,只用于开发调试,禁止上线。 |
- (BOOL)sendKeyboardEvent: (ASPKeyEvent) event; | 发送键盘按键事件。 |
// leftButton是鼠标左键还是右键,鼠标点击位置 x:0 y:0 - (BOOL)simulateMouseClick:(BOOL)leftButton; - (BOOL)simulateMouseClick:(BOOL)leftButton x:(float)x y:(float)y; | 模拟鼠标点击。 |
- (void)hideCursor; - (void)showCursor; | 隐藏和现实鼠标光标 |
- (void)setVideoProfileWithWidth:(int)width height:(int)height fps:(int)fps; | 设置分辨率。fps暂不支持 |
@property (nonatomic, assign) BOOL mute; | 设置是否静音。 |
@property (nonatomic, assign) ASPScaleType scaleType; | 画面填充模式,ASPScaleType参考本文档枚举5.1 ASPScaleType |
- (void)enableDesktopMode:(BOOL)enabled; | 设置是否以桌面模式运行,设置为enabled后,会将所有Touch消息转换为Mouse事件向服务端发送。云手机建议设置为NO。 |
- (void)enableDesktopGesture:(BOOL)enabled; | 是否使用端侧手势缩放和平移。云手机建议设置为NO。 |
@property (nonatomic, assign) BOOL enableTouchFeel; | 是否开启触感反馈,默认NO。 |
LyncChannel | 发送adb命令通道。实现参考demo的DemoLyncChannel。 |
DataChannel | 端侧与镜像接发自定义数据通道。实现参考demo的DemoEDSAgentChannel。 |
LyncChannel使用示例:
//默认命令通道LyncChannel name为static NSString *LYNC_CHANNEL_NAME = @"lync_adb_shell";
//添加LyncChannelself.lync = [[DemoLyncChannel alloc] initWithParameter:LYNC_CHANNEL_NAME];
[self.streamView addLyncChannel:self.lync];
//移除LyncChannel
[self.streamView removeLyncChannel:self.lync];
//LyncChannel实现@interface DemoLyncChannel : BaseLyncChannel@property (nonatomic, strong) NSString *lslaID;
@end@implementation DemoLyncChannel
- (void)onConnectStateChanged:(BOOL)connected {
NSLog(@"[DemoLyncChannel] onConnectStateChanged %d", connected);
if (connected) {
//TEST send cmdself.lslaID = [DemoToolBox getID];
NSString *cmdData = [DemoToolBox getJsonDataWithId:self.lslaID cmd:@"ls -la"];
NSLog(@"[DemoLyncChannel] cmdData : %@", cmdData);
LyncErrorCode code = [self sendString:cmdData];
NSLog(@"[DemoLyncChannel] cmdData code: %ld", code);
}
}
- (void)onReceiveStringData:(NSString * _Nonnull)buf {
NSLog(@"[DemoLyncChannel] onReceiveStringData %@", buf);
NSDictionary *dic = [DemoToolBox convertStringToJSON:buf];
NSString *ID = [dic objectForKey:@"id"];
if (ID != NULL && ID.length && [self.lslaID isEqualToString:ID]) {
NSLog(@"[DemoLyncChannel] onReceiveStringData get ls -la result");
}
}
- (void)onReceiveRawData:(NSData * _Nonnull)buf {
NSString *string = [[NSString alloc] initWithData:buf encoding:NSUTF8StringEncoding];
NSLog(@"[DemoLyncChannel] onReceiveRawData %@", string);
}
@end
DataChannel使用示例:
//默认DataChannel name为static NSString *DATA_CHANNEL_NAME = @"wy_vdagent_default_dc";
//添加DataChannelself.esdAgent = [[DemoEDSAgentChannel alloc] initWithParameter:DATA_CHANNEL_NAME];
[self.streamView addDataChannel:self.esdAgent];
//移除DataChannel
[self.streamView removeDataChannel:self.esdAgent];
//DataChannel实现@interface DemoEDSAgentChannel : BaseEDSAgentChannel@end@implementation DemoEDSAgentChannel
- (void)onConnectStateChanged:(ASPDCConnectState)state {
NSLog(@"[DemoEDSAgentChannel] onConnectStateChanged %ld", state);
if (state == OPEN) {
// to send data
}
}
- (void)onReceiveData:(NSData * _Nonnull)buf {
NSString *string = [[NSString alloc] initWithData:buf encoding:NSUTF8StringEncoding];
NSLog(@"[DemoEDSAgentChannel] onConnectStateChanged %@", string);
}
@end
消息通道使用datachannel,datachannel名称为wy_vdagent_default_dc。
消息大小限制500KB。
基体消息格式如下:
DataChannel 端侧发送消息给镜像 client --> guest
action:insertcontact/insertsms/sendcontactcvf
insertcontact是插入联系人
参数name是联系人名称,必填。
参数phonenumber是电话号码,必填。
insertsms是插入短信
参数phonenumber是电话号码, 必填。
参数textbody是短信内容,必填。
参数canInsertdb是否可以插入云手机短信数据库,选填,默认0, 不插入, 1为插入。
示例:let jsonarray = [{'action': 'insertcontact','importdata':[{ 'name': '**', 'phonenumber': '**' }, { 'name': '**', 'phonenumber': '**' }]},{'action': 'insertsms','importdata':[{'phonenumber':'**','textbody':'**,"canInsertdb":0/1}]}]
sendcontactvcf是通过vcf插入联系人
参数data是vcf文件数据流,必填,若文件过大,需拆包,一个包最大1MB。
参数allsize是vcf文件数据总大小,必填。
参数timestamp是时间戳,必填,一个vcf文件数据下的所有包对应一个当前时间戳,通过时间戳保证包来源于一个vcf文件。
示例:{'action': 'sendcontactvcf',importdata:{data:"", "allsize":文件数据总大小, "timestamp":时间戳}}
。
public void processByteArrayInChunks(byte[] data) {
int chunkSize = 512 * 1024; // 500KB in bytesint length = data.length;
long timestamp = System.currentTimeMillis();
for (int i = 0; i < length; i += chunkSize) {
int end = Math.min(i + chunkSize, length);
byte[] chunk = Arrays.copyOfRange(data, i, end);
String str = new String(chunk, StandardCharsets.UTF_8);
/**
{'action': 'sendcontactvcf','importdata':{"data":str, "allsize":length, "timestamp":timestamp}}
*/
}
}
String recvData = "";
int recvSize = 0;
long recvTimestamp = 0;
public void onMessageReceive(byte[] message) {
String action = getActioin(meesage);
String data = getData(message);
int length = data.length;
int allSize = getAllSize(message);
long timestamp = getTimestamp(message);
if (timestamp != recvTimestamp) {
clearRecv();
}
if (action.equals("sendcontactvcf")) {
recvSize += length;
recvData = recvData + data;
if (recvSize >= allSize) {
// recvData 写vcf文件 插入联系人
insertContact(vcfFilePath);
sendack()
clearRecv();
}
}
}
public void clearRecv() {
recvData = ""
recvSize = 0;
recvTimestamp = 0;
}
DataChannel 镜像发送消息给端侧 guest --> client
action:openeditsms/openphone/rotation
openeditsms打开编辑短信页面
参数importdata.phonenumber是电话号码,选填。
参数textbody是短信内容,选填。
示例:{'action': 'openeditsms',importdata:{"phonenumber":"***", "textbody":"***"}}
openphone打开电话拨号页面
参数importdata.phonenumber是电话号码,选填,有电话号,则打开拨打指定联系人页面,没有电话号码,则打开电话拨号页面。
示例:{'action': 'openphone','importdata':{"phonenumber":"***"}} 或 {'action': 'openphone'}
DataChannel 回执消息
action:ack
ack是指不管是端侧发送消息给镜像,还是镜像发送消息给端侧,收到消息侧,根据业务需要,可回执ack消息,表明消息处理的情况
参数source是接收到消息的action,必填。
参数code是状态码,必填值为0表示未知,1 执行source,2 执行成功,3 执行失败, 4 不支持执行。
示例:{'action': 'ack','importdata':{"source":"***", "code": *,"id":*}}
4. 参数详细说明
4.1 ASPConnTicket
建立连接的配置参数。
属性 | 说明 |
connTicket | 连接鉴权Ticket,通过GetConnectionTicket - 获取应用连接凭证接口获取。 |
caFilePath | CA文件的绝对路径,用于TLS通信加密 |
desktopId | 请传入应用实例ID(格式:ai-0cc7s3n1iagyq****)。应用实例ID可通过调用云手机的查询实例详细信息接口获取。 |
useVpc | 是否通过企业专网进入 |
enableTls | 是否进行加密传输(建议设为YES) |
enableStatistics | 是否开启埋点(建议设为YES) |
preferRtcTransport | 是否使用rtc通道(建议设为YES) |
4.2 ASPStatisticsInfo
性能数据
接口 | 类型 | 说明 |
mReceiveFps | int | 接收到的帧率 |
mRenderFps | int | 渲染帧率 |
mDownstreamBandwithMBPerSecond | double | 下行带宽 |
mUpstreamBandwithMBPerSecond | double | 上行带宽 |
mP2pFullLinkageLatencyMS | long | 端到端全链路时延,已废弃 |
mNetworkLatencyMS | long | 网络rtt时延 |
mPingGatewayRtt | long | ping rtt时延 |
mLostRate | double | 丢包率 |
mServerRenderLatencyMS | long | 云侧渲染延迟 |
mServerEncoderLatencyMS | long | 云侧编码延迟 |
mServerTotalLatencyMS | long | 云侧总延迟 |
accumulateBandwidth | long | 总带宽 |
mGuestCpuUsage | long | 镜像CPU使用率 |
mStreamType | String | 流协议类型 |
infoDic | NSDictionary | 性能数据Map |
5. 枚举类型
5.1 ASPScaleType
流化图像内容缩放处理类型。
名称 | 含义 |
ASPScaleTypeFill | 总是将流化图像拉伸至与StreamView相同大小。当StreamView的长宽比例与流化图像长宽比例不相等时。采用该策略可能导致图像有明显的变形 |
ASPScaleTypeFit | 对StreamView的渲染区域进行调整,使得StreamView总是能以相同的长宽比例渲染流化图像内容。采用该策略时,流化图像可能无法填满整个StreamView |
6. 错误代码
错误码 | 错误消息( | 定义模块 | 原因 |
2~26主要是网络相关问题 | |||
2 | 连接%s失败 | ASP SDK | 无效MAGIC |
3 | 连接%s失败 | ASP SDK | 数据有误 |
4 | 客户端与服务端版本不匹配 | ASP SDK | 版本不匹配 |
5 | 连接需要 TLS | ASP SDK | 需要TLS |
6 | 连接不需要 TLS | ASP SDK | 不需要TLS而实际使用了TLS |
7 | 您没有权限连接当前%s | ASP SDK | 权限问题 |
8 | ASP SDK | 迁移过程中client ID无效 | |
9 | 连接%s失败 | ASP SDK | channel不存在 |
20 | 连接ASP服务器失败。 | ASP SDK | channel 连接错误 |
21 | TLS认证出错了 | ASP SDK | TLS 认证错误 |
22 | 连接%s失败 | ASP SDK | channel link 错误 |
23 | 连接%s失败 | ASP SDK | 连接认证错误 |
24 | 连接%s失败 | ASP SDK | 连接IO错误 |
25 | 连接%s失败 | ASP SDK | Ticket校验失败。用户连接被断开后,若使用同一个Ticket再次请求建连,也将触发此错误。 |
26 | ASP SDK | xquic 握手失败 | |
连接中断开或遇到某些错误的情况 | |||
2000 | 获取%s数据超时,与服务端断开连接 | ASP SDK | 正常断开 |
2001 | %s已与服务端断开连接。可能是因为%s进程已被强制终止。 | ASP SDK | 一般是端上应用被终止进程了,如Android应用用户通过按下Home键终止 |
2002 | 已有用户从其他终端连接当前%s。请稍后重试。 | ASP SDK | 其他人抢占了端 |
2003 | %s正在关机或重启,一般由管理员操作,请稍后重试。 | ASP SDK | 云手机被关机或重启,一般是管理员操作 |
2004 | 当前用户连接被断开。 | ASP SDK | 客户端发起断流,或服务端发起踢人或断流 |
2005 | %s已超时断开连接,因为已达到管理员设置的使用时长限制。 | ASP SDK | 管理员设置使用时长被关 |
2010 | 连接%s失败 | ASP SDK | Vdagent连接失败 |
2011 | 连接参数传递错误。 | ASP SDK | 连接server参数传递错误 |
2027 | 拉流模式已切换。 | ASP SDK | 拉流模式由抢占模式切换为协同模式,或由协同模式切换为抢占模式 |
2100 | 剪切板权限,禁止从%s到本地 | ASP SDK | 剪贴板权限,禁止从VM到本地 |
2101 | 剪切板权限,禁止从本地到%s | ASP SDK | 剪贴板权限,禁止从本地到 VM |
2200 | %s正在尝试重连... | ASP SDK | 因为网络问题断开了,ASP SDK正在重连 |
2201 | 您的设备出现网络异常,导致%s已断开连接。 | ASP SDK | 因为网络问题断开了,ASP SDK因为镜像原因不支持重连,应用侧开始重连 |
2202 | %s重连超时,请检查设备网络后重试。 | ASP SDK | ASP SDK重连超时 |
端侧逻辑错误 | |||
5100 | %s连接ASP Server超时,请稍后重试。 | 应用侧 | 端侧在一段时间内未接收到connected事件 |
5102 | 获取%s数据超时,请稍后重试。 | 应用侧 | 端侧在一段时间内接收到了connected但未接收到display事件 |
5004 | 客户端出现错误,请重新打开 | 应用侧 | 传入端侧启动参数有误,一般出现在开发阶段 |
5200 | 客户端重连超时,请稍后重试。 | 应用侧 |
7. 常见问题
如何重启云手机
调用管控重启API 重启实例 进行重启,调用重启API后,端侧连接的云手机会断开,重启完成后,端侧再连接云手机。
常用ADB命令
功能 | 命令 |
返回键 | input keyevent KEYCODE_BACK |
Home键 | input keyevent KEYCODE_HOME |
切换键 | input keyevent KEYCODE_APP_SWITCH |
静音 | input keyevent 164 |
音量增大 | input keyevent KEYCODE_VOLUME_UP |
音量减小 | input keyevent KEYCODE_VOLUME_DOWN |
隐藏导航栏 | setprop persist.wy.hasnavibar false; killall com.android.systemui |
显示导航栏 | setprop persist.wy.hasnavibar true; killall com.android.systemui |
截图 | screencap -p /sdcard/Download/abc.png |