Android 经典蓝牙开发

本文主要讲解经典蓝牙的开发,主要包含以下几个知识点:

蓝牙 API 简介
经典蓝牙开发的一般步骤
相信通过以上步骤,您会很快上手一个 Android 经典蓝牙开发的 App。

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

BluetoothAdapter 本地蓝牙适配器
BluetoothClass 蓝牙类(主要包括服务和设备)
BluetoothClass.Device 蓝牙设备类
BluetoothClass.Device.Major 蓝牙设备管理
BluetoothClass.Service 蓝牙服务类
BluetoothDevice 蓝牙设备(远程蓝牙设备)
BluetoothServiceSocket 监听蓝牙连接的类
BluetoothSocket 蓝牙连接类

BluetoothAdapter
表示本地的蓝牙适配器 (蓝牙射频)。BluetoothAdapter 是为所有蓝牙交互的入口点。它可以发现其他蓝牙设备、 查询绑定 (配对) 设备的列表、 实例化已知的 MAC 地址的 BluetoothDevice(蓝牙设备) 和创建 BluetoothServerSocket 用于侦听来自其他设备的通信。直到我们建立 BluetoothSocket 连接之前,都要不断操作它 。BluetoothAdapter 里的方法很多,常用的有以下几个:

// 根据字面意思,是取消发现,也就是说当我们正在搜索设备的时候调用这个方法将不再继续搜索;
cancelDiscovery();
// 关闭蓝牙
disable()
// 打开蓝牙
enable();
// 这个方法打开蓝牙不会弹出提示,更多的时候我们需要问下用户是否打开,
// 以下两行代码同样是打开蓝牙,不过会提示用户:
Intemtenabler = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
// 同 startActivity(enabler);
startActivityForResult(enabler , reCode);
// 获取本地蓝牙地址
getAddress();
// 获取默认 BluetoothAdapter,实际上,也只有这一种方法获取 BluetoothAdapter。
getDefaultAdapter();
// 获取本地蓝牙名称
getName();
// 根据蓝牙地址获取远程蓝牙设备
getRemoteDevice(String address);
// 获取本地蓝牙适配器当前状态(调试的时候更需要)
getState();
// 判断当前是否正在查找设备,如果是则返回 true
isDiscovering();
// 判断蓝牙是否打开,已打开返回 true,否则,返回 false。
isEnabled();
// 根据名称,UUID 创建并返回 BluetoothServerSocket 对象,
// 这是创建 BluetoothSocket 服务器端的*步。
// *个参数表示蓝牙服务的名称,可以是任意字符串,第二个参数是 UUID。
listenUsingRfcommWithServiceRecord(String name , UUID uuid);
// 开始搜索,这是搜索的*步
startDiscovery();

BluetoothDevice
表示远程蓝牙设备。使用此类并通过 BluetoothSocket 类可以请求连接远程设备,或查询这台设备的信息如其名称、 地址、 类和绑定状态。

createRfcommSocketToServiceRecord(UUID uuid);
1
根据 UUID 创建并返回一个 BluetoothSocket , 这个方法也是我们获取 BluetoothDevice 的目的——创建 BluetoothSocket。这个类其他的方法,如 getAddress()、getName(),同 BluetoothAdapter。

备注: 蓝牙—RFCOMM 协议
串口仿真协议(RFCOMM),RFCOMM 是一个简单的协议,其中针对 9 针 RS-232 串口仿真附加了部分条款,可支持在两个蓝牙设备之间同时保持高达 60 路的通信连接。RFCOMM 的目的是针对如何在两个不同设备上的应用之间保证一条完整的通信路径。

BluetoothServerSocket
表示打开服务器套接字侦听传入的请求 (类似于 TCP ServerSocket)。为了连接两台 Android 设备,一台设备必须用此类打开一个服务器套接字。当远程蓝牙设备向此设备发出连接请求时,而且当连接被接收时,BluetoothServerSocket 将返回连接的 BluetoothSocket。这个类有三个方法。

// 两者的区别在于后者指定了过时时间,
// 需要注意的是,执行这两个方法的时候,直到接收到了客户端的请求
//(或是过期之后),都会阻塞线程,应该放在新线程里运行!
// 还需要注意,这两个方法都返回一个 BluetoothSocket,
// *后的连接也是服务器端与客户端这两个 BluetoothSocket 的连接。
accept();
accept(int timeout);
// 关闭
close();

BluetoothSocket
跟 BluetoothServerSocket 相对,是客户端。表示一个蓝牙套接字 (类似于 TCP Socket) 的接口。这是一个允许应用程序与另一台蓝牙设备通过 InputStream和 OutputStream 来交换数据的连接点。其一共5个方法,一般都会用到。

// 关闭
close();
// 连接
connect();
// 获取输入流
getInptuStream();
// 获取输出流
getOutputStream();
// 获取远程设备,这里指的是获取 BluetoothSocket 指定连接的那个远程蓝牙设备
getRemoteDevice();

BluetoothClass
描述的一般特征和蓝牙设备的功能。这是一整套只读的属性,用于定义设备的主要和次要设备类和它的服务。然而,这并不是支持所有蓝牙配置文件和服务的设备,但很适用于获取设备类型。

BluetoothProfile
表示一个蓝牙配置文件。蓝牙配置文件是基于蓝牙通信设备之间的无线接口规范。如免提规范 (Hands-Free profile)

BluetoothHeadset
蓝牙耳机与手机一起使用配置文件 ,这包括蓝牙耳机和免提(v1.5) 的配置文件

BluetoothA2dp
定义了如何高质量的音频可以进行流式处理,从一个设备到另一个通过蓝牙连接。”A2DP” 代表先进音频分配协议

BluetoothHealth
表示控制蓝牙服务健康设备协议。

BluetoothHealthCallback
BluetoothHealthCallback 是一个抽象类,您使用它来实现 BluetoothHealth 回调,你必须扩展此类并实现回调方法以接收有关更改的更新应用程序的注册和蓝牙通道状态。

BluetoothHealthAppConfiguration
表示一个蓝牙健康第三方应用程序注册与远程蓝牙健康设备进行通信的应用程序配置。

BluetoothProfile.ServiceListener
通知 BluetoothProfile IPC 客户端界面时已被连接或断开服务 (即运行一个特定的配置文件内部服务)

UUID(universal unique identifier , 全局唯一标识符)
格式如下:
UUID 格式一般是”xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx”。
UUID 分为 5 段,是一个 8-4-4-4-12 的字符串,这个字符串要求永不重复。

String uuid = java.util.UUID.randomUUID().toString();

一般在创建 Socket 时需要 UUID 作为端口的唯一性,如果两台 Android 设备互联,则没有什么特殊的,如果让非 Android 的蓝牙设备连接 Android 蓝牙设备,则 UUID 必须使用某个固定保留的 UUID。

Android 中创建 UUID 的方式如下:

UUID uuid = UUID.fromString(“00001101-0000-1000-8000-00805F9B34FB”);

常用固定的 UUID

蓝牙串口服务 ( SPP ):

SerialPortServiceClass_UUID = ‘{00001101-0000-1000-8000-00805F9B34FB}’
LANAccessUsingPPPServiceClass_UUID = ‘{00001102-0000-1000-8000-00805F9B34FB}’

拨号网络服务:

DialupNetworkingServiceClass_UUID = ‘{00001103-0000-1000-8000-00805F9B34FB}’

信息同步服务:

IrMCSyncServiceClass_UUID = ‘{00001104-0000-1000-8000-00805F9B34FB}’
SDP_OBEXObjectPushServiceClass_UUID = ‘{00001105-0000-1000-8000-00805F9B34FB}’

文件传输服务:

OBEXFileTransferServiceClass_UUID = ‘{00001106-0000-1000-8000-00805F9B34FB}’
IrMCSyncCommandServiceClass_UUID = ‘{00001107-0000-1000-8000-00805F9B34FB}’
SDP_HeadsetServiceClass_UUID = ‘{00001108-0000-1000-8000-00805F9B34FB}’
CordlessTelephonyServiceClass_UUID = ‘{00001109-0000-1000-8000-00805F9B34FB}’
SDP_AudioSourceServiceClass_UUID = ‘{0000110A-0000-1000-8000-00805F9B34FB}’
SDP_AudioSinkServiceClass_UUID = ‘{0000110B-0000-1000-8000-00805F9B34FB}’
SDP_AVRemoteControlTargetServiceClass_UUID = ‘{0000110C-0000-1000-8000-00805F9B34FB}’
SDP_AdvancedAudioDistributionServiceClass_UUID = ‘{0000110D-0000-1000-8000-00805F9B34FB}’
SDP_AVRemoteControlServiceClass_UUID = ‘{0000110E-0000-1000-8000-00805F9B34FB}’
VideoConferencingServiceClass_UUID = ‘{0000110F-0000-1000-8000-00805F9B34FB}’
IntercomServiceClass_UUID = ‘{00001110-0000-1000-8000-00805F9B34FB}’

蓝牙传真服务:

FaxServiceClass_UUID = ‘{00001111-0000-1000-8000-00805F9B34FB}’
HeadsetAudioGatewayServiceClass_UUID = ‘{00001112-0000-1000-8000-00805F9B34FB}’
WAPServiceClass_UUID = ‘{00001113-0000-1000-8000-00805F9B34FB}’
WAPClientServiceClass_UUID = ‘{00001114-0000-1000-8000-00805F9B34FB}’

蓝牙打印服务:

HCRPrintServiceClass_UUID = ‘{00001126-0000-1000-8000-00805F9B34FB}’
HCRScanServiceClass_UUID = ‘{00001127-0000-1000-8000-00805F9B34FB}’
CommonISDNAccessServiceClass_UUID = ‘{00001128-0000-1000-8000-00805F9B34FB}’
VideoConferencingGWServiceClass_UUID = ‘{00001129-0000-1000-8000-00805F9B34FB}’
UDIMTServiceClass_UUID = ‘{0000112A-0000-1000-8000-00805F9B34FB}’
UDITAServiceClass_UUID = ‘{0000112B-0000-1000-8000-00805F9B34FB}’
AudioVideoServiceClass_UUID = ‘{0000112C-0000-1000-8000-00805F9B34FB}’
SIMAccessServiceClass_UUID = ‘{0000112D-0000-1000-8000-00805F9B34FB}’
PnPInformationServiceClass_UUID = ‘{00001200-0000-1000-8000-00805F9B34FB}’
GenericNetworkingServiceClass_UUID = ‘{00001201-0000-1000-8000-00805F9B34FB}’
GenericFileTransferServiceClass_UUID = ‘{00001202-0000-1000-8000-00805F9B34FB}’
GenericAudioServiceClass_UUID = ‘{00001203-0000-1000-8000-00805F9B34FB}’
GenericTelephonyServiceClass_UUID = ‘{00001204-0000-1000-8000-00805F9B34FB}’

个人局域网服务:

PANUServiceClass_UUID = ‘{00001115-0000-1000-8000-00805F9B34FB}’
NAPServiceClass_UUID = ‘{00001116-0000-1000-8000-00805F9B34FB}’
GNServiceClass_UUID = ‘{00001117-0000-1000-8000-00805F9B34FB}’
DirectPrintingServiceClass_UUID = ‘{00001118-0000-1000-8000-00805F9B34FB}’
ReferencePrintingServiceClass_UUID = ‘{00001119-0000-1000-8000-00805F9B34FB}’
ImagingServiceClass_UUID = ‘{0000111A-0000-1000-8000-00805F9B34FB}’
ImagingResponderServiceClass_UUID = ‘{0000111B-0000-1000-8000-00805F9B34FB}’
ImagingAutomaticArchiveServiceClass_UUID = ‘{0000111C-0000-1000-8000-00805F9B34FB}’
ImagingReferenceObjectsServiceClass_UUID = ‘{0000111D-0000-1000-8000-00805F9B34FB}’
SDP_HandsfreeServiceClass_UUID = ‘{0000111E-0000-1000-8000-00805F9B34FB}’
HandsfreeAudioGatewayServiceClass_UUID = ‘{0000111F-0000-1000-8000-00805F9B34FB}’
DirectPrintingReferenceObjectsServiceClass_UUID = ‘{00001120-0000-1000-8000-00805F9B34FB}’
ReflectedUIServiceClass_UUID = ‘{00001121-0000-1000-8000-00805F9B34FB}’
BasicPringingServiceClass_UUID = ‘{00001122-0000-1000-8000-00805F9B34FB}’
PrintingStatusServiceClass_UUID = ‘{00001123-0000-1000-8000-00805F9B34FB}’

人机输入服务:

HumanInterfaceDeviceServiceClass_UUID = ‘{00001124-0000-1000-8000-00805F9B34FB}’
HardcopyCableReplacementServiceClass_UUID = ‘{00001125-0000-1000-8000-00805F9B34FB}’

经典蓝牙使用步骤详解
配置权限
<uses-permission android:name=”android.permission.BLUETOOTH”/>
<uses-permission android:name=”android.permission.BLUETOOTH_ADMIN”/>

Android 6.0 之后,如果需要利用本机查找周围的 WiFi 和蓝牙设备,需要在配置文件中申请两个权限:

<uses-permission android:name=”android.permission.ACCESS_COARSE_LOCATION”/>
<uses-permission android:name=”android.permission.ACCESS_FINE_LOCATION”/>

并在 java 代码中动态申请 runtime 权限:

if (Build.VERSION.SDK_INT >= 6.0) {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
MY_PERMISSION_REQUEST_CONSTANT);
}

public void onRequestPermissionsResult(int requestCode, String permissions[],
int[] grantResults) {
switch (requestCode) {
case MY_PERMISSION_REQUEST_CONSTANT: {
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0 && grantResults[0]
== PackageManager.PERMISSION_GRANTED) {
//permission granted!
}
return;
}
}
}

获取本地蓝牙适配器
BluetoothAdapter mAdapter= BluetoothAdapter.getDefaultAdapter();

打开蓝牙
// 弹出对话框提示用户是否打开,REQUEST_ENABLE 为自定义请求码
Intent enabler = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enabler, REQUEST_ENABLE);

// 不做提示,强行打开
// mAdapter.enable();

// 补充一下,使设备能够被搜索,REQUEST_DISCOVERABLE 为自定义请求码
Intent enabler = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
startActivityForResult(enabler,REQUEST_DISCOVERABLE);

搜索设备
// 1) mAdapter.startDiscovery(); 是*步
// 判断是否在搜索,如果在搜索,就取消搜索
if (mBluetoothAdapter.isDiscovering()) {
mBluetoothAdapter.cancelDiscovery();
}
// 开始搜索
mBluetoothAdapter.startDiscovery();
// 可是你会发现没有返回的蓝牙设备,怎么知道查找到了呢?
// 2) 所以有第二步定义 BroadcastReceiver 接收搜索结果,代码如下
BroadcastReceiver mReceiver = new BroadcastReceiver() {
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.v(TAG, “find device:” + device.getName()+ device.getAddress());
}
}
//搜索完成
else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action))
{
Log.v(TAG,”find over”);
}
//执行更新列表的代码
// updateList();
}
};

这样,没查找到新设备或是搜索完成,相应的操作都在上段代码的两个 if 语句里执行了,不过前提是你要先注册
BroadcastReceiver,具体代码如下,该段代码,一般写在 onCreate() 里。

// Register the BroadcastReceiver
IntentFilter filter = new IntentFilter();
filter.addAction(BluetoothDevice.ACTION_FOUND);// 搜索发现设备
filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);// 结束搜索设备
filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);// 状态改变
filter.addAction(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED);// 行动扫描模式改变了
filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);// 动作状态发生了变化
registerReceiver(mReceiver, filter);

当然,记得反注册哦

// Don’t forget to unregister during onDestroy
unregisterReceiver(mReceiver);

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

// 1)服务器端:
BluetoothServerSocket serverSocket =
mAdapter.listenUsingRfcommWithServiceRecord(serverSocketName,UUID);
serverSocket.accept();

// 2)客户端:
// device 为刚才在 BroadcastReceiver 中获取到的 BLuetoothDevice 对象
// 方式一:使用 UUID 形式直接连接
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();

// 方式二:使用反射机制
BluetoothSocket temp = null;
try {
// 利用反射机制
Method m = mBluetoothDevice.getClass()
.getMethod(“createRfcommSocket”, int.class);
// 这里端口默认为 1
temp = (BluetoothSocket) m.invoke(mBluetoothDevice, 1);
} catch (SecurityException | NoSuchMethodException
| IllegalArgumentException| IllegalAccessException
| InvocationTargetException e) {
Log.e(TAG, “initSocket”, e);
}
socket = temp;

关于建立连接的一个比较实用,可操作性较强的完整写法可以是:

// 定义内部类
private boolean connecting;
private class ConnectThread extends Thread {
String macAddress;

private ConnectThread(String mac) {
macAddress = mac;
}

public void run() {
connecting = true;
boolean connected = false;
if (mBluetoothAdapter == null) {
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
}
mBluetoothDevice = mBluetoothAdapter.getRemoteDevice(macAddress);
mBluetoothAdapter.cancelDiscovery();
initSocket();
while (!connected && connTime <= 10) {
try {
assert socket != null;
socket.connect();
connected = true;
} catch (IOException e1) {
connTime++;
connected = false;
try {
initSocket();
assert socket != null;
socket.connect();
} catch (IOException e) {
Log.e(TAG, “ConnectThread”, e);
}
// 关闭 socket
try {
socket.close();
socket = null;
} catch (IOException e2) {
Log.e(TAG, “ConnectThread”, e2);
}
} finally {
connecting = false;
}
// connectDevice();
}
// 重置 ConnectThread
// synchronized (BluetoothService.this) {
// ConnectThread = null;
//}
}

public void cancel() {
try {
socket.close();
socket = null;
} catch (Exception e) {
Log.e(TAG, “ConnectThread”, e);
} finally {
connecting = false;
}
}
}

初始化 socket 的代码为:

/**
* 取得 BluetoothSocket
*/
private void initSocket() {
BluetoothSocket temp = null;
try {
// 利用反射机制
Method m = mBluetoothDevice.getClass()
.getMethod(“createRfcommSocket”, int.class);
// 这里端口为 1
temp = (BluetoothSocket) m.invoke(mBluetoothDevice, 1);
} catch (SecurityException | NoSuchMethodException
|IllegalArgumentException| IllegalAccessException
| InvocationTargetException e) {
Log.e(TAG, “initSocket”, e);
}
socket = temp;
}

以上连接线程的使用方式:

new ConnectThread(device.getAddress()).start();

启动一个线程,并传入搜索到的设备的 mac 地址即可发起连接请求。

数据传输
连接成功后,可以通过 Java 流的知识做一些类似文件传输、局域网聊天等操作。

// step 1: 获取流
inputStream = socket.getInputStream();
outputStream = socket.getOutputStream();
// step 2: 写出、读入(JAVA 常规操作)这里就不赘述

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

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);
}

好了,通过以上解析,基本可以开发一款经典蓝牙应用了,下次再和大家分享更多关于 BLE(低功耗蓝牙) 开发相关实用的知识。

iOS-WKWebView取消自动选中灰色背景

在IOS中WKWebView有些地方tap点击会有一个灰色背景图层出现,会让用户感觉是个bug.

%title插图%num
-webkit-tap-highlight-color这个属性只用于iOS (iPhone和iPad)。当你点击一个链接或者通过Javascript定义的可点击元素的时候,它就会出现一个半透明的灰色背景。要重设这个表现,你可以设置-webkit-tap-highlight-color为任何颜色。想要禁用这个高亮,设置颜色的alpha值为0即可。

示例:

//设置高亮色为50%透明的红色:
-webkit-tap-highlight-color: rgba(255,0,0,0.5);

//设置高亮色透明
-webkit-tap-highlight-color: rgba(0,0,0,0);

解决方案:

//swift代码
//去除wkwebview tap选中颜色
//script1 和script2都可以
func disable_webkit_tap_highlight_color() {
//let script1= “function addStyleString(str) {” + “var node = document.createElement(‘style’);” + “node.innerHTML = str;” + “document.body.appendChild(node);” + “}” + “addStyleString(‘* {-webkit-tap-highlight-color: rgba(0,0,0,0);}’);”
let script2= “document.body.style.webkitTapHighlightColor=’transparent’;”
self.webView.evaluateJavaScript(script2, completionHandler: nil)
}

 

iOS开发-APP启动main()调用之前的加载过程

main()调用之前的加载过程
App开始启动后,系统首先加载可执行文件(自身App的所有.o文件的集合),然后加载动态链接库dyld。
dyld是一个专门用来加载动态链接库的库。
dyld源码链接
执行从dyld开始,dyld从可执行文件的依赖开始, 递归加载所有的依赖动态链接库。

动态链接库包括:
iOS 中用到的所有系统 framework
加载OC runtime方法的libobjc,
系统级别的libSystem,例如libdispatch(GCD)和libsystem_blocks (Block)。

其实无论对于系统的动态链接库还是对于App本身的可执行文件而言,他们都算是image(镜像),而每个App都是以image(镜像)为单位进行加载的。

什么是image(镜像)
1.executable可执行文件 比如.o文件。
2.dylib 动态链接库 framework就是动态链接库和相应资源包含在一起的一个文件夹结构。
3.bundle 资源文件 只能用dlopen加载,不推荐使用这种方式加载。

除了我们App本身的可行性文件,系统中所有的framework比如UIKit、Foundation等都是以动态链接库的方式集成进App中的。

系统使用动态链接有几点好处:
代码共用:很多程序都动态链接了这些 lib,但它们在内存和磁盘中中只有一份。 易于维护:由于被依赖的 lib 是程序执行时才链接的,所以这些 lib 很容易做更新,比如libSystem.dylib 是 libSystem.B.dylib 的替身,哪天想升级直接换成libSystem.C.dylib 然后再替换替身就行了。 减少可执行文件体积:相比静态链接,动态链接在编译时不需要打进去,所以可执行文件的体积要小很多。

如上图所示,不同进程之间共用系统dylib的_TEXT区,但是各自维护对应的_DATA区。

所有动态链接库和我们App中的静态库.a和所有类文件编译后的.o文件*终都是由dyld(the dynamic link editor),Apple的动态链接器来加载到内存中。每个image都是由一个叫做ImageLoader的类来负责加载(一一对应),那么ImageLoader又是什么呢?

什么是ImageLoader
image 表示一个二进制文件(可执行文件或 so 文件),里面是被编译过的符号、代码等,所以 ImageLoader 作用是将这些文件加载进内存,且每一个文件对应一个ImageLoader实例来负责加载。
两步走: 在程序运行时它先将动态链接的 image 递归加载 (也就是上面测试栈中一串的递归调用的时刻)。 再从可执行文件 image 递归加载所有符号。

当然所有这些都发生在我们真正的main函数执行前。

动态链接库加载的具体流程
动态链接库的加载步骤具体分为5步:
1.load dylibs image 读取库镜像文件
2.Rebase image
3.Bind image
4.Objc setup
5.initializers

1.load dylibs image
在每个动态库的加载过程中, dyld需要:
1.分析所依赖的动态库
2.找到动态库的mach-o文件
3.打开文件
4.验证文件
5.在系统核心注册文件签名
6.对动态库的每一个segment调用mmap()
通常的,一个App需要加载100到400个dylibs, 但是其中的系统库被优化,可以很快的加载。

针对这一步骤的优化有:
1.减少非系统库的依赖
2.合并非系统库
3.使用静态资源,比如把代码加入主程序

2.rebase/bind
由于ASLR(address space layout randomization)的存在,可执行文件和动态链接库在虚拟内存中的加载地址每次启动都不固定,所以需要这2步来修复镜像中的资源指针,来指向正确的地址。

rebase修复的是指向当前镜像内部的资源指针;
而bind指向的是镜像外部的资源指针。

rebase步骤先进行,需要把镜像读入内存,并以page为单位进行加密验证,保证不会被篡改,所以这一步的瓶颈在IO。
bind在其后进行,由于要查询符号表,来指向跨镜像的资源,加上在rebase阶段,镜像已被读入和加密验证,所以这一步的瓶颈在于CPU计算。

//通过命令行可以查看相关的资源指针:
xcrun dyldinfo -rebase -bind -lazy_bind myApp.App/myApp
1
2
优化该阶段的关键在于减少__DATA segment中的指针数量。

可以优化的点有:

1.减少Objc类数量, 减少selector数量
2.减少C++虚函数数量
3.转而使用swift stuct(其实本质上就是为了减少符号的数量)

3.Objc setup
这一步主要工作是:
1.注册Objc类 (class registration)
2.把category的定义插入方法列表 (category registration)
3.保证每一个selector唯一 (selctor uniquing)
4.由于之前2步骤的优化,这一步实际上没有什么可做的。

4.initializers
以上三步属于静态调整(fix-up),都是在修改__DATA segment中的内容,而这里则开始动态调整,开始在堆和堆栈中写入内容。 在这里的工作有:

1.Objc的+load()函数
2.C++的构造函数属性函数 形如attribute((constructor)) void DoSomeInitializationWork()
3.非基本类型的C++静态全局变量的创建(通常是类或结构体)(non-trivial initializer) 比如一个全局静态结构体的构建,如果在构造函数中有繁重的工作,那么会拖慢启动速度
Objc的load函数和C++的静态构造函数采用由底向上的方式执行,来保证每个执行的方法,都可以找到所依赖的动态库。

1).dyld 开始将程序二进制文件初始化
2).交由 ImageLoader 读取 image,其中包含了我们的类、方法等各种符号
3).由于 runtime 向 dyld 绑定了回调,当 image 加载到内存后,dyld 会通知 runtime 进行处理
4).runtime 接手后调用 mapimages 做解析和处理,接下来 loadimages 中调用 callloadmethods 方法,遍历所有加载进来的 Class,按继承层级依次调用 Class 的 +load 方法和其 Category 的 +load 方法

至此
至此,可执行文件中和动态库所有的符号(Class,Protocol,Selector,IMP,…)都已经按格式成功加载到内存中,被 runtime 所管理,再这之后,runtime 的那些方法(动态添加 Class、swizzle 等等才能生效)。

整个事件由 dyld 主导,完成运行环境的初始化后,配合 ImageLoader 将二进制文件按格式加载到内存, 动态链接依赖库,并由 runtime 负责加载成 objc 定义的结构,所有初始化工作结束后,dyld 调用真正的 main 函数。

iOS 审核被拒*问题汇总

iOS 审核被拒*问题汇总
iOS 审核被拒*问题汇总
苹果审核概述
App被拒*常见原因概述
被拒解决方案概述
苹果开发者 条款详细解读
1. Terms and conditions(法律与条款)
Guideline 1.1 问题
Guideline 1.1 – Safety – Objectionable Content
1.1.6 包含虚假信息,功能或误导性元数据。
Guideline 1.2 问题
Guideline 1.2 问题分析
2. Functionality(功能)
Guideline 2.1 问题 (App 完成度)
Guideline 2.1 – Performance – App Completeness
Guideline 2.2 问题
Guideline 2.3 问题 (准确的元数据)
2.3.0 批准后进行重大概念变更
2.3.1 有隐藏或未记录的功能,包括重定向到赌博或彩票网站的隐藏“开关”
Guideline 2.3.7 – Performance – Accurate Metadata (性能 – 准确的元数据)
Guideline 2.4 问题
2.4.1 性能 – 硬件兼容性 问题
Guideline 2.5 问题 (软件要求)
2.5
Guideline 2.6 问题
Guideline 2.7 问题
Guideline 2.8 问题
Guideline 2.9 问题
Guideline 2.10 问题
Guideline 2.11 问题
Guideline 2.12 问题
Guideline 2.13 问题
Guideline 2.14 问题
Guideline 2.15 问题
Guideline 2.16 问题
Guideline 2.17 问题
Guideline 2.18 问题
Guideline 2.19 问题
Guideline 2.20 问题
Guideline 2.21 问题
Guideline 2.22 问题
Guideline 2.23 问题
Guideline 2.24 问题
Guideline 2.25 问题
3. Metadata (name, descriptions, ratings, rankings, etc)(描述数据(名称,描述,评级,分类等))
Guideline 3.0 – Business
Guideline 3.1 问题 (购买项目)
Guideline 3.1.1 – Business – Payments – In-App Purchase
3.2.1 可接受的商业模式
Guideline 3.2 问题
Guideline 3.3 问题
Guideline 3.4 问题
Guideline 3.5 问题
Guideline 3.6 问题
Guideline 3.7 问题
Guideline 3.8 问题
Guideline 3.9 问题
Guideline 3.10 问题
Guideline 3.11 问题
Guideline 3.12 问题
4. Location(位置)
Guideline 4.1 问题
Guideline 4.2 问题 (*低功能要求)
Guideline 4.3 问题 (重复 App)
4.3.0 马甲包问题
Guideline 4.4 问题
5. Push notifications(提醒推送)
Guideline 5.1问题 (数据收集和存储)
5.1.1 Guideline 5.1.1 – Legal – Privacy – Data Collection and Storage
5.1.5 定位服务 问题
Guideline 5.2 问题 (知识产权)
5.2.1 法律 – 知识产权
Guideline 5.3 问题
5.3.4 没有使用应用程序的所有位置的必要许可和权限
Guideline 5.4 问题
Guideline 5.5 问题
Guideline 5.6 问题
Guideline 5.7 问题
Guideline 5.8 问题
Guideline 5.9 问题
6. Game Center(游戏中心)
Guideline 6.1 问题
Guideline 6.2 问题
Guideline 6.3 问题
Guideline 6.4 问题
Guideline 6.5 问题
Guideline 6.6 问题
Guideline 6.7 问题
7. Advertising(广告)
Guideline 7.1 问题
Guideline 7.2 问题
Guideline 7.3 问题
8. Trademarks and trade dress(商标权与商标外观)
Guideline 8.1 问题
Guideline 8.2 问题
Guideline 8.3 问题
Guideline 8.4 问题
Guideline 8.5 问题
9. Media content(媒体内容)
Guideline 9.1 问题
Guideline 9.2 问题
Guideline 9.3 问题
Guideline 9.4 问题
10. User interface(用户界面)
Guideline 10.1 问题
Guideline 10.2 问题
Guideline 10.3 问题
Guideline 10.4 问题
Guideline 10.5 问题
Guideline 10.6 问题
11. Purchasing and currencies(购买与流通货币)
Guideline 11.1 问题
Guideline 11.2 问题
Guideline 11.3 问题
Guideline 11.4 问题
Guideline 11.5 问题
Guideline 11.6 问题
Guideline 11.7 问题
Guideline 11.8 问题
Guideline 11.9 问题
Guideline 11.10 问题
Guideline 11.11 问题
Guideline 11.12 问题
Guideline 11.13 问题
Guideline 11.14 问题
Guideline 11.15 问题
12. Scraping and aggregation(抓去与整合)
Guideline 12.1 问题
Guideline 12.2 问题
Guideline 12.3 问题
13. Damage to device(损害设备)
Guideline 13.1 问题
Guideline 13.2 问题
14. Personal attacks(人身攻击)
Guideline 14.1 问题
Guideline 14.2 问题
15. Violence(暴力)
Guideline 15.1 问题
Guideline 15.2 问题
Guideline 15.3 问题
Guideline 15.4 问题
Guideline 15.5 问题
16. Objectionable content(负面内容)
Guideline 16.1 问题
Guideline 16.2 问题
17. Privacy(隐私)
Guideline 17.1 问题
Guideline 17.2 问题
Guideline 17.3 问题
18. Pornography(色情)
Guideline 18.1 问题
Guideline 18.2 问题
19. Religion, culture, and ethnicity(信仰,文化和种族)
Guideline 19.1 问题
Guideline 19.2 问题
20. Contests, sweepstakes, lotteries, and raffles(竞赛,赌博,彩票和抽*)
Guideline 20.1 问题
Guideline 20.2 问题
Guideline 20.3 问题
Guideline 20.4 问题
21. Charities and contributions(慈善与捐助)
Guideline 21.1 问题
Guideline 21.2 问题
22. Legal requirements(法律要求)
Guideline 22.1 问题
Guideline 22.2 问题
Guideline 22.3 问题
Guideline 22.4 问题
Guideline 22.5 问题
Guideline 22.6 问题
Guideline 22.7 问题
Guideline 22.8 问题
iOS 审核被拒*问题汇总
苹果审核概述
目前机审机制越来越完善了,而且也越来越受重视,相比前几个月,近期的苹果审核时间逐渐缩短,平均审核时间为23.9 小时。

应用被拒分为两种:Binary Rejected 和 Metadata Rejected。前者需要重新上传应用并且重新排队,后者只需要修改信息,不需要重新上传应用。

苹果审核大体分为三部分,预审、机审和人工审核。目前应用提审的整个流程大体分为五个阶段:Prepare For Upload(准备上传)、Waiting For Review(等待审核)、 In Review(审核)、Pending Developer Release(等待开发者发布)、Ready For Sale(准备销售)。

APP上传后,会进入到 Wait for Review 状态,而后进入到In Review状态,In Review一般2天左右就会审核通过或者是被打回。

包上传后首先进入的是预审,会被扫描API等,预审通过后会在iTC里出现,然后才可以提交至 Waiting;

在 Waiting For Review(等待审核)阶段一般是机审,机审主要是对代码进行机器审核,排查APP是否重复应用,“2.1苹果狗年大*包”事件就更多地依赖机器自动审核,减少人工成本;

通过后会进入In Review(审核)阶段,即人工审核阶段,这个阶段主要看的是App的元数据,例如APP封面、功能、体验等等,注重用户体验。

App被拒*常见原因概述
1、应用内包含检查更新功能

iOS 应用的版本更新必须通过 App Store 进行,自身 App 内不能包含提示更新功能。从2015年3月起,所有包含检查更新功能的 App 都会被拒*上架。

附被拒理由原文:
Your app includes an update button or alerts the user to update the app. To avoid user confusion, app version updates must utilize the iOS built-in update mechanism. We’ve attached screenshot(s) for your reference.
Next Steps

Please remove the update feature from your app. To distribute a new version of your app, upload the new app binary version into the same iTunes Connect record you created for the app’s previous version. Updated versions keep the same Apple ID, iTunes Connect ID (SKU), and bundle ID as the original version, and are available free to customers who purchased a previous version.

2、使用第三方登录时未做安装检测

接入第三方登录要检测是否安装了第三方客户端,未安装时不要显示对应按钮。2015年9月之前,通常可以采用判断未安装则隐藏登录按钮的方式。但目前隐藏按钮的方式也可能被审核拒*,QQ 和微博提供了 web 登录的方式,如果判断未安装,需要允许用户使用 webview 的登录方式。苹果在条款中有声明不允许 iOS 应用的正常使用需要依赖另外一个 App。

附被拒理由原文:
We noticed that third-party app QQ/WeChat is required to use third-party authentication method. The user should be able to login without installing additional applications.
Next Steps

If you choose to support third-party authentication, please use methods that can authenticate users from within your app, such as a native web-view.

3、采集设备IDFA但应用没有广告功能

从2014年2月起,Apple 开始拒*采集 IDFA (identifier for advertising) 却未集成任何广告服务的应用进入 App Store。如果 App 本身没有广告,ASO100.com 建议可以在审核的时候显示一个 Banner 广告,并且放在比较明显的位置,审核通过后关掉即可。

附被拒理由原文:
We found that your app uses the iOS Advertising Identifier but does not include ad functionality. This does not comply with the terms of the iOS Developer Program License Agreement, as required by the App Store Review Guidelines.
Specifically, section 3.3.12 of the iOS Developer Program License Agreement states:
“You and Your Applications (and any third party with whom you have contracted to serve advertising) may us the Advertising Identifier, and any information obtained through the use of the Advertising Identifier, only for the purpose of serving advertising. If a user resets the Advertising Identifier, then You agree not to combine, correlate, link or otherwise associate, either directly or indirectly, the prior Advertising Identifier and any derived information with the reset Advertising Identifier.”

Please remove the iOS Advertising Identifier from your app or add ad functionality to your app.

4、含UGC却未提供用户协议及举报功能

如果你的 App 内有发帖等UGC(用户产生内容)功能,必须提供用户协议,并留有内容举报功能,否则就会被审核拒*。

附被拒理由原文:
We found your app enables the display of user-generated content which may become sexually explicit. Therefore we ask that you put the following precautions in place, to ensure your app remains in compliance with the App Store Review Guidelines.
Use Moderators to flag and remove inappropriate content
Require that your users agree to terms (EULA) and these terms must be clear that there’s no tolerance for objectionable content
Users need a way to flag or report objectionable content and users generating this content
Developer must act on objectionable content reports within 24 hours by removing the content and ejecting the user who provided the offending content
Developer needs a method for ejecting users who violate the terms of the EULA

Please keep in mind that it is not sufficient for the user to report an issue through a general user feedback / 反馈 or like/dislike feature of the app. Please ensure that the contents that may become objectionable have a reporting or flagging mechanism readily accessible by the user to allow the user to promptly report or flag the issue and clearly identify the offending content.

5、上传时没有使用真实的应用截图

应用程序的名称、描述、截图或者预览与应用的内容和功能不相关将会被拒*。有 App 因为应用截图使用的是自己设计的插画而被审核拒*。

附被拒理由原文:
We noticed that your marketing screenshot(s) do not sufficiently reflect your app in use.We’ve attached screenshot(s) for your reference.
Next Steps

Please revise your screenshots to demonstrate the app functionality in use.

6、应用必须使用邀请码才能注册使用

苹果要求应用不能限制只有部分用户可以使用。

附被拒理由原文:
Your app arbitrarily restrict users by requiring invitation code to register, which is not allowed on the App Store. We’ve attached screenshot(s) for your reference.
Next Steps

Please revise your app to remove any functionality that limits who can use the app.

7、应用内出现第三方移动平台的名字或图标

一直以来,苹果都不允许iOS开发者在进行软件描述时提到 Android 版本,而自从2015年4月起,在 App 内、截图等任何地方提到安卓、Android 的文字、图标、系统界面都会被拒。曾经有电商 App,因为出现了售卖三星安卓手机而被拒。。。

附被拒理由原文:
We found that your app and/or its metadata contains inappropriate or irrelevant platform information, which is not in compliance with the App Store Review Guidelines.
Specifically, your app mentioned other platforms, such as Android.

Providing future platform compatibility plans, or other general platform references, is not appropriate in the context of the App Store. It would be appropriate to remove this information.

8、应用内涉及*励,未声明与苹果无关

App 里有实物*励的话,不能使用苹果产品(例如 iPhone 、iPad 等)作为*品。另外一定要声明“*励由本公司提供,与苹果官方无关”。

附被拒理由原文:
Your app includes a contest or sweepstakes but it does not:
Indicate that Apple is not involved in any way with the contest or sweepstakes.
Next Steps
It is necessary to:
Include official rules of the contest or sweepstakes in the app
Include an explicit statement in the contest or sweepstakes rules specifying that > Apple is not a sponsor

Ensure that the contest or sweepstake prizes are not Apple products

9、没有提供恢复内购的方法

增加一个“恢复购买记录”的按钮即可。

附被拒理由原文:
We found that your app offers In-App Purchase/s that can be restored but it does not include a “Restore” feature to allow users to restore the previously purchased In-App Purchase/s.

To restore previously purchased In-App Purchase products, it would be appropriate to provide a “Restore” button and initiate the restore process when the “Restore” button is tapped.

10、未注册时不能使用与账号无关的功能

对于资讯等 App,在没有进行与用户信息相关的操作时,却强行让用户登录,甚至不登录就无法看到任何内容,有可能会被拒*。

附被拒理由原文:
We noticed that your app requires users to register with personal information to access non account-based features. Apps cannot require user registration prior to allowing access to app content and features that are not associated specifically to the user.
Specifically, your app forces users to login before they can read the news.
We features that your app requires users to register or log in, prior to accessing non account-based features. Apps cannot require user registration or login prior to allowing access to app content and features that are not associated specifically to the user.
Next Steps

User registration that requires the sharing of personal information must be optional or tied to account-specific functionality. Additionally, the requested information must be relevant to the features.

11、iPhone 应用在 iPad 上不能正常显示

iPhone程序必须不经修改就能以iPhone分辨率和2倍iPhone 3GS的分辨率在iPad上运行。即使你的App 只为 iPhone 用户提供,在 iPad 上也必须能够正常显示,否则审核会被拒*。

附被拒理由原文:
We noticed that your app did not run at iPhone resolution when reviewed on iPad running iOS 9.1, which is a violation of the App Store Review Guidelines. We’ve attached screenshot(s) for your reference.
Specifically, the buttons at the bottom of the app are inaccessible when running on iPad.
Next Steps

Please revise your app to ensure it runs at iPhone resolution on iPad.

12、侵犯第三方版权

对于视频、音乐、图书类的应用很容易因为这一条而被拒。另外 ASO100.com 建议应用内*好不要出现第三方的商标,例如运营商的Logo、影视公司的 Logo 等。

附被拒理由原文一:

We found that your app allows users to download music without authorization from the relevant third-party sources.
We’ve attached screenshot(s) for your reference.
Next Steps
Please provide documentary evidence of your rights to allow music or video content download from third-party sources. If you do not have the requested permissions, please remove the music or video download functionality from your app.
附被拒理由原文二:
Your app includes content or features that resemble a well-known, third-party mark, Fox . We’ve attached screenshot for your reference.
Pursuant to your agreement with Apple, you represent and warrant that your application does not infringe the rights of another party, and that you are responsible for any liability to Apple because of a claim that your application infringes another party’s rights. Moreover, we may reject or remove your application for any reason, at our sole discretion.

Accordingly, please provide documentary evidence of rights to use this content. Once Legal has reviewed your documentation and confirms its validity, we will proceed with the review of your app.

13、应用截图、名称、描述等出现不雅词汇

在应用截图、名称、描述等任何地方出现例如诸如 牛逼、绿茶婊、无节操、逗比 等词汇,都会被苹果审核拒*。

附被拒理由原文:
We found that your app contains content that many audiences would find objectionable, which is not in compliance with the App Store Review Guidelines.
Specifically, we noticed your app name 打飞机-简单粗暴 is objectionable.

We encourage you to review your app content and evaluate whether you can modify the content to bring it into compliance with the Guidelines.

14、应用出现 beta版、测试版字样

不要过度谦虚地在启动画面或者应用名称上加上”beta”字样,苹果不允许测试版产品上架。

附被拒理由原文:
Your app appears to be a pre-release, test, or trial version with a limited feature set. Apps that are created for demonstration or trial purposes are not appropriate for the App Store and do not comply with the App Store Review Guidelines.
To ensure compliance with the App Store Review Guidelines, it would be appropriate to revise your app to complete, remove, or fully configure any partially implemented feature(s).

If you would like to conduct beta trial for your app, you may wish to review the TestFlight Beta Testing Guide.

15、注册缺少隐私政策

如果应用包含注册功能,注册页面必须提供隐私说明协议按钮或者链接。另外在 iTunes connect 提交新版本的时候,Privacy Policy URL 必须要填写。

附被拒理由原文:
We noticed that your app includes account registration or access to users’ existing accounts but does not include a privacy policy, which does not comply with the App Store Review Guidelines.

Please update your app metadata to include a privacy policy and ensure that the privacy policy URL you provide directs the user to the intended destination.

16、应用出现崩溃、加载失败等 bug

审核期间出现崩溃会导致审核被拒。ASO100.com 建议,在审核期间务必保证服务器稳定,避免审核人员审核时出现内容加载失败的情况,导致被拒。

附被拒理由原文:
We discovered one or more bugs in your app when reviewed on iPhone running iOS 8.1.2 on both Wi-Fi and cellular networks.
Specifically, no content is fetched when users launch the app.Please see the attached screenshot/s for more information.
It would be appropriate to revise such issue(s) in your application.
Next Steps

Please run your app on a device to identify the issue(s), then revise and resubmit your app for review.

17、应用描述、截图和应用功能不符

如果应用的描述或截图介绍的功能在审核期间没有体现,则会被拒*,如果介绍文案不够详细也会有一定概率被拒。

附被拒理由原文:
We found that your app did not achieve the core functionality described in your marketing materials or release notes, as required by the App Store Review Guidelines.
Specifically, your app does not include the feature of 微信朋友圈分享 that is written in your release note.

It would be appropriate to revise your app to ensure this feature is fully implemented or to revise your Application Description, Release Notes, and/or screenshots to remove this content.

18、应用包含应用推荐功能

除特殊情况,苹果明令禁止应用内推荐其他APP。

附被拒理由原文:
The 应用推荐 feature in your app displays or promotes third-party apps, which violates the App Store Review Guidelines. We’ve attached screenshot(s) for your reference.
Next Steps

Please remove the 应用推荐 feature from your app.

19、应用包含不正确的诊断功能

如果你的应用中,包含不真实的系统检测或优化功能,苹果会认为这项功能有误导用户的嫌疑,审核时会被拒*。

附被拒理由原文:
We noticed that your app provides potentially inaccurate diagnostic functionality for iOS devices to the user.

We’ve attached screenshot(s) for your reference.

Next Steps

Currently, there is no publicly available infrastructure to support iOS diagnostic analysis. Therefore your app may report inaccurate information which could mislead or confuse your users. We encourage you to review your app concept and incorporate different content and features that are in compliance with the App Store Review Guidelines.

20、应用提交的新版本与上一版差异过大

如果你提交的新版本应用与上一版相比,功能上变化过大,比如将游戏升级为工具类应用,或在新版本中完全改掉前一版产品的功能,则会被苹果拒*。

附被拒理由原文:
We found that your app did not achieve the core functionality described in your marketing materials or release notes, as required by the App Store Review Guidelines.

Specifically, the app has a whole content swap from a Game app to a Mobile Data Tracking app, which does not provide a good user experience when updating the app.

It would be appropriate to revise your app to ensure this feature is fully implemented or to revise your Application Description, Release Notes, and/or screenshots to remove this content.

If your iTunes Connect Application State is Rejected, a new binary will be required. Make the desired metadata changes when you upload the new binary.

21、应用违反当地法律法规

应用程序必须遵守上线地区的法律法规,禁止含有赌博、色情、有偿陪伴等违反法律的内容,尤其为用户提供付费社交服务的APP,例如在线直播类APP,必须严格遵守相关规定。

附被拒理由原文:
Your app contains content – or facilitates, enables, and encourages an activity – that is not legal in all of the locations where the app is available. Specifically, your app is advertised as a platform to provide paid companionship services.

We’ve attached screenshot(s) for your reference.
Next Steps

We encourage you to review your app concept and incorporate different content and features that are in compliance with the App Store Review Guidelines.

22、应用作者名与金融机构名字不一致
针对理财、P2P等金融相关产品,苹果增加规定
开发者的名字必须与APP内的金融机构名字保持一致,否则会被拒。
且由同一品牌的金融机构提供服务的APP,必须发布在同一个开发者账号跟名称下。

如果你已经代表委托人或者公司发布了这些APP,你的委托人或者公司应该注册iOS开发者账号,并把你添加到他们的开发者账号里,这样你就可以在他们账号下面提交并发布APP了。

附被拒理由原文:
We found that the Seller and/or Artist names associated with your app do not reflect the name of the financial institution in the app and/or its name and metadata.

To be appropriate for the App Store, your app must be published under a Seller name and Artist name that reflects the financial institution brand, as required by the iOS Developer Program License Agreement.

Section 1.2:
“You” and “Your” means and refers to the person(s) or legal entity (whether the company, organization, educational institution, or governmental agency, instrumentality, or department) using the Apple Software or otherwise exercising rights under this Agreement. For the sake of clarity, You may authorize contractors to develop Applications on Your behalf, but any such Applications must be submitted under Your developer account.

If you have published these apps on behalf of a client, it would be appropriate for your client to enroll in the iOS Developer Program, then add you to their development team so you can develop an app for them to submit under their developer account.

23、应用提供功能过于简单
应用内的功能不能太过单一,苹果虽然理念中提倡“简单”,但并不代表能接受功能不够完善的应用,他们对应用的核心要求,是希望能够给用户更有价值的体验。当然,如果你的产品太有创意,可能苹果的审核员没能理解它的独到之处,这样的情况下,你可以通过申诉来更详细的描述产品优势,以便通过审核。

附被拒理由原文:
We found that your app only provides a very limited set of features. It only provides an augmented reality reader mechanism with no other functionality. While we value simplicity, we consider simplicity to be uncomplicated – not limited in features and functionality.

We understand that there are no hard and fast rules to define useful or entertaining, but Apple and Apple customers expect apps to provide a really great user experience. Apps should provide valuable utility or entertainment, draw people in by offering compelling capabilities or content, or enable people to do something they couldn’t do before or in a way they couldn’t do it before.

We encourage you to review your app concept and evaluate whether you can incorporate additional content and features to be in compliance with the Guidelines. For information on the basics of creating great apps, watch the video The Ingredients of Great Apps.If you feel we didn’t understand the features of your app, or that we missed key functionality, and your app was incorrectly rejected, you may appeal to the App Review Board.

被拒解决方案概述
1、账号关联性问题。

原来:各一级账号,授权同一账号上传产品,授权同批账号测试产品。

修改:每个一级账号,授权到不同账号上传产品,授权不同账号测试产品。

2、代码关联、相识性程度

在不影响产品的情况下,让各产品之间代码相似程序降低。
(例如:增加垃圾代码和其它技术手段使二进制代码不同)

3、产品相关

后台外部元素(优先级排列)
套餐ID、SKU。
APP内购项目(增加内购内容,不同产品添加不同额度计费点)
文字介绍(针对不同地区抒写,不允许套用模版)
游戏广告图(要有明显的区别,不能只改插图)
关键字
开发者联系人
联系地址
产品发行地区
游戏类别
测试账号(涉及白名单,准备3~4个测试账号)
备注(软著、备案、版号信息)
其他注意事项(产品的相关信息介绍描述)
内部UI和界面
大厅UI调整、启动页面、大厅背景图页面。

4、出包机器、上传应用机器

暂定不增加出包机器
上传提交产品时,使用手机4G网络提交。

针对可以用户自由发布信息的APP
那种用户可以发布信息的app,一定要针对用户发布行为做机制。
用户发布行为要有条款说明;要有不良信息过滤机制;
浏览用户如果不喜欢某条信息要可以屏蔽;
浏览用户如果不喜欢某人可以把他拉入黑名单;
游戏代练代打这类信息,必须得有游戏方的授权证明,否则千万不能出现。
如果审核一次又一次被拒,你的审核时间就会越来越长。

如果不是代码问题,不需要重传二进制ipa包,就通过走申诉途径。

加速审核的方式
1、Apple 提供了一个加速审核的通道:
https://developer.apple.com/appstore/contact/appreviewteam/index.html

当然,也可以通过itunes 进入加急审核通道。
步骤:“联系我们”->App Review -> App Store Review -> Request Expedited Review -> Request an Expedited App review
%title插图%num

%title插图%num

%title插图%num

如果是走申诉通道时,想联系苹果,*好在Explanation里面 留下你公司负责人的联系方式,到时候苹果可能会主动打电话给你,通过邮件的方式非常慢。

除了上面的截图,中间需要填写app的一些信息。

填写你的联系方式(电话)
App名字,id
选择App Store
选择加急理由。苹果给出三个选项”bug修复”,“重大节日”,“其他”。
根据自己的当前个人情况,选择加急理由,而后写上你的加急理由。(写加急理由的时候要注意,尽量描述清楚你遇到的情况,让苹果审核团队一看就懂即可)

加速是否能申请成功,关键是看你填写*后部分的描述。(大致的意思应该可以猜到吧,可以参考:http://translate.google.cn
比如你是紧急修复严重bug,
1、那你要解释这个bug的严重性,必须修复
2、*好把bug重新步骤等描述等等。 要提供足够的细节。
3、尽量使用英文描述;
4、分条描述App存在的重要问题,如:Crash,用户无法使用等;
强调自己已经解决(fixed)问题或者致命的八阿哥(bug);
5、 网上说iOS审核次数只有2-3次。目前已被证伪了。当然,虽然加急审核没有次数限制。但*好是不要滥用苹果加急审核通道。如果理由不充分却频繁申请加急审核,是很容易被苹果拉进黑名单,导致之后难以加急成功的。

苹果开发者 条款详细解读
1. Terms and conditions(法律与条款)
Guideline 1.1 问题
Guideline 1.1 – Safety – Objectionable Content
问题分析:

苹果邮件内容:

Guideline 1.1 – Safety – Objectionable Content
Your app includes content that many users would find objectionable and offensive. Specifically, your app provided paid chat service.
Please see attached screenshots for details.
Next Steps
To resolve this issue, please remove all potentially objectionable content from your app and submit your revised binary for review.

For app design information, check out the following videos: and “Designing Intuitive User Experiences,” available on the Apple Developer website.
You may also want to review the iOS Human Interface Guidelines for more information on how to create a great user experience in your app.
翻译出来是:
准则1.1 – 安全 – 不良内容
您的应用包含许多用户会觉得令人反感和令人反感的内容。 具体来说,您的应用提供了付费聊天服务
详情请参阅附件截图。
下一步
要解决此问题,请从您的应用中删除所有可能令人反感的内容,并提交修改后的二进制文件以供审核。
有关应用设计信息,请查看以下视频:以及Apple Design Developer网站上提供的“设计直观的用户体验”。
您可能还需要查看iOS人机界面指南,以获取有关如何在您的应用中创建出色用户体验的更多信息。

问题分析:

分析:我们的app是一个交友软件,上面的大部分是女性。所以被拒了。额。。有点奇葩。解放方法:上传一些男性的照片并且放在显眼的位置。
1
解决办法:

上传一些男性的照片并且放在显眼的位置。
1
1.1.6 包含虚假信息,功能或误导性元数据。
问题分析:

苹果邮件内容:

1
问题分析:

1
解决办法:

Guideline 1.2 问题
Guideline 1.2 问题分析
苹果邮件内容:
Guideline 1.2 – Safety – User Generated Content
Your app enables the display of user-generated content but does not have the proper precautions in place.
Next Steps
To resolve this issue, please revise your app to implement all of the following precautions:

Require that users agree to terms (EULA) and these terms must make it clear that there is no tolerance for objectionable content or abusive users
A method for filtering objectionable content
A mechanism for users to flag objectionable content
A mechanism for users to block abusive users
The developer must act on objectionable content reports within 24 hours by removing the content and ejecting the user who provided the offending content

翻译出来是:
准则1.2 – 安全 – 用户生成的内容
您的应用可以显示用户生成的内容,但没有适当的预防措施。
下一步
要解决此问题,请修改您的应用以实施以下所有预防措施:

要求用户同意条款(EULA),
并且这些条款必须明确表示不允许对不良内容或滥用用户
用于过滤令人反感的内容的方法
用户标记令人反感的内容的机制
用户阻止滥用用户的机制
开发人员必须在24小时内处理令人反感的内容报告,
方法是删除内容并弹出提供违规内容的用户。

解决办法:
这个被拒的原因是因为app能展示用的内容,
但却没有防范措施。解决方法是加上用户协议,
加上拉黑以及举报功能

2. Functionality(功能)
Guideline 2.1 问题 (App 完成度)
Apps that crash will be rejected(存在Crash(崩溃,死机)的应用会被拒。)

主要有应用出现崩溃、加载失败等非常明显的Bug、应用不支持 IPv6网络下使用、测试账号、隐藏开关等。

解决方法:提前测试产品是否有bug、和在IPV6网络下是否能使用等,根据提供邮件,一个个审查自身产品信息是否符合,适当情况下可以发送截图视频给苹果官方以证明自己的清白。

Guideline 2.1 – Performance – App Completeness
问题分析:
2.1 – 性能 – 应用程序完整性
Apps that crash will be rejected(存在Crash(崩溃,死机)的应用会被拒。)

苹果邮件内容:

Guideline 2.1 – Performance – App Completeness
We discovered one or more bugs in your app when reviewed on iPad running iOS 11.3 on Wi-Fi connected to an IPv6 network.
We could not load the contents in 消息.
The steps to reproduce are:

Launched the app
Input demo account information
Tapped 消息 at the right bottom
No contents

翻译出来是:
准则2.1 – 性能 – 应用程序完整性
我们在连接到IPv6网络的Wi-Fi上运行iOS 11.3的iPad上检查时,发现您的应用存在一个或多个错误。
我们无法加载消息中的内容。
重现的步骤是:
1.启动应用程序
2.输入模拟账户信息
3.在右下角点击消息
4.没有内容

问题分析:

及时通讯集成的是环信,环信在ipv6下是坑定没有问题的。
后来想到审核人员是把app删掉之后重新装的,
而环信的信息和微信一样是缓存在本地的。
删掉后从新进入当然没有。
解决方法:在提审是说明一下。
eg:经我们测试,在ipva6网络下是没有问题的。
我们的聊天信息是缓存在本地的。

解决办法:

app需要通信协议需要志长ipv6
1
Guideline 2.2 问题
Apps that exhibit bugs will be rejected(存在明显bug的应用会被拒。)

问题分析:

苹果邮件内容:

1
问题分析:

1
解决办法:

Guideline 2.3 问题 (准确的元数据)
主要是应用标题、描述、截图等与应用功能严重不符。
Apps that do not perform as advertised by the developer will be rejected(不符合开发者描述的应用会被拒。)

解决方法:

重新更换截图,保证整个APP功能、流程看起来是一致的。去除隐藏功能模块代码或将需要隐藏功能的代码及定向跳转链接网址做混淆处理,适当增加逻辑复杂度。

2.3.0 批准后进行重大概念变更
问题分析:

苹果邮件内容:

1
问题分析:

1
解决办法:

2.3.1 有隐藏或未记录的功能,包括重定向到赌博或彩票网站的隐藏“开关”
问题分析:

苹果邮件内容:

1
问题分析:

1
解决办法:

Guideline 2.3.7 – Performance – Accurate Metadata (性能 – 准确的元数据)
问题分析:

苹果邮件内容:

Guideline 2.3.7 – Performance – Accurate Metadata
Your app name or subtitle to be displayed on the App Store includes keywords or descriptors, which are not appropriate for use in these metadata items.
Specifically, the following words in your app name or subtitle are considered keywords or descriptors:
翻译出来是:
准则2.3.7 – 性能 – 准确的元数据
您在App Store上显示的应用名称或字幕包含关键字或描述符
,这些关键字或描述符不适用于这些元数据项目。
具体来说,应用程序名称或副标题中的以下单词被视为关键字或描述符:

 

问题分析:

分析:在填写应用信息的时候主标题和副标题关键字重复。

解决办法:

去掉重复的关键字就可以了。
1
Guideline 2.4 问题
Apps that include undocumented or hidden features inconsistent with the description of the App will be rejected(有未说明或隐藏特性或有悖描述的应用会被拒。)

2.4.1 性能 – 硬件兼容性 问题
问题分析:

我们注意到,在运行iOS 11.3的iPad上查看时,您的应用没有按预期运行或显示。 详情请参阅附件截图。

苹果邮件内容:

1
问题分析:

虽然有时候我们的app是只支持iPhone手机的,但苹果的审核人员测试使用的是ipad,这样他们要求app必须对ipad进行适配。解放方案:对ipad进行适配。一般来说只要在iPhone上适配没有问题,在ipad上的适配问题都不大。*后是让ipad值支持竖屏,去掉横屏。有很多人是适配了竖屏,而苹果审核人员发现横屏没有支持被拒了,所以info.plist 的Supported interface orientations (iPad)里设置只支持竖屏就好了。

解决办法:

info.plist 的Supported interface orientations (iPad)里设置只支持竖屏就好了。
1
Guideline 2.5 问题 (软件要求)
主要是产品加入违规代码
Apps that use non-public APIs will be rejected(使用非公开API的应用会被拒。)

解决方法:

很可能是三方库中含有SDK,可以更新所有三方库,或者反编译提交的ipa,检查文档中是否有违规字符串,有的话删掉。
1
2.5
问题分析:

1
苹果邮件内容:

1
问题分析:

1
2
解决办法:

1
Guideline 2.6 问题
Apps that read or write data outside its designated container area will be rejected(试图读写非允许范围内的数据的应用会被拒。)

Guideline 2.7 问题
Apps that download code in any way or form will be rejected(试图以任何方式方法下载代码的应用会被拒。)

解决方法:

1
问题分析:

1
苹果邮件内容:

1
问题分析:

1
2
解决办法:

1
Guideline 2.8 问题
Apps that install or launch other executable code will be rejected(安装或运行其他可执行代码的应用会被拒。)

解决方法:

1
问题分析:

1
苹果邮件内容:

1
问题分析:

1
2
解决办法:

1
Guideline 2.9 问题
Apps that are “beta”, “demo”, “trial”, or “test” versions will be rejected(任何“beta”,“演示(demo)”,“试用(trial)”或“测试(test)”版本的应用会被拒。)

解决方法:

1
问题分析:

1
苹果邮件内容:

1
问题分析:

1
2
解决办法:

1
Guideline 2.10 问题
iPhone Apps must also run on iPad without modification, at iPhone resolution, and at 2X iPhone 3GS resolution(iPhone应用必须可以无条件运行在iPad上,支持普通iPhone分辨率和2倍iPhone 3GS分辨率。)

解决方法:

1
问题分析:

1
苹果邮件内容:

1
问题分析:

1
2
解决办法:

1
Guideline 2.11 问题
Apps that duplicate Apps already in the App Store may be rejected, particularly if there are many of them, such as fart, burp, flashlight, and Kama Sutra Apps.(任何与App Store中上架应用重复的应用会被拒,尤其是已经有了很多的:如放屁,打嗝,手电照明和爱经。)

解决方法:

1
问题分析:

1
苹果邮件内容:

1
问题分析:

1
2
解决办法:

1
Guideline 2.12 问题
Apps that are not very useful, unique, are simply web sites bundled as Apps, or do not provide any lasting entertainment value may be rejected(没有用处的应用,web页面简单组合的应用,或任何哗众取宠,不能提供娱乐价值的应用会被拒。)

解决方法:

1
问题分析:

1
苹果邮件内容:

1
问题分析:

1
2
解决办法:

1
Guideline 2.13 问题
Apps that are primarily marketing materials or advertisements will be rejected(纯粹用于市场推广或广告的应用会被拒。)

解决方法:

1
问题分析:

1
苹果邮件内容:

1
问题分析:

1
2
解决办法:

1
Guideline 2.14 问题
Apps that are intended to provide trick or fake functionality that are not clearly marked as such will be rejected(有意提供隐蔽或虚假功能,却又不能明显标示的应用会被拒。)

Guideline 2.15 问题
Apps larger than 50MB in size will not download over cellular networks (this is automatically prohibited by the App Store)(大于20MB的应用无法通过蜂窝网络下载安装(App Store自动处理)。)

解决方法:

1
问题分析:

1
苹果邮件内容:

1
问题分析:

1
2
解决办法:

1
Guideline 2.16 问题
Multitasking Apps may only use background services for their intended purposes: VoIP, audio playback, location, task completion, local notifications, etc.(多任务应用只允许在后台运行如下相应的服务:VoIP,音频播放,地理位置,任务记录,本地提醒等。)

Guideline 2.17 问题
Apps that browse the web must use the iOS WebKit framework and WebKit Javascript(应用只允许通过iOS WebKit框架和WebKit Javascript访问web页面。)

Guideline 2.18 问题
Apps that encourage excessive consumption of alcohol or illegal substances, or encourage minors to consume alcohol or smoke cigarettes, will be rejected(鼓励酗酒,使用违法药物,或诱导未成年人饮酒,吸烟的应用会被拒。)

Guideline 2.19 问题
Apps that provide incorrect diagnostic or other inaccurate device data will be rejected(提供错误的系统信息或设备数据的应用会被拒。)

Guideline 2.20 问题
Developers “spamming” the App Store with many versions of similar Apps will be removed from the iOS Developer Program(通过许多版本的类似应用对App Store造成干扰的开发者会被取消IDP身份。)

Guideline 2.21 问题
Apps that are simply a song or movie should be submitted to the iTunes store. Apps that are simply a book should be submitted to the iBookstore.(歌曲和电影应该提交到iTunes store。书籍应该提交到iBookstore。)

Guideline 2.22 问题
Apps that arbitrarily restrict which users may use the App, such as by location or carrier, may be rejected(随意通过位置或运营商来限制用户使用的应用会被拒。)

Guideline 2.23 问题
Apps must follow theiOS Data Storage Guidelinesor they will be rejected(加入iCloud支持后,应用必须遵守iOS数据存储指南(iOS Data Storage Guidelines)否则将被拒。)

Guideline 2.24 问题
Apps that are offered in Newsstand must comply with schedules 1, 2 and 3 of the DeveloperProgram License Agreement or they will be rejected(在Newsstand里提交的应用必须遵守Developer Program License Agreement的第1,2和3条,否则将被拒。)

Guideline 2.25 问题
Apps that display Apps other than your own for purchase or promotion in a manner similar to or confusing with the App Store will be rejected(与AppStore类似的推荐或为其他应用做广告的应用将无法通过AppStore审核。)

3. Metadata (name, descriptions, ratings, rankings, etc)(描述数据(名称,描述,评级,分类等))
Guideline 3.0 – Business
问题分析:

苹果邮件内容:

Your game app includes in-app purchase products for consumable credits priced over $99.99, Tier 60.
Next Steps
To resolve this issue, please revise your app to ensure that a single in-app purchase product for consumable credits is priced at nothing more than $99.99, Tier 60.
To edit in-app purchases:

Log in to iTunes Connect
Click on “My Apps”
Select your app
Click on “Features” to view your in-app purchases
Click on a Reference Name that is in Developer Action Needed status
Click “Edit In-App Purchase Details”
Click Save
Once you’ve completed all changes, click the “Submit for Review” button at the top of the App Version Information page.

Note: When an in-app purchase is in the “Developer Action Needed” state, you must make some change to it before it can move to Waiting for Review.
翻译出来是:
准则3.0 – 业务
您的游戏应用程序包括应用程序内消费信用卡购买产品,价格超过99.99美元,60级。
下一步
要解决此问题,请修改您的应用,以确保单个应用内购买消费信用产品的价格不超过99.99美元,Tier 60。
编辑应用内购买:

登录iTunes Connect
点击“我的应用程序”
选择你的应用程序
点击“功能”查看您的应用内购买
单击“开发者操作需要”状态下的参考名称
点击“编辑应用内购买详情”
点击保存
完成所有更改后,请单击应用程序版本信息页面顶部的“提交以供查看”按钮。

注意:当应用程序内购买处于“需要开发者操作”状态时,您必须对其进行一些更改,然后才能转到等待审核。

问题分析:

在app中用到了苹果内购,苹果规定一次性消耗种类的价格不能大于99美元。解决方法:修改价格即可。

解决办法:

修改价格
1
Guideline 3.1 问题 (购买项目)
主要是接入第三方支付。

解决方法:

老老实实地走 IAP 的支付方式,用内购。如果隐藏虚拟产品或者通过后更改支付方式,都是有一定风险的。
1
Guideline 3.1.1 – Business – Payments – In-App Purchase
问题分析:
使用应用程序内购买以外的支付机制解锁应用程序中的功能或功能
Guideline 3.1.1 – Business – Payments – In-App Purchase
We also noticed that your app uses in-app purchase products to purchase credits or currencies that are not consumed within the app, which is not appropriate for the App Store.

苹果邮件内容:

Guideline 3.1.1 – Business – Payments – In-App Purchase
We also noticed that your app uses in-app purchase products to purchase credits or currencies that are not consumed within the app, which is not appropriate for the App Store.
Please see attached screenshots for details.
翻译出来是:
准则3.1.1 – 业务 – 付款 – 应用内购买
我们还注意到,您的应用使用应用内购买产品来购买未在应用内消费的点数或币种,这不适用于App Store。
详情请参阅附件截图。
下一步
要解决此问题,请修改您的应用,确保通过应用内购买产品所使用的信用卡或货币在应用中使用,或完全删除应用内购买。

问题分析:

这个被拒的原因是因为app中有充值功能,但审核人员找不到若何消费充值金币。解决方式是在提审的时候告诉审核人员如何消费,并附上截图。

解决办法:

在提审的时候告诉审核人员如何消费,并附上截图。
1
3.2.1 可接受的商业模式
问题分析:

主要是没有资质。

解决方法:

*佳方案是拿到资质,如果实在没有资质,建议大家尽可能多的把自己公司合规的证据资料发给苹果,而套壳、换新账号碰运气上架等操作,不得已的话可以尝试。
1
苹果邮件内容:

1
问题分析:

1
解决办法:

Guideline 3.2 问题
Apps with placeholder text will be rejected(描述数据有未填写项,存留占位符文本会被拒。)

Guideline 3.3 问题
Apps with descriptions not relevant to the application content and functionality will be rejected(描述中提到与应用内容和功能无关信息会被拒。)

Guideline 3.4 问题
App names in iTunes Connect and as displayed on a device should be similar, so as not to cause confusion(应用在iTunes Connect与设备上显示的名称应该类似,否则会造成混淆。)

Guideline 3.5 问题
Small and large App icons should be similar, so as to not to cause confusion(不同尺寸的icon要一致,否则会造成混淆。)

Guideline 3.6 问题
Apps with App icons and screenshots that do not adhere to the 4+ age rating will be rejected(图标与截屏不符合4+年龄评级的应用会被拒。)

Guideline 3.7 问题
Apps with Category and Genre selections that are not appropriate for the App content will be rejected(应用的内容与所选分类和风格不符会被拒。)

Guideline 3.8 问题
Developers are responsible for assigning appropriate ratings to their Apps. Inappropriate ratings may be changed/deleted by Apple(开发者有责任把应用放到恰当的分级(Rating)。不恰当的评级可能会被Apple修改,甚至删除。)

Guideline 3.9 问题
Developers are responsible for assigning appropriate keywords for their Apps. Inappropriate keywords may be changed/deleted by Apple(开发者有责任给应用撰写恰当的关键词。不恰当的关键词可能会被Apple修改,甚至删除。)

Guideline 3.10 问题
Developers who attempt to manipulate or cheat the user reviews or chart ranking in the App Store with fake or paid reviews, or any other inappropriate methods will be removed from the iOS Developer Program(通过伪造,付费评价或其他非正规手段,获取App Store中较好的评价与星级的开发者会被取消IDP身份。)

Guideline 3.11 问题
Apps which recommend that users restart their iOS device prior to installation or launch may be rejected(任何提示需要用户重启iOS设备来安装或运行的应用会被拒。)

Guideline 3.12 问题
Apps should have all included URLs fully functional when you submit it for review, such as support and privacy policy URLs(应用在提交审核过程中,所有涉及到的URL都要处于正常运行状态,例如保密协议,相关支持页面等。)

4. Location(位置)
Guideline 4.1 问题
Apps that do not notify and obtain user consent before collecting, transmitting, or using location data will be rejected(未提示用户且获得用户允许之前收集,传输或使用位置数据的应用会被拒。)

问题分析:

苹果邮件内容:

1
问题分析:

1
解决办法:

Guideline 4.2 问题 (*低功能要求)
主要问题在于苹果认为部分开发者上传的App功能不够,或者没有自己的核心功能

解决办法:

可以添加一些功能丰富产品,如果觉得功能已经全了,还没有通过审核,可以向苹果解释产品解决的用户需求,以及具体功能的展现。

问题分析:

苹果邮件内容:

1
问题分析:

1
解决办法:

Guideline 4.3 问题 (重复 App)
主要针对的是重复App,就是马甲包。

解决办法:
可通过修改名字、icon、主色调、代码等解决,并且注意相同的马甲包提交至少间隔一天以上。

4.3.0 马甲包问题
是另一个应用程序的副本或与另一个应用程序显着相似

问题分析:

大势已经的ios马甲包无法上架=只有借用资质。从17年彩票,棋牌马甲包大批大批的袭来。很多公司都在花重金找人上架马甲包,期初可以的,但是现在已经泛滥了,咱党的政策可以让你小玩,但*不应许泛滥,但是由于某X公司大量的找人或骗人上架马甲包后。整个市场都充满了找人制作马甲包的广告。当然这不是针对谁,仅代表个人想法。而且相对使用相关资质账号上架*大的优点就是可以原生上架,质量好,带来的流量高,不影响用户使用。同样更新不会担心!就好比一个苹果X手机。缺点:价格高,难弄到资质 而马甲包就好比(一个老人机,同样都可以打电话可以发短信)但是垃圾的页面,让用户根本不想玩下去,你引流的人也不过是过眼云烟!!!刷榜、aso;不同包装拓量,自然流量。马甲包作用还是很大的。之前刷榜还流行的时候,马甲包随便刷个榜就赚小几十万现在作用没那么多,但是买量公司还是很需要多包跑的为什么现在马甲包大势已去?主要有两个原因,一个就是影响用户体验,影响正常的iOS流量分发,一个就是政策原因。大部分马甲包的上架难,无疑就是*种所致。贷款类、理财类、彩票类的APP上架,主要都是政策原因导致。那么对于这一类政策原因导致的上架难,主要的解决方案有两种,一个是做假页面方案,混淆审核人员的视听,以图上架成功,但是假页面方案一定要做的够精致、功能稍微多点,不然百分之九十遇到4.2条款被拒,也就是*小功能条款。所以现在*好的办法就是用有资质的账号上,要具备相应的经营范围及金融资质。

苹果邮件内容:

举例1:

Apple3. 2.1 Business: Other Business Model Issues – AcceptableGuideline 3.2.1 – Business – Other Business Model Issues – AcceptableWe found that your app facilitates loan applications, but the seller and company names associated with your app do not reflect the financial institution in the app or its metadata, as required by Guideline 3.2.1(viii) of the App Store Review Guidelines.Next StepsTo help us proceed with the review of your app, please provide the following information. The more information you can provide upfront, the sooner we can complete your review.1. Both a copy and the direct link to the government website of your Business License that verifies the authorization from the Internet Loan Information Agency (营业执照,营业范围证明其是网络借贷信息中介结构).2. Both a copy and the direct link to the government website of your Finance Permit issued from the local finance governing authority (金融许可证).3. A copy of the Value Added Telecom Business Operation Permit issued by the local Ministry of Information Industry and Technology (从当地工信部获得的增值电信业务经营许可证).4. Your app’s and service’s Terms & Conditions.5. In the case of dispute, what resolution mechanism does your app and service offer?6. What is your responsibility in such case? Is such responsibility stated clearly in the Terms & Conditions?7. How will the involved parties trace one another?Additionally, please ensure your app’s Support and Privacy URLs in the metadata direct users to the webpages with appropriate information.Please attach documentary evidence in the App Review Information section in iTunes Connect. In accordance with section 3.2(f) of the Apple Developer Program License Agreement, you acknowledge that submitting falsified or fraudulent documentation can result in the termination of your Apple Developer Program account and the removal of your apps from the App Store. Once Legal has reviewed your documentation and confirms its validity, we will proceed with the review of your app.
1
举例2:

1
问题分析:

为什么近期马甲应用提审困难。(→因为苹果加大了对重复提交的应用或和第三方应用类似的应用的审核力度!)
如果因该理由被拒,我们该如何处理呢?
1、收到苹果的通知/邮件后,如果是误会,可考虑向苹果解释清楚,然后请求苹果停止处罚。如果真的存在欺骗行为且确认苹果已发现该行为,可考虑(改正后)态度诚恳地回复苹果,承认错误并保证以后不再采取该行为。如果苹果接受了道歉,也许可以解封。但如果苹果态度坚决,就只能另想办法了。(主动承认有一定风险,请综合利弊后,谨慎选择。)
2、如果是提交了重复应用而被拒,还可考虑修改应用名称、图标等元数据以及功能、界面,或者去掉代码特征等后,重新提审。(需要注意的是,据传苹果现在已经开始通过技术手段扫代码,如果只是简单的更改,仍然可能会被拒。)或者,直接用新账号提交审核,必要时可考虑修改名称、图标等元数据以及功能、界面等,这样更容易过审。(注意,如果只是简单的更改,仍然可能被拒,且新账号也有被封的风险 。)

解决办法:

1、使用React Native

2、使用开关,审核时与上架后显示页面不同(此方案有被封号的风险)

3、购买有资质的账号(如银行类或具有金融许可证的,此方案*好)

4、将个人开发者账号升级为企业开发者账号,提供公司白氏编码。
企业账号的审核相对个人账号没有那么严格。

5、从代码层面进行修改

 

iOS马甲包上架首先明白一点,这个上架的app马甲包一定是不合规的.不然也不会使用马甲包上架.因为已经上架了自己的App,但是还需要上架一个这个App的马甲包.所以在原来的工程里面需要怎么做才可以呢?

解决方案:

一:UI部分
(1)、在原有的UI基础上修改新的UI,这个咩有具体的怎么修改,怎么改都行,只要和之前的不一样.
(2)、启动图不能和之前的一样
(3)、logo坚决不能一样

具体可以在同一份代码中建立多个Target,每个target的名字logo,使用的资源都不同,通过执行脚本,编译时知道对应的资源包。

二:代码部分
(1)、修改工程中文件夹名字(全部都需要修改)
(2)、修改项目名字
(3)、修改类名 ,一般都会有前缀,一键替换,然后类名的后缀一般是view/viewController/model之类的 能改就改
(4)、添加混淆代码,把其他地方的代码 ,引入到工程里面去,用不用先不管,反正拉进去就行 代码混淆工具:点击这里下载代码混淆工具
(5)、记得修改boundID
(6)、可以根据之前的App做功能部分删除或者添加部分功能,不能完全使用之前的功能,一定要修改一部分
(7)、这一条是补充说明:前两天亲测了一下,之前有上过一个账号助手的app,代码没改 ,logo没改,项目名称没改,换个boundID换个开发者账号,修改宣传文本,修改描述文本,竟然一夜之间上去了………是不是很懵逼,所以我决定过两天做个升级,试试能不能上去.请同学们耐心等待.
(8)、API 加密,首先base64加密API肯定不能再使用了,苹果都说了,加密特征太过明显。幸好之前已经把所有自定义的API名称都加上了ab_前缀,使得我们写脚本很好识别。我们将所有扫描出的API放到一个plist文件中保存在本地,然后我们建立了6个数组,每个数组中有6个单词,每次从每个数组中随机抽取一个单词。将6个单词拼接成一段方法名保存在另一个plist文件中,当然,在保存前,先去重,如果这个方法名已经用过了,那我们随机再换,这样一共可以生成46656种方法名,对于我们的工程已经够用了
(9)、修改所有资源asset包图片名称,我们利用脚本遍历本地所有png文件,当然你可以自行添加.jpg格式遍历。根据自己的命名规则将所有图片重新命名了一波。另外我们也发现网上有个轮子可以利用shell命令对所有图片资源进行超轻量级的压缩,在不影响图片质量的情况下,改变图片的hash值。当然我们的马甲包中图片没有和主界面相似的,所以的这一步我们没有实践。
(10)、类前缀替换:这一步主要是更改文件名。程序扫描*对地址下的所有文件,只要是带”XX”开头的文件都替换成”AB”这种,另外每次替换一个文件都要遍历所有文件,将所有用到这个头文件的文件内容进行更换。类前缀替换有时会有个别没有替换到,我没能定位到问题,但很少,可以手动查找替换。
(11)、生成垃圾代码:我用plist专门搞了一个垃圾方法名,每四个方法生成一个带参数名的方法。暂时每个文件里只生成一个垃圾方法。当然可以多运行几次,就会生成几个垃圾方法

三:以上两部做完以后可以打包了 ,同样有问题,因为需要上架账号是选择和之前App同一个开发者账号上架还是新的这个就要看*部和第二部你是怎么做的,如果*部和第二步做的比较好,并且你不在意随时被干掉的话,就可以直接使用同一个账号上传审核,如果主App很重要(一般都是很重要的)那就换一个新的开发者账号进行打包上架.

四:在第三步里面为什么说要换一个账号呢?
(1) 、因为是马甲包 肯定是不合规的 随时有被干掉的危险,Apple不单单是干掉你这个app还会对这个开发者账号进行处理或者给你下架如果使用同一个账号的话 ,主App就挂了.
(2) 、做马甲包就是引导用户的,刷评论什么的,还是会被Apple发现的.发现以后这个账号就又废掉了(结果参见上一条)
(3) 、反正就是使用新的账号吧,安全!

五:不要以为打包以后就可以提交审核,
在提交审核之前需要注意:
(1) 、项目描述不能和之前的一样(不要问我怎么知道的,不信你可以试试)
(2) 、项目宣传也不可以一样(不要问我怎么知道的,不信你可以试试)
(3) 、需要测试账号的,不要提供一样的账号!一般是手机号,那么多人用手机号都没有重复的,你要是提供一个一样的测试手机号,只能说明一件事,那么小概率事件都被你遇到了,你还狡辩是不明智的!

说完怎么上马甲以后再说下什么情况会被拒*:

1、项目里面有支付的sdk,但是apple审核的时候没有看到项目里面有用到支付的地方,那就会直接回复说,项目里面有隐藏功能,属于欺骗,或者去掉支付的sdk
2、 需要使用内购的部分,没有使用内购也会悲剧,比如发*物.
3、审核的时候发现了你做的隐藏功能
4、付费陪伴,这个是什么意思呢(比如付费视频聊天,聊天发消息扣费,诸如此类的线上完成任务付费的)
5、 评级不对的,(比如,社交软件里面的美女啊什么的 但是你的评级才4+的 )
6、宣传文本,宣传图片,app描述,里面有 诱导性语言的(比如,美女多多,帅哥多多等你来哦!)
7、特别注意这一条:
(1)、如果怀疑你这个app里面有问题,他会直接给你提交的版本回复你,你这个app里面有可能包含不合规定的内容,并说,让你确认,一旦发现违规内容就会封号……请注意,这个时候你不要感觉Apple已经拒*你了,其实他们没有证据,只是怀疑.所以 你这个时候 ,只要你感觉他们不会发现你的隐藏内容,你就可以直接回复他们,说自己app里面没有违规内容,自己认为app里面的都是合法的,找不到apple所说的违规内容,请apple审核人员给出更详细的违规部分的截图,(ps,网上有现成的回复文本,这个就是之前的那个大事件,具体是什么时候的我忘了……不好意思)………………不出意外的话 ,你第二天就会发现审核通过了. 如果,你看到这个回复的时候,没有胆子了,撤回了这个版本 ,恭喜你 ,你再次提交的时候,相信Apple那边一定会针对你这个app的……然后就是各种再次提交,玩的多了 这个账号就废了 。
(2)、审核的时候发现了你做的隐藏功能 (3)、付费陪伴,这个是什么意思呢(比如付费视频聊天,聊天发消息扣费,诸如此类的线上完成任务付费的)
(4)、评级不对的,(比如,社交软件里面的美女啊什么的 但是你的评级才4+的 )
(5)、宣传文本,宣传图片,app描述,里面有 诱导性语言的(比如,美女多多,帅哥多多等你来哦!)

Guideline 4.4 问题
Location data can only be used when directly relevant to the features and services provided by the App to the user or to support approved advertising uses(位置数据只能用于应用提供的直接相关功能或服务,或者有授权的广告。)

5. Push notifications(提醒推送)
Guideline 5.1问题 (数据收集和存储)
Apps that provide Push Notifications without using the Apple Push Notification (APN) API will be rejected(不使用Apple Push Notification(APN) API提供消息推送的应用会被拒。)

5.1.1 Guideline 5.1.1 – Legal – Privacy – Data Collection and Storage
问题描述:
Guideline 5.1.1 – Legal – Privacy – Data Collection and Storage
主要是App 强制用户注册,且基于不需要用户信息的功能之上、暗中采集/共享用户的个人信息。
解决方法:先与用户协商,让用户同意后注册,有“强登陆”功能的一定要修改为提示登陆的版本。

网上列子:
5.1.1数据收集和存储;在后台中,添加隐私说明地址;由于是小号,用的是个人账户,事先将内容发布在个人博客中,在后台的隐私的位置加的是个人博客的地址

拒*邮件分析:
被拒原因是因为在请求一些权限的时候没有说明功能,
eg:获取用户相机权限或者获取麦克风权限。

 

Guideline 5.1.1 – Legal – Privacy – Data Collection and Storage
We noticed that your app requests the user’s consent to access their photos but does not clarify the use of this feature in the permission modal alert.

Next Steps

To resolve this issue, please revise the permission modal alert to specify why the app is requesting access to the user’s photos.

The permission request alert should specify how your app will use this feature to help users understand why your app is requesting access to their personal data.

Resources

For additional information and instructions on configuring and presenting an alert, please review the Requesting Permission section of the iOS Human Interface Guidelines and the Information Property List Key Reference. You may also want to review the Technical Q&A QA1937: Resolving the Privacy-Sensitive Data App Rejection page for details on how to provide a usage description for permission request alerts.

Learn more about Protecting the User’s Privacy.

Please see attached screenshot for details.

解决方法:

修改权限申请描述,之前的描述过于简单,
没有告诉用户APP什么功能需要用到拍照,相册,定位,蓝牙等权限。
需要描述app 什么功能使用到了这些权限。
在info.plist文件中修改文案,标明使用这个功能是做什么的。
eg:访问相机 eg:“亲,我们需要访问您的相机,用于照片拍摄。

“NSPhotoLibraryUsageDescription” = “允许Jimu访问你的相册,用于上传照片以及保存你用此应用拍摄的照片。”;
“NSMicrophoneUsageDescription” = “允许Jimu访问你的麦克风,用于拍摄视频时录制声音。”;
“NSCameraUsageDescription” = “允许Jimu访问你的相机,用于为你的Jimu机器人拍摄照片或视频,以及扫描二维码查找产品信息。”;
“NSBluetoothPeripheralUsageDescription” = “允许Jimu访问你的蓝牙,用于连接、控制以及为Jimu机器人编程。”;
“NSLocationWhenInUseUsageDescription” = “允许Jimu访问你的地理位置,用于为你提供符合你所在地的产品及活动信息。”;

5.1.5 定位服务 问题
主要是 App 未得到允许,与第三方共享收集的用户数据,且并未说明使用目的等,例:位置、账号……

解决方法:

如果要采取用户数据信息,需要给予用户提示,
并得到用户的允许,或设置为可选,
并且明确告知苹果采集用户数据信息的使用目的。

问题分析:
5.1.5 定位服务;修改了地址获取的弹出窗口方案,写清楚需要使用地址是为了给学生推荐个性化的活动。之前权限获取的说明写的很模糊,就是需要获取您的地址。因为这个被拒后,把所有的需要用户授权的内容都找出来看了下,每个都重新写了一遍,且只在当前使用的时候才弹出来让用户授权。

苹果邮件内容:

1
问题分析:

1
解决办法:

Guideline 5.2 问题 (知识产权)
Apps that use the APN service without obtaining a Push Application ID from Apple will be rejected(使用APN服务却没从Apple获取一个Push Application ID的应用会被拒。)

主要是未经授权,使用受版权保护的第三方材料、App不得与苹果现有产品类似等。

解决方法:

确保 app 只包含由您创建或拥有使用许可的内容,
提交产品时使用受版权保护的第三方的书面证据
或者将产品中包含的未经第三方授权的部分隐藏。

5.2.1 法律 – 知识产权
问题分析:
Guideline 5.2.1 – Legal – Intellectual Property

未由拥有并负责提供应用提供的任何服务的法人提交

苹果邮件内容:

Guideline 5.2.1 – Legal – Intellectual Property
Your app facilitates, enables, or encourages live video chat or performance (网络直播/表演/秀场), but you haven’t provided a sufficient Internet Culture Business License (网络文化经营许可证) for the services in your app.
Next Steps
To resolve this issue, please complete the following:
— Provide both a copy and the direct link to the government website that displays your Internet Culture Business License (网络文化经营许可证).
— List Internet Show (网络表演/秀场) in the allowed business areas (经营范围) section of the Internet Culture Business License (网络文化经营许可证).
— Ensure the seller and artist names associated with your account match the authorized name (单位名称) listed in the Internet Culture Business License (网络文化经营许可证).
— Provide the complete license number (编号) of your Internet Culture Business License (网络文化经营许可证) in the “Review Notes” section.
Resources
To provide a copy of the Internet Culture Business License (网络文化经营许可证):

Log in to iTunes Connect
Click on “My Apps”
Select your app
Click on the app version on the left side of the screen
Scroll down to “App Review Information”
Attach the scanned copy of your Internet Culture Business License (网络文化经营许可证) in the “Attachment” section
Click “Save”

Once this information is available, please reply to this message in Resolution Center, and we can continue with our review.
翻译出来是:
准则5.2.1 – 法律 – 知识产权
您的应用程序促进,启用或鼓励实时视频聊天或表演,但您的应用程序中的服务未提供足够的互联网文化经营许可证。
下一步
要解决此问题,请完成以下步骤:

提供政府网站的副本和直接链接,以显示您的网络文化经营许可证。
在网络文化经营许可证允许的经营范围部分列出网络表演/秀场。
确保与您账户相关的卖家和艺术家名称与“互联网文化经营许可证”中列出的授权名称(单位名称)相匹配。
在“评论备注”部分提供您的互联网文化经营许可证的完整许可证编号。

资源
提供“互联网文化经营许可证”的复印件:

登录iTunes Connect
点击“我的应用程序”
选择你的应用程序
点击屏幕左侧的应用程序版本
向下滚动到“应用程序评论信息”
在“附件”一节中附上您的互联网文化经营许可证的扫描副本
点击“保存”

获得此信息后,请在解决中心回复此消息,我们可以继续进行审核。

问题分析:

苹果审核人员把我们的app认定为直播类的了。
苹果规定直播类型的app必须提供“互联网文化经营许可证”。
解决方法:
没办法,去掉那些功能,
或者让公司提示证书。
还有一种是加个开关隐藏,这个就看你运气了,
有的能被发现,有的是不能被发现。真的有点看运气了。
不过*好的解决方法是让公司提供证书。

解决办法:

Guideline 5.3 问题
Apps that send Push Notifications without first obtaining user consent will be rejected(在首次推送消息之前未取得的用户允许的应用会被拒。)

5.3.4 没有使用应用程序的所有位置的必要许可和权限
问题分析:

苹果邮件内容:

1
问题分析:

1
解决办法:

Guideline 5.4 问题
Apps that send sensitive personal or confidential information using Push Notifications will be rejected(使用提醒推送服务推送敏感的个人或机密信息的应用会被拒。)

Guideline 5.5 问题
Apps that use Push Notifications to send unsolicited messages, or for the purpose of phishing or spamming will be rejected(使用提醒推送发送主动消息,欺骗或干扰信息的应用会被拒。)

Guideline 5.6 问题
Apps cannot use Push Notifications to send advertising, promotions, or direct marketing of any kind(应用不可以使用提醒推送发送广告,活动或任何形式的直接推广信息。)

Guideline 5.7 问题
Apps cannot charge users for use of Push Notifications(应用不可以提供收费的提醒推送服务。)

Guideline 5.8 问题
Apps that excessively use the network capacity or bandwidth of the APN service or unduly burden a device with Push Notifications will be rejected(使用APN服务过度占用网络带宽或容量或通过提醒推送大量占用系统资源的应用会被拒。)

Guideline 5.9 问题
Apps that transmit viruses, files, computer code, or programs that may harm or disrupt the normal operation of the APN service will be rejected(传输病毒,文件,代码或程序,导致破坏或扰乱正常的APN服务操作的应用会被拒。)

6. Game Center(游戏中心)
Guideline 6.1 问题
Apps that display any Player ID to end users or any third party will be rejected(向终端用户或第三方展示Player ID的应用会被拒。)

Guideline 6.2 问题
Apps that use Player IDs for any use other than as approved by the Game Center terms will be rejected(Player ID被用于Game Center条款款意外的用途的应用会被拒。)

Guideline 6.3 问题
Developers that attempt to reverse lookup, trace, relate, associate, mine, harvest, or otherwise exploit Player IDs, alias, or other information obtained through the Game Center will be removed from the iOS Developer Program(试图通过Game Center反查,跟踪,描述,关联,发掘,收割,或利用Player ID,别名或其他信息的开发者会被取消IDP身份。)

Guideline 6.4 问题
Game Center information, such as Leaderboard scores, may only be used in Apps approved for use with the Game Center(Game Center信息,例如Leaderboard得分,只能通过Game Center用于应用中。)

Guideline 6.5 问题
Apps that use Game Center service to send unsolicited messages, or for the purpose of phishing or spamming will be rejected(使用Game Center发送主动消息,欺骗或干扰信息的应用会被拒。)

Guideline 6.6 问题
Apps that excessively use the network capacity or bandwidth of the Game Center will be rejected(使用Game Center过度占用网络带宽或容量的应用会被拒。)

Guideline 6.7 问题
Apps that transmit viruses, files, computer code, or programs that may harm or disrupt the normal operation of the Game Center service will be rejected(传输病毒,文件,代码或程序,导致破坏或扰乱正常的Game Center操作的应用会被拒。)

7. Advertising(广告)
Guideline 7.1 问题
Apps that artificially increase the number of impressions or click-throughs of ads will be rejected(人工刷广告浏览或点击率的应用会被拒。)

Guideline 7.2 问题
Apps that contain empty iAd banners will be rejected(带有空iAd banner广告的应用会被拒。)

Guideline 7.3 问题
Apps that are designed predominantly for the display of ads will be rejected(设计主要用来展示广告的应用会被拒。)

8. Trademarks and trade dress(商标权与商标外观)
Guideline 8.1 问题
Apps must comply with all terms and conditions explained in theGuidelines for Using Apple Trademarks and Copyrightsand theApple Trademark List(应用必须遵守Guidelines for Using Apple Trademarks and Copyrights 和Apple Trademark List中描述的所有条款和条件。)

Guideline 8.2 问题
Apps that suggest or infer that Apple is a source or supplier of the App, or that Apple endorses any particular representation regarding quality or functionality will be rejected(任何误导或暗示Apple为该应用来源或提供商,或Apple以任何形式认可其质量或功能的应用会被拒。)

Guideline 8.3 问题
Apps which appear confusingly similar to an existing Apple product or advertising theme will be rejected(外观与现有Apple产品或广告主题类似或混淆的应用会被拒)

Guideline 8.4 问题
Apps that misspell Apple product names in their App name (i.e., GPS for Iphone, iTunz) will be rejected(应用名称中出现错误的Apple产品拼写(如,GPS for IPhone, iTunz)的应用会被拒。)

Guideline 8.5 问题
Apps may not use protected third party material such as trademarks, copyrights, patents or violate 3rd party terms of use. Authorization to use such material must be provided upon request.(使用受保护的第三方资源(商标,版权,商业机密,以及其他私有内容),如果要求请提供一份文本形式的使用授权。)

9. Media content(媒体内容)
Guideline 9.1 问题
Apps that do not use the MediaPlayer framework to access media in the Music Library will be rejected(使用MediaPlayer框架以外的方法访问Music Library中媒体数据的应用会被拒。)

Guideline 9.2 问题
App user interfaces that mimic any iPod interface will be rejected(用户界面模仿任何iPod界面的应用会被拒。)

Guideline 9.3 问题
Audio streaming content over a cellular network may not use more than 5MB over 5 minutes(通过蜂窝网络传输的流媒体音频内容不得超过5MB或多余5分钟。)

Guideline 9.4 问题
Video streaming content over a cellular network longer than 10 minutes must use HTTP Live Streaming and include a baseline 64 kbps audio-only HTTP Live stream(通过蜂窝网络传输超过10分钟流媒体视频内容,必须使用HTTP Live Streaming,并包含一条基线64kbps的音频HTTP Live流。)

10. User interface(用户界面)
Guideline 10.1 问题
Apps must comply with all terms and conditions explained in theApple iOS Human Interface Guidelines(应用必须遵守Apple iOS Human Interface Guidelines中的所有条款和条件。)

Guideline 10.2 问题
Apps that look similar to Apps bundled on the iPhone, including the App Store, iTunes Store, and iBookstore, will be rejected(外观与iPhone自带应用(如:App Store,iTunes Store和iBookstore)相似的应用会被拒。)

Guideline 10.3 问题
Apps that do not use system provided items, such as buttons and icons, correctly and as described in theApple iOS Human Interface Guidelinesmay be rejected(不按照Apple iOS Human Interface Guidelines中的描述正确使用系统控件比如按钮,图标等的应用会被拒。)

Guideline 10.4 问题
Apps that create alternate desktop/home screen environments or simulate multi-App widget experiences will be rejected(试图创建多桌面/主屏环境或模拟多Widget应用工具的应用会被拒。)

Guideline 10.5 问题
Apps that alter the functions of standard switches, such as the Volume Up/Down and Ring/Silent switches, will be rejected(修改标准开关标准功能例如:音量增加/减少,响铃/震动的应用会被拒。)

Guideline 10.6 问题
Apple and our customers place a high value on simple, refined, creative, well thought through interfaces. They take more work but are worth it. Apple sets a high bar. If your user interface is complex or less than very good, it may be rejected(Apple和我们的用户都界面报以很高期望,希望他设计的超级简洁,精致,充满创造力,深思熟虑。做到这些确实会消耗很多精力,但是值得。Apple在这方面要求非常高。如果你的用户界面过于复杂,甚至仅仅是不够好,都可能被拒。)

11. Purchasing and currencies(购买与流通货币)
Guideline 11.1 问题
Apps that unlock or enable additional features or functionality with mechanisms other than the App Store will be rejected(通过App Store以外的渠道解锁或开启附加属性或功能的应用会被拒。)

Guideline 11.2 问题
Apps utilizing a system other than the In-App Purchase API (IAP) to purchase content, functionality, or services in an App will be rejected(使用In App Purchase API (IAP)以外的系统提供购买内容,功能或服务的应用会被拒。)

Guideline 11.3 问题
Apps using IAP to purchase physical goods or goods and services used outside of the application will be rejected(使用IAP为与应用无关的实体商品或商品服务收费的应用会被拒。)

Guideline 11.4 问题
Apps that use IAP to purchase credits or other currencies must consume those credits within the application(应用使用IAP购买积分(Credit)或其他货币,必须在应用中消费。)

Guideline 11.5 问题
Apps that use IAP to purchase credits or other currencies that expire will be rejected(使用IAP购买的积分(Credit)或货币会过期的应用会被拒)

Guideline 11.6 问题
Content subscriptions using IAP must last a minimum of 7 days and be available to the user from all of their iOS devices(使用IAP收费订阅的内容至少要在7天内有效,而且允许在所有iOS设备间共享。)

Guideline 11.7 问题
Apps that use IAP to purchase items must assign the correct Purchasability type(用到IAP收费项目的应用必须分派到正确的收费类目中。)

Guideline 11.8 问题
Apps that use IAP to purchase access to built-in capabilities provided by iOS, such as the camera or the gyroscope, will be rejected(使用IAP向用户收费以获取iOS内建功能(如摄像头,陀螺仪)的应用会被拒。)

Guideline 11.9 问题
Apps containing “rental” content or services that expire after a limited time will be rejected(包含“出租”内容或服务的应用,在一段时间实效的会被拒。)

Guideline 11.10 问题
Insurance applications must be free, in legal-compliance in the regions distributed, and cannot use IAP(保险类应用必须免费,遵守发布地区的法律,并且不允许使用IAP。)

Guideline 11.11 问题
In general, the more expensive your App, the more thoroughly we will review it(一般来说,越贵的应用审核就越仔细彻底。)

Guideline 11.12 问题
Apps offering subscriptions must do so using IAP, Apple will share the same 70/30 revenue split with developers for these purchases, as set forth in the DeveloperProgram License Agreement.(提供收费订阅的应用必须使用IAP,Apple将会按照Developer Program License Agreement中约定的70/30的比例与开发者分账。)

Guideline 11.13 问题
Apps that link to external mechanisms for purchases or subscriptions to be used in the App, such as a “buy” button that goes to a web site to purchase a digital book, will be rejected(应用中如果提供了IAP以外的收费或订阅机制,如:“buy”按钮,跳转到一个购买电子书的web页面,会被拒。)

Guideline 11.14 问题
Apps can read or play approved content (specifically magazines, newspapers, books, audio, music, and video) that is subscribed to or purchased outside of the App, as long as there is no button or external link in the App to purchase the approved content. Apple will not receive any portion of the revenues for approved content that is subscribed to or purchased outside of the App(应用可以阅读或播放任何在应用以外取得授权的内容(包括指定的杂志,报纸,书籍,音频,音乐和视频),只要在应用中不允许出现获取授权的收费链接或按钮。Apple不会对在应用外订阅或购买授权项目收取任何费用。)

Guideline 11.15 问题
Apps may only use auto renewing subscriptions for periodicals (newspapers, magazines), business Apps (enterprise, productivity, professional creative, cloud storage) and media Apps (video, audio, voice), or the App will be rejected.(应用只能自动更新订阅的期刊(报纸、杂志),自动更新商业应用(企业、生产力、专业创意、云存储)和媒体应用(视频、音频,声音)将被拒*。)

12. Scraping and aggregation(抓去与整合)
Guideline 12.1 问题
Applications that scrape any information from Apple sites (for example fromapple.com, iTunes Store, App Store, iTunes Connect, Apple Developer Programs, etc) or create rankings using content from Apple sites and services will be rejected(从Apple的页面(如:apple.com, iTunes Store, App Store, iTunes Connect, Apple Developer Programs, 等)抓取内容,或利用Apple页面和服务中的内容进行排名的应用会被拒。)

Guideline 12.2 问题
Applications may use approved Apple RSS feeds such as the iTunes Store RSS feed(应用可以使用授权的Apple RSS,例如iTunes Store RSS。)

Guideline 12.3 问题
Apps that are simply web clippings, content aggregators, or a collection of links, may be rejected(简单的web页面裁剪,内容整合或链接收集应用会被拒。)

13. Damage to device(损害设备)
Guideline 13.1 问题
Apps that encourage users to use an Apple Device in a way that may cause damage to the device will be rejected(任何怂恿用户做出可能损坏Apple设备的行为的应用会被拒。)

Guideline 13.2 问题
Apps that rapidly drain the device’s battery or generate excessive heat will be rejected(快速耗光设备电量或产生大量热量的应用会被拒。)

14. Personal attacks(人身攻击)
Guideline 14.1 问题
Any App that is defamatory, offensive, mean-spirited, or likely to place the targeted individual or group in harms way will be rejected(任何涉嫌诽谤,侮辱,狭隘内容或打击个人或团体的应用会被拒。)

Guideline 14.2 问题
Professional political satirists and humorists are exempt from the ban on offensive or mean-spirited commentary(职业政治讽刺家和幽默作家不受该诽谤和狭隘条款约束。)

15. Violence(暴力)
Guideline 15.1 问题
Apps portraying realistic images of people or animals being killed or maimed, shot, stabbed, tortured or injured will be rejected(展示人或动物被杀戮,致残,枪击,针刺或其他伤害的真实图片的应用会被拒)

Guideline 15.2 问题
Apps that depict violence or abuse of children will be rejected(描述暴力或虐待儿童的应用会被拒。)

Guideline 15.3 问题
“Enemies” within the context of a game cannot solely target a specific race, culture, a real government or corporation, or any other real entity(游戏中的“敌人”不能单独的设定为某特定比赛,文化,真实的政府或组织,或者任何现实事物。)

Guideline 15.4 问题
Apps involving realistic depictions of weapons in such a way as to encourage illegal or reckless use of such weapons will be rejected(含有以鼓励非法或鲁莽使用的方式描述真实武器的应用会被拒。)

Guideline 15.5 问题
Apps that include games of Russian roulette will be rejected(带有俄罗斯轮盘游戏的应用会被拒。)

16. Objectionable content(负面内容)
Guideline 16.1 问题
Apps that present excessively objectionable or crude content will be rejected(介绍过度三俗和粗鲁内容的应用会被拒。)

Guideline 16.2 问题
Apps that are primarily designed to upset or disgust users will be rejected(设计来惹怒或恶心用户的应用会被拒。)

17. Privacy(隐私)
Guideline 17.1 问题
Apps cannot transmit data about a user without obtaining the user’s prior permission and providing the user with access to information about how and where the data will be used(在未获得用户事先允许,或未告知用户信息将被如何,在哪里使用的情况下,应用不可以传输用户数据。)

Guideline 17.2 问题
Apps that require users to share personal information, such as email address and date of birth, in order to function will be rejected(要求用户提供个人信息,如邮箱地址,生日等,才能使用其功能的应用会被拒。)

Guideline 17.3 问题
Apps that target minors for data collection will be rejected(专门收集未成年人数据的应用会被拒。)

18. Pornography(色情)
Guideline 18.1 问题
Apps containing pornographic material, defined by Webster’s Dictionary as “explicit descriptions or displays of sexual organs or activities intended to stimulate erotic rather than aesthetic or emotional feelings”, will be rejected(含有韦氏词典中定义的色情素材(explicit descriptions or displays of sexual organs or activities intended to stimulate erotic rather than aesthetic or emotional feelings)的应用会被拒。)

Guideline 18.2 问题
Apps that contain user generated content that is frequently pornographic (ex “Chat Roulette” Apps) will be rejected(经常有用户提供色情内容(例如:Chat Roulette http://en.wikipedia.org/wiki/Chatroulette )的应用会被拒。)

19. Religion, culture, and ethnicity(信仰,文化和种族)
Guideline 19.1 问题
Apps containing references or commentary about a religious, cultural or ethnic group that are defamatory, offensive, mean-spirited or likely to expose the targeted group to harm or violence will be rejected(带有对一种信仰,文化或种族进行诽谤,侮辱,狭隘,或以他们为目标的暴力或伤害内容的应用会被拒。)

Guideline 19.2 问题
Apps may contain or quote religious text provided the quotes or translations are accurate and not misleading. Commentary should be educational or informative rather than inflammatory(应用若带有或应用对一种信仰的文字描述,那么这个引用或翻译必须是精确,无歧义的。注释内容可以具有教育性,信息性,但不可以为煽动性。)

20. Contests, sweepstakes, lotteries, and raffles(竞赛,赌博,彩票和抽*)
Guideline 20.1 问题
Sweepstakes and contests must be sponsored by the developer/company of the App(赌博和竞赛必须是由应用开发者或所有公司发起资助的。)

Guideline 20.2 问题
Official rules for sweepstakes and contests, must be presented in the App and make it clear that Apple is not a sponsor or involved in the activity in any manner(应用中必须展示赌博和竞赛的官方条款,并声明Apple不是资助者,并且在任何情况下与此事无关。)

Guideline 20.3 问题
It must be permissible by law for the developer to run a lottery App, and a lottery App must have all of the following characteristics: consideration, chance, and a prize(开发者必须经过法律允许才能上线一款抽*应用,而且抽*应用必须具备以下要素:报酬,机会,和*金。)

Guideline 20.4 问题
Apps that allow a user to directly purchase a lottery or raffle ticket in the App will be rejected(直接允许用户在应用中购买彩票或抽*的应用会被拒。)

21. Charities and contributions(慈善与捐助)
Guideline 21.1 问题
Apps that include the ability to make donations to recognized charitable organizations must be free(含有向已认证的慈善机构捐助功能的应用必须是免费的。)

Guideline 21.2 问题
The collection of donations must be done via a web site in Safari or an SMS(慈善募捐必须通过短信息或通过Safari访问web页面完成。)

22. Legal requirements(法律要求)
Guideline 22.1 问题
Apps must comply with all legal requirements in any location where they are made available to users. It is the developer’s obligation to understand and conform to all local laws(应用必须遵守所有发布地区当地法律。开发者有义务了解和遵守各地的法律。)

Guideline 22.2 问题
Apps that contain false, fraudulent or misleading representations or use names or icons similar to other Apps will be rejected(任何带有虚假,欺诈和带有歧义的内容的应用会被拒。)

Guideline 22.3 问题
Apps that solicit, promote, or encourage criminal or clearly reckless behavior will be rejected(任何召集,推销和股东犯罪和鲁莽行为的应用会被拒。)

Guideline 22.4 问题
Apps that enable illegal file sharing will be rejected(非法文件共享应用会被拒。)

Guideline 22.5 问题
Apps that are designed for use as illegal gambling aids, including card counters, will be rejected(任何设计用来非法赌博工具,包括算牌的应用会被拒。)

Guideline 22.6 问题
Apps that enable anonymous or prank phone calls or SMS/MMS messaging will be rejected(提供知识拨打电话或知识发送短消息/彩信功能的应用会被拒。)

Guideline 22.7 问题
Developers who create Apps that surreptitiously attempt to discover user passwords or other private user data will be removed from the iOS Developer Program(任何开发暗中获取用户密码和私有数据的开发者会被取消IDP身份。)

Guideline 22.8 问题
Apps which contain DUI checkpoints that are not published by law enforcement agencies, or encourage and enable drunk driving, will be rejected(任何非法律执行部门发布的带有DUI检查点信息,或鼓励且协助酒后驾车的应用会被拒。)

springboot以FTP方式上传文件到远程服务器

此处远程服务器是ubuntu,关于ftpserver的配置请参考该文https://blog.csdn.net/sunxiaoju/article/details/85224602,在此处就不再赘述。该篇文章主要针对如何实现代码来进行代码干货的分享。

本小编用的前台为layui框架。话不多说直接上干货,代码没什么好解释的,直接贴出前后台代码以供大家分享使用。

一、html代码

<div class=”layui-form-item”>
<label class=”layui-form-label”>上传附件:</label>
<div class=”layui-input-block doc-litpic”>
<button type=”button” name=”avatar” class=”layui-btn layui-btn-sm” data-url=”/admin/archives/upload.html” id=”larry-litpic”><i class=”layui-icon”></i>本地上传</button>
<a id=”upload-filename-display” style=”color: blue” href=””></a>
<div class=”larryms-img-view”>
</div>
</div>
</div>
二、js代码

upload.render({
accept: ‘file’,
elem: ‘#larry-litpic’,
url: interface_cms_article_upload,
field: ‘fileNames’,
done : function (res, index, upload) {
if(res.code != 200){
layer.open({
icon : 2,
skin : “layui-layer-molv”,
content : res.msg
});
}else{
layer.open({
icon : 1,
skin : “layui-layer-molv”,
content : res.msg
});
$(‘#upload-filename-display’).text(res.filename);
$(“input[name=’fileId’]”).val(res.filename);
}
},
error : function (res) {

}
});
三、后台controller:

@RequestMapping(value = “/upload”)
@ApiOperation(value = “本地文件上传”,notes =”本地文件上传” )
public Map uploadfunction(HttpServletRequest request, HttpServletResponse response){
//创建文件对象并获取请求中的文件对象
MultipartFile file = null;
Map resultData = new HashMap();

try{
MultipartHttpServletRequest mRequest = (MultipartHttpServletRequest) request;
file = mRequest.getFile(“fileNames”);

//判断上传非空
if(null == file) {
resultData.put(“code”,0);
resultData.put(“msg”,”上传文件失败”);
resultData.put(“filename”,file.getOriginalFilename());
return resultData;
}
//上传需要导入数据的文件
//用来检测程序运行时间
long startTime=System.currentTimeMillis();
System.out.println(“上传的文件名为:”+file.getOriginalFilename());
String fileName = file.getOriginalFilename();

InputStream inputStream = file.getInputStream();

String hostName = uploadUtil.getHostname();
String username = uploadUtil.getUsername();
String password = uploadUtil.getPassword();
String targetPath = uploadUtil.getTargetPath();
String suffix = cmsArticleService.getSuffix(fileName);
fileName = cmsArticleService.upload(hostName,username,password,targetPath,suffix,inputStream);
//计算上传时间
long endTime=System.currentTimeMillis();
String uploadTime = String.valueOf(endTime-startTime);
System.out.println(“上传所用时间:”+uploadTime+”ms”);

resultData.put(“code”,200);
resultData.put(“msg”,”上传文件成功”);
resultData.put(“filename”,fileName);
return resultData;

} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
四、后台service上传至远程服务器

//FTP文件上传
public static String upload(String hostname,String username,String password,
String targetPath,String suffix,InputStream inputStream) throws SocketException, IOException {
//实例化ftpClient
FTPClient ftpClient = new FTPClient();
//设置登陆超时时间,默认是20s
ftpClient.setDataTimeout(12000);
//1.连接服务器
ftpClient.connect(hostname,21);
//2.登录(指定用户名和密码)
boolean b = ftpClient.login(username,password);
if(!b) {
System.out.println(“登陸超時”);
if (ftpClient.isConnected()) {
// 断开连接
ftpClient.disconnect();
}
}
// 设置字符编码
ftpClient.setControlEncoding(“UTF-8″);
//基本路径,一定存在
String basePath=”/”;
String[] pathArray = targetPath.split(“/”);
for(String path:pathArray){
basePath+=path+”/”;
//3.指定目录 返回布尔类型 true表示该目录存在
boolean dirExsists = ftpClient.changeWorkingDirectory(basePath);
//4.如果指定的目录不存在,则创建目录
if(!dirExsists){
//此方式,每次,只能创建一级目录
boolean flag=ftpClient.makeDirectory(basePath);
if (flag){
System.out.println(“创建成功!”);
}
}
}
//重新指定上传文件的路径
ftpClient.changeWorkingDirectory(targetPath);
//5.设置上传文件的方式
ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
//使用uuid,保存文件名唯一性
String uuid= UUID.randomUUID().toString();
/**
* 6.执行上传
* remote 上传服务后,文件的名称
* local 文件输入流
* 上传文件时,如果已经存在同名文件,会被覆盖
*/
boolean uploadFlag = ftpClient.storeFile(uuid+suffix,inputStream);
if(uploadFlag)
System.out.println(“上传成功!”);
return uuid+suffix;
}
五、获取yml配置的工具类

@Data
@Component
public class UploadUtil {
@Value(“${upload.hostname}”)
private String hostname;
@Value(“${upload.username}”)
private String username;
@Value(“${upload.password}”)
private String password;
@Value(“${upload.targetPath}”)
private String targetPath;
}

git基于某个Tag修改提交

如果要在某个tag的基础上做修改,直接切换到tab,修改后是无法提交的。

因为这时HEAD指向了一个具体的commit id,而没有处在一个分支中。

解决方法
先根据这个tag新建一个分支
git checkout -b 新分支 tag名

$ git checkout -b newbranch tag1.1

然后在这个新分支上修改后,提交代码

Java SpringBoot 上传图片到服务器,完美可用

基本上每个我们项目都会有上传图片的操作

老规矩先上效果图

%title插图%num

%title插图%num

1.首先贴一下上传文件的工具类

import java.io.File;
import java.io.FileOutputStream;

/**
* @Author: Manitozhang
* @Data: 2019/1/9 16:51
* @Email: manitozhang@foxmail.com
*
* 文件工具类
*/
public class FileUtil {

public static void uploadFile(byte[] file, String filePath, String fileName) throws Exception {
File targetFile = new File(filePath);
if(!targetFile.exists()){
targetFile.mkdirs();
}
FileOutputStream out = new FileOutputStream(filePath+fileName);
out.write(file);
out.flush();
out.close();
}

}
2.再贴一下Controller层的代码

@Resource
HttpServletRequest request;

//处理文件上传
@RequestMapping(value=”/testuploadimg”, method = RequestMethod.POST)
public @ResponseBody String uploadImg(@RequestParam(“file”) MultipartFile file) {
String fileName = file.getOriginalFilename();
//设置文件上传路径
String filePath = request.getSession().getServletContext().getRealPath(“imgupload/”);
try {
FileUtil.uploadFile(file.getBytes(), filePath, fileName);
return “上传成功”;
} catch (Exception e) {
return “上传失败”;
}
}
因为我们的路径是上传到SpringBoot自带的Tomcat服务器中,所以在项目中看不到,不过可以直接获取到

声明:这个方法只要Tomcat重启之后之前上传的图片就会丢失

这就可以上传成功了,上传位置可以修改

用树莓派搭建svn服务器

打算用树莓派作为自己的服务器了, 搭建一个svn服务器是必要的, 来看看:

1.   安装svn服务器:

 

sudo apt-get install subversion
2.   创建svn仓库, 我用/home/pi/svn_taoge作为svn仓库的根路径

 

 

svnadmin create /home/pi/svn_taoge
3.   修改配置文件:

vim /home/pi/svn_taoge/conf/svnserve.conf
%title插图%num

4. 都配置好了, 现在来给用户开放权限吧, 用户名taoge, 密码xxxxxx, 如下:

%title插图%num

为了安全, 其实都应该对密码进行加密, 我这里先不考虑这些了。

5. 接下来肯定是要启动svn服务啊:

 

svnserve -d -r /home/pi/svn_taoge
可以ps看一下, 确实启动了。但是, 这里其实有个问题, 那就是当树莓派服务器重启后, svnserve并没有重启。 那怎么让svnserve开启自动重启呢? 在/etc/rc.local中加入如上语句就可以了, 试了一下, 靠谱。

 

访问svn的方法是:svn://192.168.1.34  , 其中192.168.1.34就是树莓派的ip.  访问过程, 肯定是要输入用户和密码的。

接下来, 我在Windows上安装了TortoiseSVN客户端。 怎么初次创建文件呢? 如下两种方法都可以

1. 用 svn的import功能, 初始化创建仓库文件。

2  用 svn的 check out先下载文件(实际上仓库为空), 然后add文件, 然后commit.

 

在这里, 我用方法2, 搞定, 来看看:

%title插图%num

如上是Windows下的svn客户端, 其实也可以创建linux下的svn客户端, 道理类似。比如, 可以直接在树莓派上执行 svn co svn://localhost  , 就把刚才的test.txt下载下来了, 此时, svn服务端和svn客户端在同一台机器上。

另外, 可以去树莓派服务器上看下svn服务器中是否有test.txt文件, 发现没有, 为什么呢? 因为svn服务器做了手脚, 并不会直接存test.txt文件, 想详细了解, 可以自己在网上查询原因, 简单。

 

树莓派搭建web服务器

安装nginx+sqlite+php5打造轻量级W服务器

简单介绍一下

Nginx是一款轻量级的Web 服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器,并在一个BSD-like 协议下发行。其特点是占有内存少,并发能力强,事实上nginx的并发能力确实在同类型的网页服务器中表现较好,中国大陆使用nginx网站用户有:百度、京东、新浪、网易、腾讯、淘宝等。如果建站只要求静态网页建议使用Nginx

 

SQLite,是一款轻型的数据库,是遵守ACID的关系型数据库管理系统,它包含在一个相对小的C库中。它是D.RichardHipp建立的公有领域项目。它的设计目标是嵌入式的,而且目前已经在很多嵌入式产品中使用了它,它占用资源非常的低,在嵌入式设备中,可能只需要几百K的内存就够了。它能够支持Windows/Linux/Unix等等主流的操作系统,同时能够跟很多程序语言相结合,比如 Tcl、C#、PHP、Java等

 

PHP是一种简单、轻便的服务器端脚本语言。

 

 

教程开始

*步安装Nginx

sudo apt-get install nginx

第二步安装php+sqlite

sudo apt-get install php5-fpm sqlite

第三步配置nginx

1打开文件

sudo  nano  /etc/nginx/sites-available/default

2修改文件

改之前这样
%title插图%num

改成这样

location / {

index index.php index.html index.htm ;

}

location ~ \.php$ {

fastcgi_pass      unix:/var/run/php5-fpm.sock;

fastcgi_index index.php;

fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;

include        fastcgi_params;

}

location ~ \.sqlite$ {

deny all;

}

}

第四步大功告成,重启服务

sudo service nginx restart

一个轻量级的WEB服务器已经建设好了

开始测试一下我们的服务器

编写一个php网站

 

1.建立文件

sudo vi /var/www/html/index.php

2写一个简单的网页代码

<?PHP

echo “长春工业大学 计算机科学与工程学院”;

3.保存退出

4打开浏览器输入你的树莓派IP就可以看到你的网页啦

%title插图%num

iOS项目架构总结

前言
我们常见的分层架构,有三层架构:视图层、业务层、数据层。也有四层架构:视图层、业务层、网络层、本地数据层。
这里说三层、四层,跟TCP/IP所谓的五层或者七层不是同一种概念。再具体说就是:你的架构在逻辑上设计的是几层那就是几层,具体每一层的名称和作用,没有特定的规范, 这主要是针对模块分类而言的。

1.视图层设计方案
2.网络层设计方案
3.本地持久化方案
4.动态部署方案

上面这四大点,稍微细说一下就是:

页面如何组织,才能尽可能降低业务方代码的耦合度?尽可能降低业务方开发界面的复杂度,提高他们的效率?
如何让业务开发工程师方便安全地调用网络API?然后尽可能保证用户在各种网络环境下都能有良好的体验?
当数据有在本地存取的需求的时候,如何能够保证数据在本地的合理安排?如何尽可能地减小性能消耗?
iOS应用有审核周期,如何能够通过不发版本的方式展示新的内容给用户?如何修复紧急bug?
一个好的架构
遵循代码规范代码,分类明确(没有难以区分模块的文件夹或模块)
注释明了, 逻辑清晰, 不用文档,或很少文档,就能让业务方上手
思路和方法要统一,尽量不要多元
没有横向依赖,尽可能少的跨层访问
对业务方该限制的地方有限制,该灵活的地方要给业务方创造灵活实现的条件
易测试,易拓展
保持一定量的超前性
接口少,接口参数少
低内存,高性能
一、视图层设计方案
一般来说,一个不够好的View层架构,主要原因有以下五种:
1.代码混乱不规范
2.过多继承导致的复杂依赖关系
3.模块化程度不够高,组件粒度不够细
4.横向依赖
5.架构设计失去传承

1.View层的代码结构规范
制定代码规范严格来讲不属于View层架构的事情,但它对View层架构未来的影响会比较大,也是属于架构师在设计View层架构时需要考虑的事情。制定View层规范的重要性在于:
1.提高业务方View层的可读性可维护性
2.防止业务代码对架构产生腐蚀
3.确保传承
4.保持架构发展的方向不轻易被不合理的意见所左右

1.1 一个好的VC代码结构
苹果有一套Coding Guidelines,当我们定代码结构或规范的时候,首先一定要符合这个规范。

%title插图%num

抽取我项目中的一个controller作为例子:

@interface LZMineCollectionVC ()
/* 页面控件 */
@property (nonatomic, strong) LZMineCollectionBar *collectionBar;
/* 数据源 */
@property (nonatomic, strong) NSMutableArray *favorNewsArr;
/* 临时标示 */

@end

@implementation LZMineCollectionVC

#pragma mark –
#pragma mark – life cycle

– (void)viewDidLoad {
[super viewDidLoad];

[self initializeMineCollectionVC];
[self configureCollectionBar];

}

#pragma mark –
#pragma mark – initialization

– (void)initializeMineCollectionVC
{
self.title = @”我的收藏”;
}

#pragma mark –
#pragma mark – RequestAndDataHandel

– (void)requestForMyFavorNewsData
{

}

#pragma mark –
#pragma mark – UI Layout

– (void)configureCollectionBar
{

}

#pragma mark –
#pragma mark – Actions

– (void)backToFront
{
[self.navigationController popViewControllerAnimated:YES];
}
– (void)buttonClick
{

}

#pragma mark –
#pragma mark – LZMineCollectionBarDelegate

#pragma mark –
#pragma mark – TableViewDelegate/TableViewDataSource

#pragma mark –
#pragma mark – Setter/Getter And LazyLoad

– (NSMutableArray *)favorNetwsArr
{
if (!_favorNewsArr) {
self.favorNewsArr = [NSMutableArray array];
}
return _favorNewsArr;
}

 

2.view的布局(code/xib/storyboard的选择)
尤其是在一定规模的团队开发中, 在针对View层这边的要求时,我也是建议不要用StoryBoard。实现简单的东西,用Code一样简单,实现复杂的东西,Code比StoryBoard更简单, 而且易于维护。所以我更加提倡用code去写view而不是storyboard。xib比storyboard灵活, 但没有code的易于维护, 而且项目中大量的xib文件会影响项目的效率。所以, 我认为优选Code > Xib > Storyboard.

Autolayout这边可以考虑使用Masonry,代码的可读性就能好很多.让业务工程师使用良好的工具来做View的布局,能提高他们的工作效率,也能减少bug发生的几率。

3.是否有必要统一派生基类的ViewController?
3.1 不进行统一派生的原因:
1.使用派生比不使用派生更容易增加业务方的使用成本
2.不使用派生手段一样也能达到统一设置的目的

3.2 统一派生不足:
1.集成成本
对于业务层存在的所有父类来说,它们是很容易跟项目中的其他代码纠缠不清的,这使得业务方开发时遇到一个两难问题:要么把所有依赖全部搞定,然后基于App环境(比如天猫)下开发Demo,要么就是自己Demo写好之后,按照环境要求改代码。这里的两难问题都会带来成本,都会影响业务方的迭代进度。
2.上手接受成本:新来的业务工程师有的时候不见得都记得每一个ViewController都必须要派生自TMViewController而不是直接的UIViewController, 所以定制性很差。以后有新人加入之后,都要嘱咐其继承自这个基类,所以这种方式并不可取。比如说:所有的ViewController都必须继承自TMViewController。
3.架构维护难度提升

3.3 替代方法
建议使用AOP来代替派生解决此类问题, 达到的效果也是如下:
1.业务方可以不用通过继承的方法,然后框架能够做到对ViewController的统一配置。
2.业务方即使脱离框架环境,不需要修改任何代码也能够跑完代码。业务方的ViewController一旦丢入框架环境,不需要修改任何代码,框架就能够起到它应该起的作用。

其实就是要实现不通过业务代码上对框架的主动迎合,使得业务能够被框架感知这样的功能。细化下来就是两个问题,框架要能够拦截到ViewController的生命周期,另一个问题就是,拦截的定义时机。
对于方法拦截,很容易想到Method Swizzling,那么我们可以写一个实例,在App启动的时候添加针对UIViewController的方法拦截,这是一种做法。还有另一种做法就是,使用NSObject的load函数,在应用启动时自动监听。使用后者的好处在于,这个模块只要被项目包含,就能够发挥作用,不需要在项目里面添加任何代码。
然后另外一个要考虑的事情就是,原有的TMViewController(所谓的父类)也是会提供额外方法方便子类使用的,Method Swizzling只支持针对现有方法的操作,拓展方法的话,嗯,当然是用Category啦。
我本人不赞成Category的过度使用,但鉴于Category是*典型的化继承为组合的手段,在这个场景下还是适合使用的。还有的就是,关于Method Swizzling手段实现方法拦截,业界也已经有了现成的开源库:Aspects,我们可以直接拿来使用。

推荐文章:Runtime之黑魔法-Swizzling Method

附:关于AOP
AOP(Aspect Oriented Programming),面向切片编程,这也是面向XX编程系列术语之一哈,但它跟我们熟知的面向对象编程没什么关系。
1.什么是切片?
程序要完成一件事情,一定会有一些步骤,1,2,3,4这样。这里分解出来的每一个步骤我们可以认为是一个切片。
什么是面向切片编程?
你针对每一个切片的间隙,塞一些代码进去,在程序正常进行1,2,3,4步的间隙可以跑到你塞进去的代码,那么你写这些代码就是面向切片编程。
2.为什么会出现面向切片编程?
你要想做到在每一个步骤中间做你自己的事情,不用AOP也一样可以达到目的,直接往步骤之间塞代码就好了。但是事实情况往往很复杂,直接把代码塞进去,主要问题就在于:塞进去的代码很有可能是跟原业务无关的代码,在同一份代码文件里面掺杂多种业务,这会带来业务间耦合。为了降低这种耦合度,我们引入了AOP。
3.如何实现AOP?
AOP一般都是需要有一个拦截器,然后在每一个切片运行之前和运行之后(或者任何你希望的地方),通过调用拦截器的方法来把这个jointpoint扔到外面,在外面获得这个jointpoint的时候,执行相应的代码。
在iOS开发领域,objective-C的runtime有提供了一系列的方法,能够让我们拦截到某个方法的调用,来实现拦截器的功能,这种手段我们称为Method Swizzling。Aspects通过这个手段实现了针对某个类和某个实例中方法的拦截。

另外,也可以使用protocol的方式来实现拦截器的功能,具体实现方案就是这样:

@protocol RTAPIManagerInterceptor <nsobject>
@optional
– (void)manager:(RTAPIBaseManager *)manager beforePerformSuccessWithResponse:(AIFURLResponse *)response;
– (void)manager:(RTAPIBaseManager *)manager afterPerformSuccessWithResponse:(AIFURLResponse *)response;
– (void)manager:(RTAPIBaseManager *)manager beforePerformFailWithResponse:(AIFURLResponse *)response;
– (void)manager:(RTAPIBaseManager *)manager afterPerformFailWithResponse:(AIFURLResponse *)response;
– (BOOL)manager:(RTAPIBaseManager *)manager shouldCallAPIWithParams:(NSDictionary *)params;
– (void)manager:(RTAPIBaseManager *)manager afterCallingAPIWithParams:(NSDictionary *)params;
@end
@interface RTAPIBaseManager : NSObject
@property (nonatomic, weak) id<rtapimanagerinterceptor> interceptor;
@end</rtapimanagerinterceptor></nsobject>

这么做对比Method Swizzling有个额外好处就是,你可以通过拦截器来给拦截器的实现者提供更多的信息,便于外部实现更加了解当前切片的情况。另外,你还可以更精细地对切片进行划分。Method Swizzling的切片粒度是函数粒度的,自己实现的拦截器的切片粒度可以比函数更小,更加精细。
缺点就是,你得自己在每一个插入点把调用拦截器方法的代码写上,通过Aspects(本质上就是Mehtod Swizzling)来实现的AOP,就能轻松一些。

4.架构模式(MVC、MVVM、MVCS、VIPER的选择)
4.1 MVC
任务均摊–View和Model确实是分开的,但是View和Controller却是紧密耦合的
可测试性–由于糟糕的分散性,只能对Model进行测试
易用性–与其他几种模式相比*小的代码量。熟悉的人很多,因而即使对于经验不那么丰富的开发者来讲维护起来也较为容易。
4.2 MVVM
任务均摊 – 在例子中并不是很清晰,但是事实上,MVVM的View要比MVP中的View承担的责任多。因为前者通过ViewModel的设置绑定来更新状态,而后者只监听Presenter的事件但并不会对自己有什么更新。
可测试性 – ViewModel不知道关于View的任何事情,这允许我们可以轻易的测试ViewModel。同时View也可以被测试,但是由于属于UIKit的范畴,对他们的测试通常会被忽略。
易用性 – 在我们例子中的代码量和MVP的差不多,但是在实际开发中,我们必须把View中的事件指向Presenter并且手动的来更新View,如果使用绑定的话,MVVM代码量将会小的多。
4.3 VIPER
任务均摊 – 毫无疑问,VIPER是任务划分中的佼佼者。
可测试性 – 不出意外地,更好的分布性就有更好的可测试性。
易用性 – *后你可能已经猜到了维护成本方面的问题。你必须为很小功能的类写出大量的接口。
4.4 总结
这些架构模式还是要根据你的项目需求, 项目规模等条件来进行选择。项目规模越小, 越简单的话, 就尽量使用*基本的MVC, 项目再复杂一些的话, 可以选择使用MVP, MVVM, 更加繁琐的项目的话, 那VIPER就可以排上用场了。你会发现, 这个顺序其实是由简至繁的, 而为什么要做这样的选择呢? 因为他们都是遵循单一责任原则的, 当简单的项目繁重后, 尽量开辟出新的角色, 将其工作任务单一化, 这样就可以达到项目思路清晰, 易于测试, 易用性高, 维护成本低等要求了。

其实, 你会发现其实这些架构模式都是可以从MVC的模式下拆分出来的。 我个人认为, 在做具体的架构设计时,不需要拘泥于MVC、MVVM、VIPER等死规矩, 也可以自己做一些小的改变, 但要记住只能拆分其它不重要的任务, 而且拆分后的模块要尽可能提高可复用性和抽象度。
具体想了解的可以参考博主之前的博客: iOS架构模式(MVC/MVCS/MVP/MVVM/VIPER)

5.减少横向依赖和跨层访问
跨业务页面调用是指,当一个App中存在A业务,B业务等多个业务时,B业务有可能会需要展示A业务的某个页面,A业务也有可能会调用其他业务的某个页面。在小规模的App中,我们直接import其他业务的某个ViewController然后或者push或者present,是不会产生特别大的问题的。但是如果App的规模非常大,涉及业务数量非常多,再这么直接import就会出现问题。

%title插图%num
可以看出,跨业务的页面调用在多业务组成的App中会导致横向依赖。那么像这样的横向依赖,如果不去设法解决,会导致什么样的结果?
1.当一个需求需要多业务合作开发时,如果直接依赖,会导致某些依赖层上端的业务工程师在前期空转,依赖层下端的工程师任务繁重,而整个需求完成的速度会变慢,影响的是团队开发迭代速度。
2.当要开辟一个新业务时,如果已有各业务间直接依赖,新业务又依赖某个旧业务,就导致新业务的开发环境搭建困难,因为必须要把所有相关业务都塞入开发环境,新业务才能进行开发。影响的是新业务的响应速度。
3.当某一个被其他业务依赖的页面有所修改时,比如改名,涉及到的修改面就会特别大。影响的是造成任务量和维护成本都上升的结果。
当然,如果App规模特别小,这三点带来的影响也会特别小,但是在阿里这样大规模的团队中,像天猫/淘宝这样大规模的App,一旦遇上这里面哪怕其中一件事情,就会很坑很坑。

6.跨业务如何处理?
让依赖关系下沉。
怎么让依赖关系下沉?引入Mediator模式。
所谓引入Mediator模式来让依赖关系下沉,实质上就是每次呼唤页面的时候,通过一个中间人来召唤另外一个页面,这样只要每个业务依赖这个中间人就可以了,中间人的角色就可以放在业务层的下面一层,这就是依赖关系下沉。

%title插图%num

当A业务需要调用B业务的某个页面的时候,将请求交给Mediater,然后由Mediater通过某种手段获取到B业务页面的实例,交还给A就行了。在具体实现这个机制的过程中,有以下几个问题需要解决:
1.设计一套通用的请求机制,请求机制需要跟业务剥离,使得不同业务的页面请求都能够被Mediater处理
2.设计Mediater根据请求如何获取其他业务的机制,Mediater需要知道如何处理请求,上哪儿去找到需要的页面
这个看起来就非常像我们web开发时候的URL机制,发送一个Get或Post请求,CGI调用脚本把请求分发给某个Controller下的某个Action,然后返回HTML字符串到浏览器去解析。苹果本身也实现了一套跨App调用机制,它也是基于URL机制来运转的,只不过它想要解决的问题是跨App的数据交流和页面调用,我们想要解决的问题是降低各业务的耦合度。
不过我们还不能直接使用苹果原生的这套机制,因为这套机制不能够返回对象实例。而我们希望能够拿到对象实例,这样不光可以做跨业务页面调用,也可以做跨业务的功能调用。另外,我们又希望我们的Mediater也能够跟苹果原生的跨App调用兼容,这样就又能帮业务方省掉一部分开发量。
就我目前所知道的情况,AutoCad旗下某款iOS应用(时间有点久我不记得是哪款应用了,如果你是AutoCad的iOS开发,可以在评论区补充一下。)就采用了这种页面调用方式。天猫里面目前也在使用这套机制,只是这一块由于历史原因存在新老版本混用的情况,因此暂时还没能够很好地发挥应有的作用。

7.视图层总结
1.制定良好的规范,规定好代码在文件中的布局,尤其是ViewController
2.View布局的选择, 灵活复用, 恰到好处
3.尽可能减少继承层级,涉及苹果原生对象的尽量不要继承
4.针对项目实际情况, 选择好合适的架构模式(MVC、MVCS、MVVM、VIPER)
5.根据业务情况针对ViewController做好拆分,好好利用工具集进行开发

二、网络层设计方案
1.网络层跟业务对接部分的设计
2.网络层的安全机制实现
3.网络层的优化方案

1.网络层跟业务对接部分的设计
1.1 使用哪种交互模式来跟业务层做对接?
iOS开发领域有很多对象间数据的传递方式,我看到的大多数App在网络层所采用的方案主要集中于这三种:Delegate,Notification,Block。KVO和Target-Action我目前还没有看到有使用的。
然而在我这边,我的意见是以Delegate为主,Notification为辅。原因如下:
1.尽可能减少跨层数据交流的可能,限制耦合
2.统一回调方法,便于调试和维护
3.在跟业务层对接的部分只采用一种对接手段(在我这儿就是只采用delegate这一个手段)限制灵活性,以此来交换应用的可维护性

2.尽可能减少跨层数据交流的可能,限制耦合
说得具象一点就是,我们考虑这样一种情况:A<-B<-C。当C有什么事件,通过某种方式告知B,然后B执行相应的逻辑。一旦告知方式不合理,让A有了跨层知道C的事件的可能,你就很难保证A层业务工程师在将来不会对这个细节作处理。一旦业务工程师在A层产生处理操作,有可能是补充逻辑,也有可能是执行业务,那么这个细节的相关处理代码就会有一部分散落在A层。然而前者是不应该散落在A层的,后者有可能是需求。另外,因为B层是对A层抽象的,执行补充逻辑的时候,有可能和B层针对这个事件的处理逻辑产生冲突,这是我们很不希望看到的。
严格来说,使用Notification来进行网络层和业务层之间数据的交换,并不代表这一定就是跨层数据交流,但是使用Notification给跨层数据交流开了一道口子,因为Notification的影响面不可控制,只要存在实例就存在被影响的可能。另外,这也会导致谁都不能保证相关处理代码就在唯一的那个地方,进而带来维护灾难。
所以,为了符合前面所说的这些要求,使用Delegate能够很好地避免跨层访问,同时限制了响应代码的形式,相比Notification而言有更好的可维护性。

3.为什么尽量不要用block。

block很难追踪,难以维护
block会延长相关对象的生命周期
block会给内部所有的对象引用计数加一,这一方面会带来潜在的retain cycle,不过我们可以通过Weak Self的手段解决。另一方面比较重要就是,它会延长对象的生命周期。
然而使用delegate就不会有这样的问题,delegate是弱引用,哪怕请求仍然在外面飞,,ViewController还是能够及时被回收的,回收之后指针自动被置为了nil,无伤大雅。
所以平时尽量不要滥用block,尤其是在网络层这里。
4.统一回调方法,便于调试和维护
在网络请求和网络层接受请求的地方时,使用Block没问题。但是在获得数据交给业务方时,*好还是通过Delegate去通知到业务方。因为Block所包含的回调代码跟调用逻辑放在同一个地方,会导致那部分代码变得很长,因为这里面包括了调用前和调用后的逻辑。从另一个角度说,这在一定程度上违背了single function,single task的原则,在需要调用API的地方,就只要写API调用相关的代码,在回调的地方,写回调的代码。

这实质上跟使用Delegate的手段没有什么区别,只是绕了一下,不过还是没有解决统一回调方法的问题,因为block里面写的方法名字可能在不同的ViewController对象中都会不一样,毕竟业务工程师也是很多人,各人有各人的想法。所以架构师在这边不要贪图方便,还是使用delegate的手段吧,业务工程师那边就能不用那么绕了。Block是目前大部分第三方网络库都采用的方式,因为在发送请求的那一部分,使用Block能够比较简洁,因此在请求那一层是没有问题的,只是在交换数据之后,还是转变成delegate比较好,比如AFNetworking里面:

[AFNetworkingAPI callApiWithParam:self.param successed:^(Response *response){
if ([self.delegate respondsToSelector:@selector(successWithResponse:)]) {
[self.delegate successedWithResponse:response];
}
} failed:^(Request *request, NSError *error){
if ([self.delegate respondsToSelector:@selector(failedWithResponse:)]) {
[self failedWithRequest:request error:error];
}
}];

1.2 交付什么样的数据给业务层?是否有必要将API返回的数据封装成对象然后再交付给业务层?
我见过非常多的App的网络层在拿到JSON数据之后,会将数据转变成对应的对象原型。注意,我这里指的不是NSDictionary,而是类似Item这样的对象。这种做法是能够提高后续操作代码的可读性的。在比较直觉的思路里面,是需要这部分转化过程的,但这部分转化过程的成本是很大的,主要成本在于:
1.数组内容的转化成本较高:数组里面每项都要转化成Item对象,如果Item对象中还有类似数组,就很头疼。
2.转化之后的数据在大部分情况是不能直接被展示的,为了能够被展示,还需要第二次转化。
3.只有在API返回的数据高度标准化时,这些对象原型(Item)的可复用程度才高,否则容易出现类型爆炸,提高维护成本。
4.调试时通过对象原型查看数据内容不如直接通过NSDictionary/NSArray直观。
5.同一API的数据被不同View展示时,难以控制数据转化的代码,它们有可能会散落在任何需要的地方。

而我自己用总结一下,reformer(名字而已,叫什么都好)事实上是把转化的代码封装之后再从主体业务中拆分了出来,拆分出来之后不光降低了原有业务的复杂度,更重要的是,它提高了数据交付的灵活性。另外,由于Controller负责调度Manager和View,因此它是知道Manager和View之间的关系的,Controller知道了这个关系之后,就有了充要条件来为不同的View选择不同的Reformer,并用这个Reformer去改造Mananger的数据,然后ViewController获得了经过reformer处理过的数据之后,就可以直接交付给view去使用。Controller因此得到瘦身,负责业务数据转化的这部分代码也不用写在Controller里面,提高了可维护性。

所以reformer机制能够带来以下好处:

好处1:绕开了API数据原型的转换,避免了相关成本。
好处2:在处理单View对多API,以及在单API对多View的情况时,reformer提供了非常优雅的手段来响应这种需求,隔离了转化逻辑和主体业务逻辑,避免了维护灾难。
好处3:转化逻辑集中,且将转化次数转为只有一次。使用数据原型的转化逻辑至少有两次,*次是把JSON映射成对应的原型,第二次是把原型转变成能被View处理的数据。reformer一步到位。另外,转化逻辑在reformer里面,将来如果API数据有变,就只要去找到对应reformer然后改掉就好了。
好处4:Controller因此可以省去非常多的代码,降低了代码复杂度,同时提高了灵活性,任何时候切换reformer而不必切换业务逻辑就可以应对不同View对数据的需要。
好处5:业务数据和业务有了适当的隔离。这么做的话,将来如果业务逻辑有修改,换一个reformer就好了。如果其他业务也有相同的数据转化逻辑,其他业务直接拿这个reformer就可以用了,不用重写。另外,如果controller有修改(比如UI交互方式改变),可以放心换controller,完全不用担心业务数据的处理。
对于业务层而言,由Controller根据View和APIManager之间的关系,选择合适的reformer将View可以直接使用的数据(甚至reformer可以用来直接生成view)转化好之后交付给View。对于网络层而言,只需要保持住原始数据即可,不需要主动转化成数据原型。然后数据采用NSDictionary加Const字符串key来表征,避免了使用对象来表征带来的迁移困难,同时不失去可读性。

1.3 使用集约化调用方式还是离散型调用方式去调用API?
集约型API调用其实就是所有API的调用只有一个类,然后这个类接收API名字,API参数,以及回调着陆点(可以是target-action,或者block,或者delegate等各种模式的着陆点)作为参数。然后执行类似startRequest这样的方法,它就会去根据这些参数起飞去调用API了,然后获得API数据之后再根据指定的着陆点去着陆。
离散型API调用是这样的,一个API对应于一个APIManager,然后这个APIManager只需要提供参数就能起飞,API名字、着陆方式都已经集成入APIManager中。

集约型API调用和离散型API调用这两者实现方案不是互斥的,单看下层,大家都是集约型。因为发起一个API请求之后,除去业务相关的部分(比如参数和API名字等),剩下的都是要统一处理的:加密,URL拼接,API请求的起飞和着陆,这些处理如果不用集约化的方式来实现,作者非癫即痴。然而对于整个网络层来说,尤其是业务方使用的那部分,我倾向于提供离散型的API调用方式,并不建议在业务层的代码直接使用集约型的API调用方式。原因如下:

原因1:当前请求正在外面飞着的时候,根据不同的业务需求存在两种不同的请求起飞策略:一个是取消新发起的请求,等待外面飞着的请求着陆。另一个是取消外面飞着的请求,让新发起的请求起飞。集约化的API调用方式如果要满足这样的需求,那么每次要调用的时候都要多写一部分判断和取消的代码,手段就做不到很干净。
原因2:便于针对某个API请求来进行AOP。在集约型的API调用方式下,如果要针对某个API请求的起飞和着陆过程进行AOP,这代码得写成什么样。
原因3:当API请求的着陆点消失时,离散型的API调用方式能够更加透明地处理这种情况。
2.网络层的安全机制
2.1 判断API的调用请求是来自于经过授权的APP
使用这个机制的目的主要有两点:
1.确保API的调用者是来自你自己的APP,防止竞争对手爬你的API
2.如果你对外提供了需要注册才能使用的API平台,那么你需要有这个机制来识别是否是注册用户调用了你的API

解决方案:设计签名
要达到*个目的其实很简单,服务端需要给你一个密钥,每次调用API时,你使用这个密钥再加上API名字和API请求参数算一个hash出来,然后请求的时候带上这个hash。服务端收到请求之后,按照同样的密钥同样的算法也算一个hash出来,然后跟请求带来的hash做一个比较,如果一致,那么就表示这个API的调用者确实是你的APP。为了不让别人也获取到这个密钥,你*好不要把这个密钥存储在本地,直接写死在代码里面就好了。另外适当增加一下求Hash的算法的复杂度,那就是各种Hash算法(比如MD5)加点盐,再回炉跑一次Hash啥的。这样就能解决*个目的了:确保你的API是来自于你自己的App。
一般情况下大部分公司不会出现需要满足第二种情况的需求,除非公司开发了自己的API平台给第三方使用。这个需求跟上面的需求有一点不同:符合授权的API请求者不只是一个。所以在这种情况下,需要的安全机制会更加复杂一点。
这里有一个较容易实现的方案:客户端调用API的时候,把自己的密钥通过一个可逆的加密算法加密后连着请求和加密之后的Hash一起送上去。当然,这个可逆的加密算法肯定是放在在调用API的SDK里面,编译好的。然后服务端拿到加密后的密钥和加密的Hash之后,解码得到原始密钥,然后再用它去算Hash,*后再进行比对。

2.2 保证传输数据的安全
使用这个机制的主要目的有两点:
1.防止中间人攻击,比如说运营商很喜欢往用户的Http请求里面塞广告…
2.SPDY依赖于HTTPS,而且是未来HTTP/2的基础,他们能够提高你APP在网络层整体的性能。

解决方案:HTTPS
目前使用HTTPS的主要目的在于防止运营商往你的Response Data里面加广告啥的(中间人攻击),面对的威胁范围更广。从2011年开始,国外业界就已经提倡所有的请求(不光是API,还有网站)都走HTTPS,国内差不多晚了两年(2013年左右)才开始提倡这事,天猫是这两个月才开始做HTTPS的全APP迁移。
关于速度,HTTPS肯定是比HTTP慢的,毕竟多了一次握手,但挂上SPDY之后,有了链接复用,这方面的性能就有了较大提升。这里的性能提升并不是说一个请求原来要500ms能完成,然后现在只要300ms,这是不对的。所谓整体性能是基于大量请求去讨论的:同样的请求量(假设100个)在短期发生时,挂上SPDY之后完成这些任务所要花的时间比不用SPDY要少。SPDY还有Header压缩的功能,不过因为一个API请求本身已经比较小了,压缩数据量所带来的性能提升不会特别明显,所以就单个请求来看,性能的提升是比较小的。不过这是下一节要讨论的事儿了,这儿只是顺带说一下。

2.3 安全机制小总结
这一节说了两种安全机制,一般来说*种是标配,第二种属于可选配置。不过随着我国互联网基础设施的完善,移动设备性能的提高,以及优化技术的提高,第二种配置的缺点(速度慢)正在越来越微不足道,因此HTTPS也会成为不久之后的未来App的网络层安全机制标配。各位架构师们,如果你的App还没有挂HTTPS,现在就已经可以开始着手这件事情了。

3.网络层的优化方案
网络层的优化手段主要从以下三方面考虑:
1.针对链接建立环节的优化
2.针对链接传输数据量的优化
3.针对链接复用的优化

这三方面是所有优化手段的内容,各种五花八门的优化手段基本上都不会逃脱这三方面,下面我就会分别针对这三方面讲一下各自对应的优化手段。

3.1 针对链接建立环节的优化
在API发起请求建立链接的环节,大致会分这些步骤:
1.发起请求
2.DNS域名解析得到IP
3.根据IP进行三次握手(HTTPS四次握手),链接建立成功

3.1.1 针对发起请求的优化手段
其实要解决的问题就是网络层该不该为此API调用发起请求。

1.使用缓存手段减少请求的发起次数
对于大部分API调用请求来说,有些API请求所带来的数据的时效性是比较长的,比如商品详情,比如App皮肤等。那么我们就可以针对这些数据做本地缓存,这样下次请求这些数据的时候就可以不必再发起新的请求。
一般是把API名字和参数拼成一个字符串然后取MD5作为key,存储对应返回的数据。这样下次有同样请求的时候就可以直接读取这里面的数据。关于这里有一个缓存策略的问题需要讨论:什么时候清理缓存?要么就是根据超时时间限制进行清理,要么就是根据缓存数据大小进行清理。这个策略的选择要根据具体App的操作日志来决定。

再比如网络图片缓存,数据量基本上都特别大,这种就比较适合针对缓存大小来清理缓存的策略。
另外,之前的缓存的前提都是基于内存的。我们也可以把需要清理的缓存存储在硬盘上(APP的本地存储,我就先用硬盘来表示了,虽然很少有手机硬盘的说法,哈哈),比如前面提到的图片缓存,因为图片很有可能在很长时间之后,再被显示的,那么原本需要被清理的图片缓存,我们就可以考虑存到硬盘上去。当下次再有显示网络图片的需求的时候,我们可以先从内存中找,内存找不到那就从硬盘上找,这都找不到,那就发起请求吧。

2.使用策略来减少请求的发起次数
这个我在前面提到过,就是针对重复请求的发起和取消,是有对应的请求策略的。我们先说取消策略。
如果是界面刷新请求这种,而且存在重复请求的情况(下拉刷新时,在请求着陆之前用户不断执行下拉操作),那么这个时候,后面重复操作导致的API请求就可以不必发送了。
如果是条件筛选这种,那就取消前面已经发送的请求。虽然很有可能这个请求已经被执行了,那么取消所带来的性能提升就基本没有了。但如果这个请求还在队列中待执行的话,那么对应的这次链接就可以省掉了。
以上是一种,另外一种情况就是请求策略:类似用户操作日志的请求策略。
用户操作会触发操作日志上报Server,这种请求特别频繁,但是是暗地里进行的,不需要用户对此有所感知。所以也没必要操作一次就发起一次的请求。在这里就可以采用这样的策略:在本地记录用户的操作记录,当记录满30条的时候发起一次请求将操作记录上传到服务器。然后每次App启动的时候,上传一次上次遗留下来没上传的操作记录。这样能够有效降低用户设备的耗电量,同时提升网络层的性能。

3.1.2 针对DNS域名解析做的优化,以及建立链接的优化
其实在整个DNS链路上也是有DNS缓存的,理论上也是能够提高速度的。这个链路上的DNS缓存在PC用户上效果明显,因为PC用户的DNS链路相对稳定,信号源不会变来变去。但是在移动设备的用户这边,链路上的DNS缓存所带来的性能提升就不太明显了。因为移动设备的实际使用场景比较复杂,网络信号源会经常变换,信号源每变换一次,对应的DNS解析链路就会变换一次,那么原链路上的DNS缓存就不起作用了。而且信号源变换的情况特别特别频繁,所以对于移动设备用户来说,链路的DNS缓存我们基本上可以默认为没有。所以大部分时间是手机系统自带的本地DNS缓存在起作用,但是一般来说,移动设备上网的需求也特别频繁,专门为我们这个App所做的DNS缓存很有可能会被别的DNS缓存给挤出去被清理掉,这种情况是特别多的,用户看一会儿知乎刷一下微博查一下地图逛一逛点评再聊个Q,回来之后很有可能属于你自己的App的本地DNS缓存就没了。这还没完,这里还有一个只有在中国特色社会主义的互联网环境中才会有的问题:国内的互联网环境由于GFW的存在,就使得DNS服务速度会比正常情况慢不少。
基于以上三个原因所导致的*终结果就是,API请求在DNS解析阶段的耗时会很多。
那么针对这个的优化方案就是,索性直接走IP请求,那不就绕过DNS服务的耗时了嘛。
另外一个,就是上面提到的建立链接时候的第三步,国内的网络环境分北网通南电信(当然实际情况更复杂,这里随便说说),不同服务商之间的连接,延时是很大的,我们需要想办法让用户在*适合他的IP上给他提供服务,那么就针对我们绕过DNS服务的手段有一个额外要求:尽可能不要让用户使用对他来说很慢的IP。
所以综上所述,方案就应该是这样:本地有一份IP列表,这些IP是所有提供API的服务器的IP,每次应用启动的时候,针对这个列表里的所有IP取ping延时时间,然后取延时时间*小的那个IP作为今后发起请求的IP地址。

3.2 针对链接传输数据量的优化
这个很好理解,传输的数据少了,那么自然速度就上去了。这里没什么花样可以讲的,就是压缩呗。各种压缩。

3.3 针对链接复用的优化
建立链接本身是属于比较消耗资源的操作,耗电耗时。SPDY自带链接复用以及数据压缩的功能,所以服务端支持SPDY的时候,App直接挂SPDY就可以了。如果服务端不支持SPDY,也可以使用PipeLine,苹果原生自带这个功能。
一般来说业界内普遍的认识是SPDY优于PipeLine,然后即便如此,SPDY能够带来的网络层效率提升其实也没有文献上的图表那么明显,但还是有性能提升的。还有另外一种比较笨的链接复用的方法,就是维护一个队列,然后将队列里的请求压缩成一个请求发出去,之所以会存在滞留在队列中的请求,是因为在上一个请求还在外面飘的时候。这种做法*终的效果表面上看跟链接复用差别不大,但并不是真正的链接复用,只能说是请求合并。
还是说回来,我建议*好是用SPDY,SPDY和pipeline虽然都属于链接复用的范畴,但是pipeline并不是真正意义上的链接复用,SPDY的链接复用相对pipeline而言更为彻底。SPDY目前也有现成的客户端SDK可以使用,一个是twitter的CocoaSPDY,另一个是Voxer/iSPDY,这两个库都很活跃,大家可以挑合适的采用。
不过目前业界趋势是倾向于使用HTTP/2.0来代替SPDY,不过目前HTTP/2.0还没有正式出台,相关实现大部分都处在demo阶段,所以我们还是先SPDY搞起就好了。未来很有可能会放弃SPDY,转而采用HTTP/2.0来实现网络的优化。这是要提醒各位架构师注意的事情。嗯,我也不知道HTTP/2.0什么时候能出来。

4 总结
*部分主要讲了网络层应当如何跟业务层进行数据交互,进行数据交互时采用怎样的数据格式,以及设计时代码结构上的一些问题,诸如继承的处理,回调的处理,交互方式的选择,reformer的设计,保持数据可读性等等等等,主要偏重于设计(这可是艺术活,哈哈哈)。
第二部分讲了网络安全上,客户端要做的两点。当然,从网络安全的角度上讲,服务端也要做很多很多事情,客户端要做的一些边角细节的事情也还会有很多,比如做一些代码混淆,尽可能避免代码中明文展示key。不过大头主要就是这两个,而且也都是需要服务端同学去配合的。主要偏重于介绍。(主要是也没啥好实践的,google一下教程照着来就好了)。
第三部分讲了优化,优化的所有方面都已经列出来了,如果业界再有七七八八的别的手段,也基本逃离不出本文的范围。这里有些优化手段是需要服务端同学配合的,有些不需要,大家看各自情况来决定。主要偏重于实践。

RTNetworking是原文作者当年设计并实现的安居客的网络层架构代码。当然,该脱敏的地方我都已经脱敏了,所以编不过是正常的,哈哈哈。但是代码比较齐全,重要地方注释我也写了很多。

三、动态部署方案
这里讨论的动态部署方案,就是指通过不发版的方式,将新的内容、新的业务流程部署进已发布的App。
其实单纯就动态部署方案来讲,没什么太多花头可以说的,就是H5、Lua、JS、OC/Swift这几门基本技术的各种组合排列。写到后面觉得,动态部署方案其实是非常好的用于讲解某些架构模式的背景。一般我们经验总结下来的架构模式包括但不限于:
1.Layered Architecture
2.Event-Driven Architecture
3.Microkernel Architecture
4.Microservices Architecture
5.Space-Based Architecture
另外,上述五种架构模式在Software Architecture Patterns这本书里有非常详细的介绍

1.Web App
实现方案
其实所谓的web app,就是通过手机上的浏览器进行访问的H5页面。这个H5页面是针对移动场景特别优化的,比如UI交互等。
优点
无需走苹果流程,所有苹果流程带来的成本都能避免,包括审核周期、证书成本等。
版本更新跟网页一样,随时生效。
不需要Native App工程师的参与,而且市面上已经有很多针对这种场景的框架。
缺点
由于每一页都需要从服务器下载,因此web app重度依赖网络环境。
同样的UI效果使用web app来实现的话,流畅度不如Native,比较影响用户体验。
本地持久化的部分很难做好,绕过本地持久化的部分的办法就是提供账户体系,对应账户的持久化数据全部存在服务端。
即时响应方案、远程通知实现方案、移动端传感器的使用方案复杂,维护难度大。
安全问题,H5页面等于是所有东西都暴露给了用户,如果对安全要求比较高的,很多额外的安全机制都需要在服务端实现。
总结
web app一般是创业初期会重点考虑的方案,因为迭代非常快,而且创业初期的主要目标是需要验证模式的正确性,并不在于提供非常好的用户体验,只需要完成闭环即可。早年facebook曾经尝试过这种方案,*后因为用户体验的问题而宣布放弃。所以这个方案只能作为过渡方案,或者当App不可用时,作为降级方案使用。

2.Hybrid App
通过市面上各种Hybrid框架,来做H5和Native的混合应用,或者通过JS Bridge来做到H5和Native之间的数据互通。
优点
除了要承担苹果流程导致的成本以外,具备所有web app的优势
能够访问本地数据、设备传感器等
缺点
跟web app一样存在过度依赖网络环境的问题
用户体验也很难做到很好
安全性问题依旧存在
大规模的数据交互很难实现,例如图片在本地处理后,将图片传递给H5
总结
Hybrid方案更加适合跟本地资源交互不是很多,然后主要以内容展示为主的App。在天猫App中,大量地采用了JS Bridge的方式来让H5跟Native做交互,因为天猫App是一个以内容展示为主的App,且营销活动多,周期短,比较适合Hybrid。

3.React-Native
严格来说,React-Native应当放到Hybrid那一节去讲,单独拎出来的原因是Facebook自从放出React-Native之后,业界讨论得非常激烈。天猫的鬼道也做了非常多的关于React-Native的分享。
React-Native这个框架比较特殊,它展示View的方式依然是Native的View,然后也是可以通过URL的方式来动态生成View。而且,React-Native也提供了一个Bridge通道来做Javascript和Objective-C之间的交流,还是很贴心的。
然而研究了一下发现有一个比较坑的地方在于,解析JS要生成View时所需要的View,是要本地能够提供的。举个例子,比如你要有一个特定的Mapview,并且要响应对应的delegate方法,在React-Native的环境下,你需要先在Native提供这个Mapview,并且自己实现这些delegate方法,在实现完方法之后通过Bridge把数据回传给JS端,然后重新渲染。
在这种情况下我们就能发现,其实React-Native在使用View的时候,这些View是要经过本地定制的,并且将相关方法通过RCT_EXPORT_METHOD暴露给js,js端才能正常使用。在我看来,这里在一定程度上限制了动态部署时的灵活性,比如我们需要在某个点击事件中展示一个动画或者一个全新的view,由于本地没有实现这个事件或没有这个view,React-Native就显得捉襟见肘。
优点
响应速度很快,只比Native慢一点,比webview快很多。
能够做到一定程度上的动态部署
缺点
组装页面的元素需要Native提供支持,一定程度上限制了动态部署的灵活性。
总结
由于React-Native框架中,因为View的展示和View的事件响应分属于不同的端,展示部分的描述在JS端,响应事件的监听和描述都在Native端,通过Native转发给JS端。所以,从做动态部署的角度上讲,React-Native只能动态部署新View,不能动态部署新View对应的事件。当然,React-Native本身提供了很多基础组件,然而这个问题仍然还是会限制动态部署的灵活性。因为我们在动态部署的时候,大部分情况下是希望View和事件响应一起改变的。
另外一个问题就在于,View的原型需要从Native中取,这个问题相较于上面一个问题倒是显得不那么严重,只是以后某个页面需要添加某个复杂的view的时候,需要从现有的组件中拼装罢了。
所以,React-Native事实上解决的是如何不使用Objc/Swift来写iOS App的View的问题,对于如何通过不发版来给已发版的App更新功能这样的问题,帮助有限。

4.Lua Patch
大众点评的屠毅敏同学在基于wax的基础上写了waxPatch,这个工具的主要原理是通过lua来针对objc的方法进行替换,由于lua本身是解释型语言,可以通过动态下载得到,因此具备了一定的动态部署能力。然而iOS系统原生并不提供lua的解释库,所以需要在打包时把lua的解释库编译进app。
优点
能够通过下载脚本替换方法的方式,修改本地App的行为。
执行效率较高
缺点
对于替换功能来说,lua是很不错的选择。但如果要添加新内容,实际操作会很复杂
很容易改错,小问题变成大问题
总结
lua的解决方案在一定程度上解决了动态部署的问题。实际操作时,一般不使用它来做新功能的动态部署,主要还是用于修复bug时代码的动态部署。实际操作时需要注意的另外一点是,真的很容易改错,尤其是你那个方法特别长的时候,所以改了之后要彻底回归测试一次。

5.Javascript Patch
这个工作原理其实跟上面说的lua那套方案的工作原理一样,只不过是用javascript实现。而且*近新出了一个JSPatch这个库,相当好用。
优点
同Lua方案的优点
打包时不用将解释器也编译进去,iOS自带JavaScript的解释器,只不过要从iOS7.0以后才支持。
缺点
同Lua方案的缺点
总结
在对app打补丁的方案中,目前我更倾向于使用JSPatch的方案,在能够完成Lua做到的所有事情的同时,还不用编一个JS解释器进去,而且会javascript的人比会lua的人多,技术储备比较好做。

6.JSON Descripted View
其实这个方案的原理是这样的:使用JSON来描述一个View应该有哪些元素,以及元素的位置,以及相关的属性,比如背景色,圆角等等。然后本地有一个解释器来把JSON描述的View生成出来。
这跟React-Native有点儿像,一个是JS转Native,一个是JSON转Native。但是同样有的问题就是事件处理的问题,在事件处理上,React-Native做得相对更好。因为JSON不能够描述事件逻辑,所以JSON生成的View所需要的事件处理都必须要本地事先挂好。
优点
能够自由生成View并动态部署
缺点
天猫实际使用下来,发现还是存在一定的性能问题,不够快
事件需要本地事先写好,无法动态部署事件
总结
其实JSON描述的View比React-Native的View有个好处就在于对于这个View而言,不需要本地也有一套对应的View,它可以依据JSON的描述来自己生成。然而对于事件的处理是它的硬伤,所以JSON描述View的方案,一般比较适用于换肤,或者固定事件不同样式的View,比如贴纸。

7.架构模式
其实我们要做到动态部署,至少要满足以下需求:
1.View和事件都要能够动态部署
2.功能完整
3.便于维护

我更加倾向于H5和Native以JSBridge的方式连接的方案进行动态部署,在cocoapods里面也有蛮多的JSBridge了。看了一圈之后,我还是选择写了一个CTJSBridge,来满足动态部署和后续维护的需求。

7.1 为什么不是React-Native或其他方案?
首先针对React-Native来做解释,前面已经分析到,React-Native有一个比较大的局限在于View需要本地提供。假设有一个页面的组件是跑马灯,如果本地没有对应的View,使用React-Native就显得很麻烦。然而同样的情况下,HTML5能够很好地实现这样的需求。这里存在一个这样的取舍在性能和动态部署View及事件之间,选择哪一个?
我更加倾向于能够动态部署View和事件,至少后者是能够完成需求的,性能再好,难以完成需求其实没什么意义。然而对于HTML5的Hybrid和纯HTML5的web app之间,也存在一个相同的取舍,但是还要额外考虑一个新的问题,纯HTML5能够使用到的设备提供的功能相对有限,JSBridge能够将部分设备的功能以Native API的方式交付给页面,因此在考虑这个问题之后,选择HTML5的Hybrid方案就显得理所应当了。
在诸多Hybrid方案中,除了JSBridge之外,其它的方案都显得相对过于沉重,对于动态部署来说,其实需要补充的软肋就是提供本地设备的功能,其它的反而显得较为累赘。

7.2 采用什么样的架构模式才是使用JSBridge的*佳实践?
基于JSBridge的微服务架构差不多是这样的:

解释一下这种架构背后的思想:
因为H5和Native之间能够通过JSBridge进行交互,然而JSBridge的一个特征是,只能H5主动发起调用。所以理所应当地,被调用者为调用者提供服务。
另外一个想要处理的问题是,希望能够通过微服务架构,来把H5和Native各自的问题域区分开。所谓区分问题域就是让H5要解决的问题和Native要解决的问题之间,交集*小。因此,我们设计时希望H5的问题域能够更加偏重业务,然后Native为H5的业务提供基础功能支持,例如API的跨域调用,传感器设备信息以及本地已经沉淀的业务模块都可以作为Native提供的服务交给H5去使用。H5的快速部署特性特别适合做重业务的事情,Native对iPhone的功能调用能力和控制能力特别适合将其封装成服务交给H5调用。
所以这对Native提供的服务有两点要求:
1.Native提供的服务不应当是强业务相关的,*好是跟业务无关,这样才能方便H5进行业务的组装
2.如果Native一定要提供强业务相关的服务,那*好是一个完整业务,这样H5就能比较方便地调用业务模块。
只要Native提供的服务符合上述两个条件,HTML5在实现业务的时候,束缚就会非常少,也非常容易管理。
然后这种方案也会有一定的局限性,就是如果Native没有提供这样的服务,那还是必须得靠发版来解决。等于就是Native向HTML5提供API,这其实跟服务端向Native提供API的道理一样。
但基于Native提供的服务的通用性这点来看,添加服务的需求不会特别频繁,每一个App都有属于自己的业务领域,在同一个业务领域下,其实需要Native提供的服务是有限的。然后结合JSPatch提供的动态patch的能力,这样的架构能够满足*大部分动态部署的需求。
然后随着App的不断迭代,某些HTML5的实现其实是可以逐步沉淀为Native实现的,这在一定程度上,降低了App早期的试错成本。

8 总结
我在文中针对业界常见的动态部署方案做了一些总结,并且提供了我自己认为的*佳解决方案以及对应的JSBridge实现。
另外,关于动态部署方案,其实直到今天在iOS领域也并没有特别好的动态部署方案可以拿出来,我觉得*靠谱的其实还是H5和Native的Hybrid方案。React Native在我看来相比于Hybrid还是有比较多的限制。关于Hybrid方案,我也提供了CTJSBridge这个库去实现这方面的需求。

四、本地持久化方案
持久化方案不管是服务端还是客户端,都是一个非常值得讨论的话题。尤其是在服务端,持久化方案的优劣往往都会在一定程度上影响到产品的性能。然而在客户端,只有为数不多的业务需求会涉及持久化方案,而且在大多数情况下,持久化方案对性能的要求并不是特别苛刻。所以我在移动端这边做持久化方案设计的时候,考虑更多的是方案的可维护和可拓展,然后在此基础上才是性能调优。

持久化方案对整个App架构的影响和网络层方案对整个架构的影响类似,一般都是导致整个项目耦合度高的罪魁祸首。而我也是一如既往的去Model化的实践者,在持久层去Model化的过程中,我引入了Virtual Record的设计,这个在文中也会详细描述。
这里主要讲以下几点:
1.根据需求决定持久化方案
2.持久层与业务层之间的隔离
3.持久层与业务层的交互方式
4.数据迁移方案
5.数据同步方案

1.根据需求决定持久化方案
在有需要持久化需求的时候,我们有非常多的方案可供选择:NSUserDefault、KeyChain、File,以及基于数据库的无数子方案。因此,当有需要持久化的需求的时候,我们首先考虑的是应该采用什么手段去进行持久化。

1.1 NSUserDefault
一般来说,小规模数据,弱业务相关数据,都可以放到NSUserDefault里面,内容比较多的数据,强业务相关的数据就不太适合NSUserDefault了。另外我想吐槽的是,天猫这个App其实是没有一个经过设计的数据持久层的。然后天猫里面的持久化方案就很混乱,我就见到过有些业务线会把大部分业务数据都塞到NSUserDefault里面去,当时看代码的时候我特么就直接跪了。。。
NSUserDefaults数据存储总结

1.2 keychain
Keychain是苹果提供的带有可逆加密的存储机制,普遍用在各种存密码的需求上。另外,由于App卸载只要系统不重装,Keychain中的数据依旧能够得到保留,以及可被iCloud同步的特性,大家都会在这里存储用户唯一标识串。所以有需要加密、需要存iCloud的敏感小数据,一般都会放在Keychain。

1.3 文件存储
文件存储包括了Plist、archive、Stream等方式,一般结构化的数据或者需要方便查询的数据,都会以Plist的方式去持久化。Archive方式适合存储平时不太经常使用但很大量的数据,或者读取之后希望直接对象化的数据,因为Archive会将对象及其对象关系序列化,以至于读取数据的时候需要Decode很花时间,Decode的过程可以是解压,也可以是对象化,这个可以根据具体中的实现来决定。Stream就是一般的文件存储了,一般用来存存图片啊啥的,适用于比较经常使用,然而数据量又不算非常大的那种。

1.4 数据库存储
数据库存储的话,花样就比较多了。苹果自带了一个Core Data,当然业界也有无数替代方案可选,不过真正用在iOS领域的除了Core Data外,就是FMDB比较多了。数据库方案主要是为了便于增删改查,当数据有状态和类别的时候*好还是采用数据库方案比较好,而且尤其是当这些状态和类别都是强业务相关的时候,就更加要采用数据库方案了。因为你不可能通过文件系统遍历文件去甄别你需要获取的属于某个状态或类别的数据,这么做成本就太大了。当然,特别大量的数据也不适合直接存储数据库,比如图片或者文章这样的数据,一般来说,都是数据库存一个文件名,然后这个文件名指向的是某个图片或者文章的文件。如果真的要做全文索引这种需求,建议*好还是挂个API丢到服务端去做。

1.5 总的说一下
NSUserDefault、Keychain、File这些持久化方案都非常简单基础,分清楚什么时候用什么就可以了,不要像天猫那样乱写就好。而且在这之上并不会有更复杂的衍生需求,如果真的要针对它们写文章,无非就是写怎么储存怎么读取,这个大家随便Google一下就有了,我就不浪费笔墨了。由于大多数衍生复杂需求都是通过采用基于数据库的持久化方案去满足,所以这篇文章的重点就数据库相关的架构方案设计和实现。

2.持久层实现时要注意的隔离
在设计持久层架构的时候,我们要关注以下几个方面的隔离:
1.持久层与业务层的隔离
2.数据库读写隔离
3.多线程控制导致的隔离
4.数据表达和数据操作的隔离

2.1 持久层与业务层的隔离
2.1.1关于Model
在具体讲持久层下数据的处理之前,我觉得需要针对这个问题做一个完整的分析。
在View层设计中我分别提到了胖Model和瘦Model的设计思路,而且告诉大家我更加倾向于胖Model的设计思路。在网络层设计里面我使用了去Model化的思路设计了APIMananger与业务层的数据交互。这两个看似矛盾的关于Model的设计思路在我接下来要提出的持久层方案中其实是并不矛盾,而且是相互配合的。在网络层设计这篇文章中,我对去Model化只给出了思路和做法,相关的解释并不多,是因为要解释这个问题涉及面会比较广,写的时候并不认为在那篇文章里做解释是*好的时机。由于持久层在这里胖Model和去Model化都会涉及,所以我觉得在讲持久层的时候解释这个话题会比较好。

2.1.2 Data Model
Data Model这个术语针对的问题领域是业务数据的建模,以及代码中这一数据模型的表征方式。两者相辅相承:因为业务数据的建模方案以及业务本身特点,而*终决定了数据的表征方式。同样操作一批数据,你的数据建模方案基本都是细化业务问题之后,抽象得出一个逻辑上的实体。在实现这个业务时,你可以选择不同的表征方式来表征这个逻辑上的实体,比如字节流(TCP包等),字符串流(JSON、XML等),对象流。对象流又分通用数据对象(NSDictionary等),业务数据对象(HomeCellModel等)。
前面已经遍历了所有的Data Model的形式。在习惯上,当我们讨论Model化时,都是单指对象流中的业务数据对象这一种。然而去Model化就是指:更多地使用通用数据对象去表征数据,业务数据对象不会在设计时被优先考虑的一种设计倾向。这里的通用数据对象可以在某种程度上理解为范型。

2.1.3Model Layer
Model Layer描述的问题领域是如何对数据进行增删改查(CURD, Create Update Read Delete),和相关业务处理。一般来说如果在Model Layer中采用瘦Model的设计思路的话,就差不多到CURD为止了。胖Model还会关心如何为需要数据的上层提供除了增删改查以外的服务,并为他们提供相应的解决方案。例如缓存、数据同步、弱业务处理等。

2.1.4 我的倾向
我更加倾向于去Model化的设计,因为具体的Model是一种很容易引入耦合的做法,在尽可能弱化Model概念的同时,就能够为引入业务和对接业务提供充分的空间。同时,也能通过去Model的设计达到区分强弱业务的目的,这在将来的代码迁移和维护中,是至关重要的。很多设计不好的架构,就在于架构师并没有认识到区分强弱业务的重要性,所以就导致架构腐化的速度很快,越来越难维护。
所以说回来,持久层与业务层之间的隔离,是通过强弱业务的隔离达到的。而Virtual Record正是因为这种去Model化的设计,从而达到了强弱业务的隔离,进而做到持久层与业务层之间既隔离同时又能交互的平衡。

2.2 数据库读写隔离
在网站的架构中,对数据库进行读写分离主要是为了提高响应速度。在iOS应用架构中,对持久层进行读写隔离的设计主要是为了提高代码的可维护性。这也是两个领域要求架构师在设计架构时要求侧重点不同的一个方面。
在这里我们所谓的读写隔离并不是指将数据的读操作和写操作做隔离。而是以某一条界限为准,在这个界限以外的所有数据模型,都是不可写不可修改,或者修改属性的行为不影响数据库中的数据。在这个界限以内的数据是可写可修改的。一般来说我们在设计时划分的这个界限会和持久层与业务层之间的界限保持一致,也就是业务层从持久层拿到数据之后,都不可写不可修改,或业务层针对这一数据模型的写操作、修改操作都对数据库文件中的内容不产生作用。只有持久层中的操作才能够对数据库文件中的内容产生作用。

2.3 多线程导致的隔离
2.3.1 Core Data
Core Data要求在多线程场景下,为异步操作再生成一个NSManagedObjectContext,然后设置它的ConcurrencyType为NSPrivateQueueConcurrencyType,*后把这个Context的parentContext设为Main线程下的Context。这相比于使用原始的SQLite去做多线程要轻松许多。只不过要注意的是,如果要传递NSManagedObject的时候,不能直接传这个对象的指针,要传NSManagedObjectID。这属于多线程环境下对象传递的隔离,在进行架构设计的时候需要注意。

2.3.2 SQLite
纯SQLite其实对于多线程倒是直接支持,SQLite库提供了三种方式:Single Thread,Multi Thread,Serialized。
Single Thread模式不是线程安全的,不提供任何同步机制。Multi Thread模式要求database connection不能在多线程中共享,其他的在使用上就没什么特殊限制了。Serialized模式顾名思义就是由一个串行队列来执行所有的操作,对于使用者来说除了响应速度会慢一些,基本上就没什么限制了。大多数情况下SQLite的默认模式是Serialized。
根据Core Data在多线程场景下的表现,我觉得Core Data在使用SQLite作为数据载体时,使用的应该就是Multi Thread模式。SQLite在Multi Thread模式下使用的是读写锁,而且是针对整个数据库加锁,不是表锁也不是行锁,这一点需要提醒各位架构师注意。如果对响应速度要求很高的话,建议开一个辅助数据库,把一个大的写入任务先写入辅助数据库,然后拆成几个小的写入任务见缝插针地隔一段时间往主数据库中写入一次,写完之后再把辅助数据库删掉。
不过从实际经验上看,本地App的持久化需求的读写操作一般都不会大,只要注意好几个点之后一般都不会影响用户体验。因此相比于Multi Thread模式,Serialized模式我认为是性价比比较高的一种选择,代码容易写容易维护,性能损失不大。为了提高几十毫秒的性能而牺牲代码的维护性,我是觉得划不来的。

2.3.3 Realm
关于Realm我还没来得及仔细研究,所以说不出什么来

2.4 数据表达和数据操作的隔离
这是*容易被忽视的一点,数据表达和数据操作的隔离是否能够做好,直接影响的是整个程序的可拓展性。

长久以来,我们都很习惯Active Record类型的数据操作和表达方式,例如这样:

Record *record = [[Record alloc] init];
record.data = @”data”;
[record save];

或者这种:

Record *record = [[Record alloc] init];
NSArray *result = [record fetchList];

简单说就是,让一个对象映射了一个数据库里的表,然后针对这个对象做操作就等同于针对这个表以及这个对象所表达的数据做操作。这里有一个不好的地方就在于,这个Record既是数据库中数据表的映射,又是这个表中某一条数据的映射。我见过很多框架(不仅限于iOS,包括Python, PHP等)都把这两者混在一起去处理。如果按照这种不恰当的方式来组织数据操作和数据表达,在胖Model的实践下会导致强弱业务难以区分从而造成非常大的困难。使用瘦Model这种实践本身就是我认为有缺点的,具体的我在开篇中已经讲过,这里就不细说了。

强弱业务不能区分带来的*大困难在于代码复用和迁移,因为持久层中的强业务对View层业务的高耦合是无法避免的,然而弱业务相对而言只对下层有耦合关系对上层并不存在耦合关系,当我们做代码迁移或者复用时,往往希望复用的是弱业务而不是强业务,若此时强弱业务分不开,代码复用就无从谈起,迁移时就倍加困难。

另外,数据操作和数据表达混在一起会导致的问题在于:客观情况下,数据在view层业务上的表达方式多种多样,有可能是个View,也有可能是个别的什么对象。如果采用映射数据库表的数据对象去映射数据,那么这种多样性就会被限制,实际编码时每到使用数据的地方,就不得不多一层转换。

我认为之所以会产生这样不好的做法原因在于,对象对数据表的映射和对象对数据表达的映射结果非常相似,尤其是在表达Column时,他们几乎就是一模一样。在这里要做好针对数据表或是针对数据的映射要做的区分的关键要点是:这个映射对象的操作着手点相对数据表而言,是对内还是对外操作。如果是对内操作,那么这个操作范围就仅限于当前数据表,这些操作映射给数据表模型就比较合适。如果是对外操作,执行这些操作时有可能涉及其他的数据表,那么这些操作就不应该映射到数据表对象中。

因此实际操作中,我是以数据表为单位去针对操作进行对象封装,然后再针对数据记录进行对象封装。数据表中的操作都是针对记录的普通增删改查操作,都是弱业务逻辑。数据记录仅仅是数据的表达方式,这些操作*好交付给数据层分管强业务的对象去执行。具体内容我在下文还会继续说。

3.持久层与业务层的交互方式
在交互方案的设计中,架构师应当区分好强弱业务,把传统的Data Model区分成Table和Record,并由DataCenter去实现强业务,Table去实现弱业务。在这里由于DataCenter是强业务相关,所以在实际编码中,业务工程师负责创建DataCenter,并向业务层提供业务友好的方法,然后再在DataCenter中操作Table来完成业务层交付的需求。区分强弱业务,将Table和Record拆分开的好处在于:
1.通过业务细分降低耦合度,使得代码迁移和维护非常方便
2.通过拆解数据处理逻辑和数据表达形态,使得代码具有非常良好的可拓展性
3.做到读写隔离,避免业务层的误操作引入Bug
4.为Virtual Record这一设计思路的实践提供基础,进而实现更灵活,对业务更加友好的架构

任何不区分强弱业务的架构都是架构师在耍流氓,嗯。
在具体与业务层交互时,采用Virtual Record的设计思路来设计Record,由具体的业务对象来实现Virtual Record,并以它作为DataCenter和业务层之间的数据媒介进行交互。而不是使用传统的数据模型来与业务层做交互。

4.数据库版本迁移方案
一般来说,具有持久层的App同时都会附带着有版本迁移的需求。当一个用户安装了旧版本的App,此时更新App之后,若数据库的表结构需要更新,或者数据本身需要批量地进行更新,此时就需要有版本迁移机制来进行这些操作。然而版本迁移机制又要兼顾跨版本的迁移需求,所以基本上大方案也就只有一种:建立数据库版本节点,迁移的时候一个一个跑过去。

数据迁移事实上实现起来还是比较简单的,做好以下几点问题就不大了:
1.根据应用的版本记录每一版数据库的改变,并将这些改变封装成对象
2.记录好当前数据库的版本,便于跟迁移记录做比对
3.在启动数据库时执行迁移操作,如果迁移失败,提供一些降级方案

在版本迁移时要注意的一点是性能问题。我们一般都不会在主线程做版本迁移的事情,这自然不必说。需要强调的是,SQLite本身是一个容错性非常强的数据库引擎,因此差不多在执行每一个SQL的时候,内部都是走的一个Transaction。当某一版的SQL数量特别多的时候,建议在版本迁移的方法里面自己建立一个Transaction,然后把相关的SQL都包起来,这样SQLite执行这些SQL的时候速度就会快一点。

5.数据同步方案
5.1 单向数据同步
单向数据同步就是只把本地较新数据的操作同步到服务器,不会从服务器主动拉取同步操作。
比如即时通讯应用,一个设备在发出消息之后,需要等待服务器的返回去知道这个消息是否发送成功,是否取消成功,是否删除成功。然后数据库中记录的数据就会随着这些操作是否成功而改变状态。但是如果换一台设备继续执行操作,在这个新设备上只会拉取旧的数据,比如聊天记录这种。但对于旧的数据并没有删除或修改的需求,因此新设备也不会问服务器索取数据同步的操作,所以称之为单向数据同步。
单向数据同步一般来说也不需要有job去做定时更新的事情。如果一个操作迟迟没有收到服务器的确认,那么在应用这边就可以认为这个操作失败,然后一般都是在界面上把这些失败的操作展示出来,然后让用户去勾选需要重试的操作,然后再重新发起请求。微信在消息发送失败的时候,就是消息前面有个红色的圈圈,里面有个感叹号,只有用户点击这个感叹号的时候才重新发送消息,背后不会有个job一直一直跑。
所以细化需求之后,我们发现单向数据同步只需要做到能够同步数据的状态即可。

5.1.1 如何完成单向数据同步的需求
添加identifier
添加identifier的目的主要是为了解决客户端数据的主键和服务端数据的主键不一致的问题。由于是单向数据同步,所以数据的生产者只会是当前设备,那么identifier也理所应当由设备生成。当设备发起同步请求的时候,把identifier带上,当服务器完成任务返回数据时,也把这些identifier带上。然后客户端再根据服务端给到的identifier再更新本地数据的状态。identifier一般都会采用UUID字符串。

添加isDirty
isDirty主要是针对数据的插入和修改进行标识。当本地新生成数据或者更新数据之后,收到服务器的确认返回之前,isDirty置为YES。当服务器的确认包返回之后,再根据包里提供的identifier找到这条数据,然后置为NO。这样就完成了数据的同步。
然而这只是简单的场景,有一种比较*端的情况在于,当请求发起到收到请求回复的这短短几秒间,用户又修改了数据。如果按照当前的逻辑,在收到请求回复之后,这个又修改了的数据的isDirty会被置为NO,于是这个新的修改就永远无法同步到服务器了。这种*端情况的简单处理方案就是在发起请求到收到回复期间,界面上不允许用户进行修改。
如果希望做得比较细致,在发送同步请求期间依旧允许用户修改的话,就需要在数据库额外增加一张DirtyList来记录这些操作,这个表里至少要有两个字段:identifier,primaryKey。然后每一次操作都分配一次identifier,那么新的修改操作就有了新的identifier。在进行同步时,根据primaryKey找到原数据表里的那条记录,然后把数据连同identifier交给服务器。然后在服务器的确认包回来之后,就只要拿出identifier再把这条操作记录删掉即可。这个表也可以直接服务于多个表,只是还需要额外添加一个tablename字段,方便发起同步请求的时候能够找得到数据。

添加isDeleted
当有数据同步的需求的时候,删除操作就不能是简单的物理删除了,而只是逻辑删除,所谓逻辑删除就是在数据库里把这条记录的isDeleted记为YES,只有当服务器的确认包返回之后,才会真正把这条记录删除。isDeleted和isDirty的区别在于:收到确认包后,返回的identifier指向的数据如果是isDeleted,那么就要删除这条数据,如果指向的数据只是新插入的数据和更新的数据,那么就只要修改状态就行。插入数据和更新数据在收到数据包之后做的操作是相同的,所以就用isDirty来区分就足够了。总之,这是根据收到确认包之后的操作不同而做的区分。两者都要有,缺一不可。

在请求的数据包中,添加dependencyIdentifier
在我看到的很多其它数据同步方案中,并没有提供dependencyIdentifier,这会导致一个这样的问题:假设有两次数据同步请求一起发出,A先发,B后发。结果反而是B请求先到,A请求后到。如果A请求的一系列同步操作里面包含了插入某个对象的操作,B请求的一系列同步操作里面正好又删除了这个对象,那么由于到达次序的先后问题错乱,就导致这个数据没办法删除。
这个在移动设备的使用场景下是很容易发生的,移动设备本身网络环境就多变,先发的包反而后到,这种情况出现的几率还是比较大的。所以在请求的数据包中,我们要带上上一次请求时一系列identifier的其中一个,就可以了。一般都是选择上次请求里面*后的那一个操作的identifier,这样就能表征上一次请求的操作了。
服务端这边也要记录*近的100个请求包里面的*后一个identifier。之所以是100条纯属只是拍脑袋定的数字,我觉得100条差不多就够了,客户端发请求的时候denpendency应该不会涉及到前面100个包。服务端在收到同步请求包的时候,先看denpendencyIdentifier是否已被记录,如果已经被记录了,那么就执行这个包里面的操作。如果没有被记录,那就先放着再等等,等到条件满足了再执行,这样就能解决这样的问题。
之所以不用更新时间而是identifier来做标识,是因为如果要用时间做标识的话,就是只能以客户端发出数据包时候的时间为准。但有时不同设备的时间不一定完全对得上,多少会差个几秒几毫秒,另外如果同时有两个设备发起同步请求,这两个包的时间就都是一样的了。假设A1, B1是1号设备发送的请求,A2, B2,是2号设备发送的请求,如果用时间去区分,A1到了之后,B2说不定就直接能够执行了,而A1还没到服务器呢。
当然,这也是一种*端情况,用时间的话,服务器就只要记录一个时间了,凡是依赖时间大于这个时间的,就都要再等等,实现起来就比较方便。但是为了保证bug尽可能少,我认为依赖还是以identifier为准,这要比以时间为准更好,而且实现起来其实也并没有增加太多复杂度。

5.1.2 单向数据同步方案总结
改造的时候添加identifier,isDirty,isDeleted字段。如果在请求期间依旧允许对数据做操作,那么就要把identifier和primaryKey再放到一个新的表中
每次生成数据之后对应生成一个identifier,然后只要是针对数据的操作,就修改一次isDirty或isDeleted,然后发起请求带上identifier和操作指令去告知服务器执行相关的操作。如果是复杂的同步方式,那么每一次修改数据时就新生成一次identifier,然后再发起请求带上相关数据告知服务器。
服务器根据请求包的identifier等数据执行操作,操作完毕回复给客户端确认
收到服务器的确认包之后,根据服务器给到的identifier(有的时候也会有tablename,取决于你的具体实现)找到对应的记录,如果是删除操作,直接把数据删除就好。如果是插入和更新操作,就把isDirty置为NO。如果有额外的表记录了更新操作,直接把identifier对应的这个操作记录删掉就行。
要注意的点
在使用表去记录更新操作的时候,短时间之内很有可能针对同一条数据进行多次更新操作。因此在同步之前,*好能够合并这些相同数据的更新操作,可以节约服务器的计算资源。当然如果你服务器强大到不行,那就无所谓了。
5.2 双向数据同步
双向数据同步多见于笔记类、日程类应用。对于一台设备来说,不光自己会往上推数据同步的信息,自己也会问服务器主动索取数据同步的信息,所以称之为双向数据同步。

5.2.1 如何完成双向数据同步的需求
1.封装操作对象
这个其实在单向数据同步时多少也涉及了一点,但是由于单向数据同步的要求并不复杂,只要告诉服务器是什么数据然后要做什么事情就可以了,倒是没必要将这种操作封装。在双向数据同步时,你也得解析数据操作,所以互相之间要约定一个协议,通过封装这个协议,就做到了针对操作对象的封装。
这个协议应当包括:

操作的唯一标识
数据的唯一标识
操作的类型
具体的数据,主要是在Insert和Update的时候会用到
操作的依赖标识
用户执行这项操作时的时间戳
分别解释一下这6项的意义:
1.操作的唯一标识
这个跟单向同步方案时的作用一样,也是在收到服务器的确认包之后,能够使得本地应用找到对应的操作并执行确认处理。
2.数据的唯一标识
在找到具体操作的时候执行确认逻辑的处理时,都会涉及到对象本身的处理,更新也好删除也好,都要在本地数据库有所体现。所以这个标识就是用于找到对应数据的。
3.操作的类型
操作的类型就是Delete,Update,Insert,对应不同的操作类型,对本地数据库执行的操作也会不一样,所以用它来进行标识。
4.具体的数据
当更新的时候有Update或者Insert操作的时候,就需要有具体的数据参与了。这里的数据有的时候不见得是单条的数据内容,有的时候也会是批量的数据。比如把所有10月1日之前的任务都标记为已完成状态。因此这里具体的数据如何表达,也需要定一个协议,什么时候作为单条数据的内容去执行插入或更新操作,什么时候作为批量的更新去操作,这个自己根据实际业务需求去定义就行。
5.操作的依赖标识
跟前面提到的依赖标识一样,是为了防止先发的包后到后发的包先到这种*端情况。
6.用户执行这项操作的时间戳
由于跨设备,又因为旧数据也会被更新,因此在一定程度上就会出现冲突的可能。操作数据在从服务器同步下来之后,会存放在一个新的表中,这个表就是待操作数据表,在具体执行这些操作的同时会跟待同步的数据表中的操作数据做比对。如果是针对同一条数据的操作,且这两个操作存在冲突,那么就以时间戳来决定如何执行。还有一种做法就是直接提交到界面告知用户,让用户做决定。

2.新增待操作数据表和待同步数据表
前面已经部分提到这一点了。从服务器拉下来的同步操作列表,我们存在待执行数据表中,操作完毕之后如果有告知服务器的需求,那就等于是走单向同步方案告知服务器。在执行过程中,这些操作也要跟待同步数据表进行匹配,看有没有冲突,没有冲突就继续执行,有冲突的话要么按照时间戳执行,要么就告知用户让用户做决定。在拉取待执行操作列表的时候,也要把*后一次操作的identifier丢给服务器,这样服务器才能返回相应数据。
待同步数据表的作用其实也跟单向同步方案时候的作用类似,就是防止在发送请求的时候用户有操作,同时也是为解决冲突提供方便。在发起同步请求之前,我们都应该先去查询有没有待执行的列表,当待执行的操作列表同步完成之后,就可以删除里面的记录了,然后再把本地待同步的数据交给服务器。同步完成之后就可以把这些数据删掉了。因此在正常情况下,只有在待操作和待执行的操作间会存在冲突。有些从道理上讲也算是冲突的事情,比如获取待执行的数据比较晚,但其中又和待同步中的操作有冲突,像这种*端情况我们其实也无解,只能由他去,不过这种情况也是属于比较*端的情况,发生几率不大。

5.2.2 何时从服务器拉取待执行列表
1.每次要把本地数据丢到服务器去同步之前,都要拉取一次待执行列表,执行完毕之后再上传本地同步数据
2.每次进入相关页面的时候都更新一次,看有没有新的操作
3.对实时性要求比较高的,要么客户端本地起一个线程做轮询,要么服务器通过长链接将待执行操作推送过来
4.其它我暂时也想不到了,具体还是看需求吧

5.2.3 双向数据同步方案总结
1.设计好同步协议,用于和服务端进行交互,以及指导本地去执行同步下来的操作
2.添加待执行,待同步数据表记录要执行的操作和要同步的操作

5.2.4 要注意的点
我也见过有的方案是直接把SQL丢出去进行同步的,我不建议这么做。*好还是将操作和数据分开,然后细化,否则检测冲突的时候你就得去分析SQL了。要是这种实现中有什么bug,解这种bug的时候就要考虑前后兼容问题,机制重建成本等,因为贪图一时偷懒,到*后其实得不偿失。

6.总结
着重强调了一下各种持久层方案在设计时要考虑的隔离,以及提出了Virtual Record这个设计思路,并对它做了一些解释。然后在数据迁移方案设计时要考虑的一些点。在数据同步方案这一节,分开讲了单向的数据同步方案和双向的数据同步方案的设计,然而具体实现还是要依照具体的业务需求来权衡。