一、 iOS 端常見被拒原因匯總
- App 內(nèi)包含分發(fā)下載分發(fā)功能(引導(dǎo)用戶下載 App 等功能)。
- 提供的測(cè)試賬號(hào)無(wú)法查看實(shí)際功能
- 通過(guò)接口返回布爾值判斷 App 是否升級(jí),但審核期間該接口不請(qǐng)求
- 審核賬號(hào),任何時(shí)候在任何 ip 登錄看到的都是審核版。
- 提供的登陸賬號(hào)和密碼不對(duì),登陸不上
- 運(yùn)營(yíng)填寫的營(yíng)銷關(guān)鍵字有問(wèn)題
- 元數(shù)據(jù)問(wèn)題,iPhoneX 截圖中 iPhone 殼子是 iPhone7 的,應(yīng)該是 iPhoneX
- 說(shuō)明隱私權(quán)限的作用。
- 營(yíng)銷文字,某些能力需要資質(zhì)。此類功能在審核期間都關(guān)閉
- 修改隱私權(quán)限相關(guān)的文案,做到讓審核人員看得懂,做到「信達(dá)雅」
- App 無(wú)法登陸進(jìn)去,屬于 bug 級(jí)別
- App 沒(méi)有適配 ipad。
- Privacy - Data Collection and Storage,說(shuō)明 App 沒(méi)有做隱私權(quán)限的收集。
- 訪問(wèn) h5 頁(yè)面出現(xiàn)問(wèn)題。 屬于 bug 級(jí)別
- App 集成了設(shè)備指紋 SDK, 會(huì)上傳用戶設(shè)備安裝應(yīng)用列表。 解決:移除設(shè)備指紋SDK, 成功上架
二、 App 被拒原因匯總
從 Android 和 iOS 2端 App 被駁回的一些信息來(lái)看,駁回原因一般劃分為下面幾類:
審核期間,資源和配置都應(yīng)該調(diào)節(jié)為審核模式
App 包含某些關(guān)鍵字
審核相關(guān)的元數(shù)據(jù)問(wèn)題(截圖與實(shí)際內(nèi)容不匹配、機(jī)型和截圖不匹配、提供給審核的賬號(hào)和密碼登陸不上)
使用的隱私權(quán)限必須說(shuō)明,文案描述必須清晰
App 存在 bug (賬號(hào)無(wú)法登陸、沒(méi)有適配 ipad、訪問(wèn) h5 打不開 )
誘導(dǎo)用戶打開查看更多 App
Android 應(yīng)用未加固
-
應(yīng)用缺乏相關(guān)的資質(zhì)和證書
如想快速有效的提高iOS技術(shù)和能力,不防瞧瞧這一份iOS秘籍
iOS提升武功秘籍
三、 方案
常見審核失敗的原因很多,很大比重一個(gè)就是代碼或者文本里面存在一些敏感詞,所以本文的側(cè)重點(diǎn)在于關(guān)鍵詞掃描。像上架設(shè)置的截圖和當(dāng)前設(shè)備不匹配、提供的賬號(hào)無(wú)法使用功能 ?? 這種情況打一頓就好了,非主流行為不在本文范圍內(nèi)
3.1 詞云誰(shuí)去收集?
每個(gè)公司一般來(lái)說(shuō)都不止一條業(yè)務(wù)線,所以每個(gè)業(yè)務(wù)線的 App 情況和內(nèi)容也不一樣,所以敏感詞也是千差萬(wàn)別。敏感詞收集這個(gè)事情,應(yīng)該由業(yè)務(wù)線主要負(fù)責(zé) App 的開發(fā)者來(lái)收集,根據(jù)平時(shí)的上架情況,蘋果的駁回的郵件來(lái)整理。
3.2 方案設(shè)計(jì)
公司自研工具 cli(iOS SDK、iOS App、Android SDK、Android App、RN、Node、React 依賴分析、構(gòu)建、打包、測(cè)試、熱修復(fù)、埋點(diǎn)、構(gòu)建),各個(gè)端都是通過(guò)「模版」來(lái)提供能力。包含若干子項(xiàng)目,每個(gè)子項(xiàng)目就是所謂的 “模版”,每個(gè)模版其實(shí)就是一個(gè) Node 工程,一個(gè) npm 模塊,主要負(fù)責(zé)以下功能:特定項(xiàng)目類型的目錄結(jié)構(gòu)、自定義命令供開發(fā)、構(gòu)建等使用、模版持續(xù)更新及 patch 等。
所以可以在打包構(gòu)建(各個(gè)端將項(xiàng)目提交到打包系統(tǒng),打包系統(tǒng)根據(jù)項(xiàng)目語(yǔ)言、平臺(tái)調(diào)度打包機(jī))的時(shí)候,拿到源代碼進(jìn)行掃描?;谶@個(gè)現(xiàn)狀,所以方案是「掃描是基于源代碼出發(fā)的掃描的」。
按照 iOS 端 pod install 這個(gè)過(guò)程,cocoapods 為我們預(yù)留了鉤子:PreInstallHook.rb、PostInstallHook.rb,允許我們?cè)诓煌碾A段為工程做一些自定義的操作,所以我們的 iOS 模版設(shè)計(jì)也參考了這個(gè)思想,在打包構(gòu)建前、構(gòu)建中、構(gòu)建后提供了鉤子:prebuild、build、postbuild。定位好了問(wèn)題,要做的就是在 prebuild 里面進(jìn)行關(guān)鍵詞掃描的編碼工作。
確定了什么時(shí)候做什么事情,接下來(lái)就要討論怎么做才合適。
3.3 技術(shù)方案選擇
字符串匹配算法 KMP 是一開始想到的內(nèi)容,針對(duì)某個(gè) App 進(jìn)行時(shí)機(jī)測(cè)試,發(fā)現(xiàn)50多個(gè)敏感詞的情況下,代碼掃描耗時(shí)60秒鐘,覺(jué)得非常不理想,看 KMP 算法沒(méi)有啥問(wèn)題,所以換個(gè)思路走下去。
因?yàn)槟0姹举|(zhì)上 Node 項(xiàng)目,所以 Node 下的 glob 模塊正好提供根據(jù)正則匹配到合適的文件,也可以匹配文件里面的字符串。然后繼續(xù)做實(shí)驗(yàn),數(shù)據(jù)如下:9個(gè)銘感詞語(yǔ)、代碼文件5967個(gè),耗時(shí)3.5秒
3.4 完整方案
- 業(yè)務(wù)線需要自定義敏感詞云(因?yàn)槊織l業(yè)務(wù)線的關(guān)鍵詞云都不一樣)
- 敏感詞需要?jiǎng)澐值燃?jí):error、warning。掃描到 error 需要馬上停止構(gòu)建,并提示「已掃描到你的源碼中存在敏感詞,可能存在提交審核失敗的可能,請(qǐng)修改后再次構(gòu)建」。warning 的情況不需要馬上停止構(gòu)建,等任務(wù)全部結(jié)束后匯總給出提示「已掃描到你的源碼中存在敏感詞、***...,可能存在提交審核失敗的可能,請(qǐng)開發(fā)者自己確認(rèn)」
- 銘感詞云的格式
scaner.yml文件。
- error: 數(shù)組的格式。后面寫需要掃描的關(guān)鍵詞,且等級(jí)為 error,表示掃描到 error 則馬上停止構(gòu)建
- warning:數(shù)組的格式。后面寫需要掃描的關(guān)鍵詞,且等級(jí)為 warning,掃描結(jié)果不影響構(gòu)建,最終只是展示出來(lái)
- searchPath:字符串格式??梢宰寴I(yè)務(wù)線自定義需要進(jìn)行掃描的路徑。
- fileType:數(shù)組格式??梢宰寴I(yè)務(wù)線自定義需要掃描的文件類型。默認(rèn)為
sh|pch|json|xcconfig|mm|cpp|h|m - warningkeywordsScan:布爾值。業(yè)務(wù)線可以設(shè)置是否需要掃描 warning 級(jí)別的關(guān)鍵詞。
- errorKeywordsScan:布爾值。業(yè)務(wù)線可以設(shè)置是否需要掃描 error 級(jí)別的關(guān)鍵詞。
error:
- checkSwitch
warning:
- loan
- online
- ischeck
searchPath:
../fixtures
fileType:
- h
- m
- cpp
- mm
- js
warningkeywordsScan: true
errorKeywordsScan: true
- iOS 端存在私有 api 的情況,Android 端不存在該問(wèn)題 私有 api 70111個(gè)文件,每個(gè)文件假設(shè)10個(gè)方法,則共70萬(wàn)個(gè) api。所以計(jì)劃找出 top 100.去掃描匹配,支持業(yè)務(wù)線是否開啟的選項(xiàng)
其實(shí)這些問(wèn)題都是業(yè)界標(biāo)準(zhǔn)的做法,肯定需要預(yù)留這樣的能力,所以自定義規(guī)則的格式可以查看上面 yml 文件的各個(gè)字段所確定。明確了做什么事,以及做事情的標(biāo)準(zhǔn),那就可以很快的開展并落地實(shí)現(xiàn)。
'use strict'
const { Error, logger } = require('@company/BFF-utils')
const fs = require('fs-extra')
const glob = require('glob')
const YAML = require('yamljs')
module.exports = class PreBuildCommand {
constructor(ctx) {
this.ctx = ctx
this.projectPath = ''
this.fileNum = 0
this.isExist = false
this.errorFiles = []
this.warningFiles = []
this.keywordsObject = {}
this.errorReg = null
this.warningReg = null
this.warningkeywordsScan = false
this.errorKeywordsScan = false
this.scanFileTypes = ''
}
async fetchCodeFiles(dirPath, fileType = 'sh|pch|json|xcconfig|mm|cpp|h|m') {
return new Promise((resolve, reject) => {
glob(`**/*.?(${fileType})`, { root: dirPath, cwd: dirPath, realpath: true }, (err, files) => {
if (err) reject(err)
resolve(files)
})
})
}
async scanConfigurationReader(keywordsPath) {
return new Promise((resolve, reject) => {
fs.readFile(keywordsPath, 'UTF-8', (err, data) => {
if (!err) {
let keywords = YAML.parse(data)
resolve(keywords)
} else {
reject(err)
}
})
})
}
async run() {
const { argv } = this.ctx
const buildParam = {
scheme: argv.opts.scheme,
cert: argv.opts.cert,
env: argv.opts.env
}
// 處理包關(guān)鍵詞掃描(敏感詞匯 + 私有 api)
this.keywordsObject = (await this.scanConfigurationReader(this.ctx.cwd + '/.scaner.yml')) || {}
this.warningkeywordsScan = this.keywordsObject.warningkeywordsScan || false
this.errorKeywordsScan = this.keywordsObject.errorKeywordsScan || false
if (Array.isArray(this.keywordsObject.fileType)) {
this.scanFileTypes = this.keywordsObject.fileType.join('|')
}
if (Array.isArray(this.keywordsObject.error)) {
this.errorReg = this.keywordsObject.error.join('|')
}
if (Array.isArray(this.keywordsObject.warning)) {
this.warningReg = this.keywordsObject.warning.join('|')
}
// 從指定目錄下獲取所有文件
this.projectPath = this.keywordsObject ? this.keywordsObject.searchPath : this.ctx.cwd
const files = await this.fetchCodeFiles(this.projectPath, this.scanFileTypes)
if (this.errorReg && this.errorKeywordsScan) {
await Promise.all(
files.map(async file => {
try {
const content = await fs.readFile(file, 'utf-8')
const result = await content.match(new RegExp(`(${this.errorReg})`, 'g'))
if (result) {
if (result.length > 0) {
this.isExist = true
this.fileNum++
this.errorFiles.push(
`編號(hào): ${this.fileNum}, 所在文件: ${file}, 出現(xiàn)次數(shù): ${result &&
(result.length || 0)}`
)
}
}
} catch (error) {
throw error
}
})
)
}
if (this.errorFiles.length > 0) {
throw new Error(
`從你的項(xiàng)目中掃描到了 error 級(jí)別的敏感詞,建議你修改方法名稱、屬性名、方法注釋、文檔描述。\n敏感詞有 「${
this.errorReg
}」\n存在問(wèn)題的文件有 ${JSON.stringify(this.errorFiles, null, 2)}`
)
}
// warning
if (this.warningReg && !this.isExist && this.fileNum === 0 && this.warningkeywordsScan) {
await Promise.all(
files.map(async file => {
try {
const content = await fs.readFile(file, 'utf-8')
const result = await content.match(new RegExp(`(${this.warningReg})`, 'g'))
if (result) {
if (result.length > 0) {
this.isExist = true
this.fileNum++
this.warningFiles.push(
`編號(hào): ${this.fileNum}, 所在文件: ${file}, 出現(xiàn)次數(shù): ${result &&
(result.length || 0)}`
)
}
}
} catch (error) {
throw error
}
})
)
if (this.warningFiles.length > 0) {
logger.info(
`從你的項(xiàng)目中掃描到了 warning 級(jí)別的敏感詞,建議你修改方法名稱、屬性名、方法注釋、文檔描述。\n敏感詞有 「${
this.warningReg
}」。有問(wèn)題的文件有${JSON.stringify(this.warningFiles, null, 2)}`
)
}
}
for (const key in buildParam) {
if (!buildParam[key]) {
throw new Error(`build: ${key} 參數(shù)缺失`)
}
}
}
}