提交AppStore必读

基础 SDK V1.2.0 版本(包括其后版本)引入了 IDFA,可能会造成您的应用提交AppStore审核失败,请您认真阅读下文。

引入IDFA的目的

引入 IDFA 能够较精准的识别用户,能帮助我们最大程度的追踪您在使用 SDK 时的问题,确保SDK稳定性,如果您的应用集成了广告服务,建议使用我们线上版本的 SDK。

若您的应用未集成任何广告服务,但需要跟踪广告带来的激活行为,您仍旧可以使用我们线上版本的 SDK,具体内容请继续向下阅读。

审核时关于 IDFA 选项的选择

提交 AppStore 审核时关于 IDFA 的选项,如下图所示:

image

以上4项代表的含义

1、在 App 内投放广告

服务应用中的广告。如果你的应用中集成了广告的时候,你需要勾选这一项。

2、将此 App 安装归因于先前投放的特定广告

跟踪广告带来的安装。如果你使用了第三方的工具来跟踪广告带来的激活以及一些其他事件,但是应用里并没有展示广告你需要勾选这一项。

3、将此 App 中发生的操作归因于先前投放的特定广告

跟踪广告带来的用户的后续行为。如果你使用了第三方的工具来跟踪广告带来的激活以及一些其他事件。

4、iOS 中的“限制广告跟踪”设置

对您的应用使用 IDFA 的目的做下确认,只要您获取了 IDFA,那么这一项都是需要勾选的。

被 AppStore 拒绝的解决办法

1、如果您的应用里只是集成了广告,不追踪广告带来的激活行为,那么选择 1 和 4。

2、如果您的应用没有广告,而又获取了 IDFA。我们建议选择 2 和 4。

下载集成无 IDFA 的基础 SDK

如果您仍旧不希望使用集成 IDFA 版本的 SDK,您可以下载 无IDFA的版本

或者通过 Cocoapods 引用:

先编辑 Podfile 文件:

  platform :ios, '9.0' #手机的系统
  target '您工程的名字' do
  pod 'AMapLocation-NO-IDFA' #无IDFA版定位 SDK

如果您使用了我们多个SDK,则编辑为:

  target '您工程的名字' do
  pod 'AMap2DMap-NO-IDFA'
  pod 'AMapSearch-NO-IDFA'
  pod 'AMapLocation-NO-IDFA'
  end

保存后,执行命令:

  $pod install

iOS 持续集成系列 – 自动化 Code Review

为了保证代码质量,Code Review 是非常重要的一环。细到*的位置是否正确,大到代码的结构是否符合了软件开发的一些基本原则,都在这项工作的范围内。

受限于现实情况,大多数团队没有足够的时间进行 Code Review,那么只能把一部分 CR 工作交给计算机去完成了。我们只需要定下合理的流程,用代码告诉计算机需要做什么,剩下的就交给我们可靠的伙伴吧。

应用了自动化 Code Review 后,如果你的代码写得不好,Xcode 会表示不开心。

如果你忽略 Xcode 的心情,那么质量管理平台会默默地记录这一切。

这套东西既帮助开发们写出更高质量的的代码,也给经理们对工程质量的评估提供了一个切面的支持,同时只需要花费较少的人力维护,听起来是不是跃跃欲试了呢 : )

流程

整体的工作流程非常简单,如图:

自动化 Code Review 总体流程

关键点在于本地 Review远端 Review这两步。前者是提供给开发者一个即时的代码质量反馈,以便开发者修改,从而避免在接下来的远端 Review 中得到一个较低的得分。后者则是为了生成相关报表,为项目管理人员跟踪项目质量提供依据。在很多大公司里,这也是开发者们绩效的参考之一。

剩下的就是一些胶水步骤了,如何让过程更自动化,就是胶水步骤要做的事。例如利用 WebHook 自动触发远端 Review,利用 Git 的钩子进行增量校验而不是全量校验等。这些我们放在后面聊,先来看看本地校验的流程。

本地 Review

本地自动化 Code Review

在本地 Review 环节,开发者只需要像往常一样按下 CMD + B,然后只要静静地等待进度条读完,满屏的⚠️就会精确地指示出某一行的代码违反了哪条规则。此时开发者就可以根据代码规范进行对应修改。

从按下按键到产生警告主要发生了这么几件事情:

  • 生成 compile_commands.json 文件
  • OCLint 读取相关的 Rules,逐个扫描 compile_commands.json 中的 .m 文件
  • OCLint 将生成的报告展示在 Xcode 上

实现本地 Review 的核心就是 OCLint 和 compile_commands.json文件

OCLint

工欲善其事,必先利其器

OCLint 是一个开源的,基于 Clang 用 C++ 编写而成的,可以用于 C、C++ 和 Objective-C 的静态代码分析器。它可以在扫描的过程中动态加载规则文件(Rules),因此可以实现非常灵活的,高度可自定义的代码分析方案。它几乎可以和大多数系统无缝集成,例如 Cmake、Bear、xcodebuild、xctool、Xcode、xcpretty、Jenkins CI、Travis CI 等。你可以在这里找到如何将其和 Xcode 配合使用。

最新版本的 OCLint 已经自带了 71 条 Rules,基本上都是先人宝贵的经验,比如这条禁用 goto 语句的 Rule,就是来源于 Edsger W. Dijkstra 1968 年的一篇手稿

这 71 条 Rules 已经可以帮助我们避免一部分因书写习惯和语言误区而导致的问题,但是对于有完整编码规范的公司来说显然是不够的。我们必须要自己开发 Rules。

幸运的是,OCLint 已经为我们准备好了一切。

OCLint 提供了 Clang 和 AST (Abstract Syntax Tree) 的一层封装,使我们不必对抽象语法树进行解析,只需要专注规则相关的逻辑开发即可。从其提供的接口中我们可以很明显地看出这一点。

// 遇到一元操作符
bool VisitUnaryOperator(UnaryOperator *node)

// 遇到二元操作符
bool VisitBinaryOperator(BinaryOperator *node)

// 遇到 Objective-C 的函数声明
bool VisitObjCMethodDecl(ObjCMethodDecl *node)

在开发好相关的规则后,打包成 dylib,就可以在分析的时候加载我们自己的 Rule 了

compile_commands.json

compile_commands.json 是 Clang 定义的一个规范,里面存放了一组工作目录目标文件需要被执行的命令,帮助相关工具可以独立于编译系统来将源代码文件转换为 AST 并做对应的事。

看文件内容会更直观一些:

[
{
  "directory": "/path/to/project/",
  "command": "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang -x ...",
  "file": "/path/to/project/XXXViewController.m"
},
...
]

OCLint 可以根据 compile_commands.json 中的内容,批量检查源代码文件。

xcpretty

还有一个点需要关注的是,如何生成 compile_commands.json 文件?

最便捷的方式是使用 oclint-xcodebuild 来生成。首先,利用xcodebuild 生成 xcodebuild.log 文件。

xcodebuild | tee xcodebuild.log

然后利用 oclint-xcodebuild 生成 compile_commands.json

oclint-xcodebuild

截至 Xcode 8.1,这种做法可以正确生成 json 文件。由于 OCLint 团队已经声称不再维护 oclint-xcodebuild , 因此可能在未来的某个 Xcode 版本中这个方法将不再适用。

另一个推荐的方法是利用 xcpretty 。

xcpretty 可以一句话生成 json 文件。

xcodebuild | xcpretty -r json-compilation-database --output /path/to/compile_commands.json

使用本地 Review

了解了这些工具后就很容易明白本地自动化 Code Review 是如何工作的,使用方式也非常容易理解了:

  1. 首先在电脑本地安装好 OCLint 并拿到公司自定义的 Rules 文件
  2. 在 Xcode 上配置好工程
  3. build 工程,等待结果显示在 Xcode 上。

附一个我们团队的配置脚本供参考:

source ~/.bash_profile
cd ${SRCROOT}
xcodebuild clean
xcodebuild | tee xcodebuild.log
oclint-xcodebuild
oclint-json-compilation-database \
-e Vendor \
-e Pods \
-- \
-max-priority-1 100000 \
-max-priority-2 100000 \
-max-priority-3 100000 \
-report-type xcode \
-R /path/to/rules

远端 Review

远端自动化 Code Review

远端 Review 和 本地 Review 大体相似,区别在与引用构建的脚本的对象从 Xcode 变成了 Jenkins CI ,报告的展示者从 Xcode 变成了 SonarQube 。其流程是这样的:

工程师通过 git push 提交代码 → Web Hook 触发 Jenkins 构建 → OCLint 扫描代码生成PMD格式报告 → Sonar-runner 读取报告并展现到 SonarQube

CI 环境

为了实现远端 Review ,服务端必须首先有一套 CI 环境。鉴于 iOS 的特殊性,服务器必须是 macOS 系统。CI 我们直接选择开源的 Jenkins,质量管理平台则选用开源的 SonarQube。Jenkins 大名鼎鼎大家都非常熟悉了,SonarQube 则相对少的人了解。

SonarQube 是一个质量管理平台,在 SonarQube 上,你可以看到一个项目的代码行数、文件数量、代码重复率、违反的代码规范、技术债时间等等指标。SonarQube 对 Java 的支持极度友好,提供了 SonarScanner 可以直接对 Java 源代码进行扫描。Objective-C 就没有这么幸运了。虽然 SonarQube 也提供了 Objective-C 的报告展示的支持,但静态分析还是得依靠 OCLint 。

Sonnar-Runner

我们在 Jenkins 上运行 OCLint 生成了报告。需要一个中间人将报告解析成 SonarQube 可以理解的格式并传输到 SonarQube 平台。这个中间人就是 Sonnar-Runner。Sonnar-Runner 在我们的系统中也仅仅扮演这个搬运工的角色。你可以从这里了解到如何在 Jenkins 上安装和使用 Sonnar-Runner。

Sonnar-Runner 只能解析 PMD 格式的报告,因此我们在使用 OCLint 分析代码后,需要将报告格式输出为 PMD 格式

oclint -report-type pmd -o ./report.xml

Rules in Sonar

SonarQube 有一套规则,将代码问题按照严重程度分为 5 个等级,不同等级的问题会以不同权重影响到项目质量评分。这套规则和 OCLint 生成的报告中的 Rule name 必须要一一对应,SonarQube 才能正确将报告中的问题归类并评分。

如果你使用 OCLint 原生的 Rules 来检查代码,只需要在 SonarQube 上安装 SonarQube Plugin for Objective C 插件,相关的报告就会被正确识别了。

如果是使用了自行开发的 Rules ,只需要 Clone 上述插件,并在profile-oclint.xml 和 rules.txt 中添加相关的 rule name ,然后打包并将这个插件安装到 SonarQube 上即可。

举个例子:

当我们用自行开发的 Rule 检查完代码后,生成了report.xml,内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<pmd version="oclint-0.11">
    <file name="/path/to/TerribleCode.m">
        <violation rule="binary operator space (HT_iOS_Coding_style 2.8)" begincolumn="9" endcolumn="157" beginline="73" endline="73" priority="3" ruleset="HT_iOS_rules" >
            多元运算符和他们的操作数之间至少需要一个空格
        </violation>
    </file>
</pmd>

其中 binary operator space (HT_iOS_Coding_style 2.8) 是我们定义的错误rule name。在 SonarQube 上,也必须对应有这么一条 rule 的 name,才能正确识别这个错误。

此时我们只需要在上述插件的 rules.txt 中添加一段

binary operator space (HT_iOS_Coding_style 2.8)
----------

Summary:多元运算符和他们的操作数之间至少需要一个空格。

Severity: 2
Category: Hengtian iOS Coding Standard

在上述插件的 profile-oclint.xml 中添加另外一段代码

     <rule>
      <repositoryKey>OCLint</repositoryKey>
      <key>binary operator space (HT_iOS_Coding_style 2.8)</key>
    </rule>

然后将这个插件打包并安装到 SonarQube 上,SonarQube 就可以正确识别我们的问题并分类了。

使用远端 Review

在使用前,一定要确保你的 macOS 服务器已经安装好了最新版的 Xocde、OCLint、Jenkins、sonnar-runner,安装好 Jenkins 的相关插件,并将自定义的 Rule 放置在服务器上(如果有的话)。

检查并生成报告

在 Jenkins 上新建工程并配置好Git、构建触发器等其他内容。在构建步骤中添加一步 Execute Shell ,填入下述脚本

cd YourProjectDir
xcodebuild clean
xcodebuild -workspace MyProject.xcworkspace -scheme HTMarket -sdk iphonesimulator | tee xcodebuild.log | xcpretty
oclint-xcodebuild
oclint-json-compilation-database -e Pods \
-v \
-- \
-max-priority-1 100000 \
-max-priority-2 100000 \
-max-priority-3 100000 \
-report-type pmd \
-R /path/to/diy-rules \
-o /path/to/report.xml 

脚本大致和本地 Review 一致,有三个地方需要注意一下。

  1. xcodebuild 命令添加了 -sdk iphonesimulator参数,以避免 build 需要 Code Sign 的问题。
  2. -report-type pmd 输出格式必须为 pmd 格式
  3. -o /path/to/report.xml 注意输出报告的路径,下一步sonnar-runner 读取时会用到。

读取到 SonarQube

在上一步的下方再添加一步 Invoke Standalone SonarQube Analysis,选择好你的 sonnar-runner。并在 Analysis Properties 中添加如下配置:(如果没有这一项,你可能需要安装 SonarQube 相关的插件。)

sonar.projectKey=YOUR_PROJECT_NAME
sonar.projectName=YOUR_PROJECT_NAME
sonar.projectVersion=1.0
sonar.language=objc
sonar.projectDescription=YOUR_PROJECT_DESCRIPTION

# Path to source directories 
sonar.sources=/path/to/source/directories

# Xcode project configuration (.xcodeproj or .xcworkspace)
# -> If you have a project: configure only sonar.objectivec.project
# -> If you have a workspace: configure sonar.objectivec.workspace and sonar.objectivec.project
# and use the later to specify which project(s) to include in the analysis (comma separated list)
sonar.objectivec.project=YOUR_PROJECT_NAME.xcodeproj
sonar.objectivec.workspace= YOUR_PROJECT_NAME.xcworkspace

# Scheme to build your application
sonar.objectivec.appScheme=YOUR_PROJECT_NAME

sonar.sourceEncoding=UTF-8

# OCLint report generated by run-sonar.sh is stored in sonar-reports/oclint.xml
# Change it only if you generate the file on your own
 sonar.objectivec.oclint.report=YOUR_REPORT_FILE_PATH

注意看注释并修改 YOUR_PROJECT_NAME 、YOUR_PROJECT_DESCRIPTION、和 YOUR_REPORT_FILE_PATH为你项目的值。

一切顺利的话,在 Jenkins 上立即构建,你就可以在你的 Sonar 平台上看到代码质量报告了。

配合好构建触发器 和 Git 平台的 WebHook 功能,就可以在开发提交代码或者合并分支等关键点自动触发构建了。

Troubleshooting

为什么生成的 compile_commands.json 为空

检查 log 是否为空,如果 log 为空则代表 build 失败。排除失败原因后即可正常生成。

Jenkins 构建遇到了如下问题

❌  Code signing is required for product type 'Application' in SDK 'iOS 10.0'

遇到这样的情况,是因为构建了 Release 版本,且项目在 Xcode8+ 上开启了 Automatic Code Sign。解决方法如下:

  1. 如果只需要检查代码规范,则在 xcodebuild 命令后添加 -sdk iphonesimulator 参数指明以 Debug 方式构建即可。
  2. 如果希望构建 Release 版本,那么关闭自动签名,在 CI 系统上手动配置证书和Proversion Profile。或者保留自动签名,参考这个回答用 sed 命令在构建前修改相关配置。

一键清除 objc 项目中的无用方法

当项目越来越大,引入第三方库越来越多,上架的APP体积也会越来越大,对于用户来说体验必定是不好的。在清理资源,编译选项优化,清理无用类等完成后,能够做而且效果会比较明显的就只有清理无用函数了。现有一种方案是根据Linkmap文件取到objc的所有类方法和实例方法。再用工具逆向可执行文件里引用到的方法名,求个差集列出无用方法。这个方案有些比较麻烦的地方,因为检索出的无用方法没法确定能够直接删除,还需要挨个检索人工判断是否可以删除,这样每次要清理时都需要这样人工排查一遍是非常耗时耗力的。

这样就只有模拟编译过程对代码进行深入分析才能够找出确定能够删除的方法。具体效果可以先试试看,程序代码在:https://github.com/ming1016/SMCheckProject 选择工程目录后程序就开始检索无用方法然后将其注释掉。

首先遍历目录下所有的文件。

let fileFolderPath = self.selectFolder()

let fileFolderStringPath = fileFolderPath.replacingOccurrences(of: “file://”, with: “”)

let fileManager = FileManager.default;

//深度遍历

let enumeratorAtPath = fileManager.enumerator(atPath: fileFolderStringPath)

//过滤文件后缀

let filterPath = NSArray(array: (enumeratorAtPath?.allObjects)!).pathsMatchingExtensions([“h”,“m”])

然后将注释排除在分析之外,这样做能够有效避免无用的解析。这里可以这样处理。

640

这里/…/这种注释是允许换行的,所以使用.*的方式会有问题,因为.是指非空和换行的字符。那么就需要用到[\s\S]这样的方法来包含所有字符,\s是匹配任意的空白符,\S是匹配任意不是空白符的字符,这样的或组合就能够包含全部字符。

接下来就要开始根据标记符号来进行切割分组了,使用Scanner,具体方式如下

//根据代码文件解析出一个根据标记符切分的数组

class func createOCTokens(conent:String) -> [String] {

    var str = conent

 

    str = self.dislodgeAnnotaion(content: str)

 

    //开始扫描切割

    let scanner = Scanner(string: str)

    var tokens = [String]()

    //Todo:待处理符号,.

    let operaters = [Sb.add,Sb.minus,Sb.rBktL,Sb.rBktR,Sb.asterisk,Sb.colon,Sb.semicolon,Sb.divide,Sb.agBktL,Sb.agBktR,Sb.quotM,Sb.pSign,Sb.braceL,Sb.braceR,Sb.bktL,Sb.bktR,Sb.qM]

    var operatersString = “”

    for op in operaters {

        operatersString = operatersString.appending(op)

    }

 

    var set = CharacterSet()

    set.insert(charactersIn: operatersString)

    set.formUnion(CharacterSet.whitespacesAndNewlines)

 

    while !scanner.isAtEnd {

        for operater in operaters {

            if (scanner.scanString(operater, into: nil)) {

                tokens.append(operater)

            }

        }

 

        var result:NSString?

        result = nil;

        if scanner.scanUpToCharacters(from: set, into: &result) {

            tokens.append(result as! String)

        }

    }

    tokens = tokens.filter {

        $0 != Sb.space

    }

    return tokens;

}

由于objc语法中有行分割解析的,所以还要写个行解析的方法

//根据代码文件解析出一个根据行切分的数组

class func createOCLines(content:String) -> [String] {

    var str = content

    str = self.dislodgeAnnotaion(content: str)

    let strArr = str.components(separatedBy: CharacterSet.newlines)

    return strArr

}

获得这些数据后就可以开始检索定义的方法了。我写了一个类专门用来获得所有定义的方法

class ParsingMethod: NSObject {

    class func parsingWithArray(arr:Array) -> Method {

        var mtd = Method()

        var returnTypeTf = false //是否取得返回类型

        var parsingTf = false //解析中

        var bracketCount = 0 //括弧计数

        var step = 0 //1获取参数名,2获取参数类型,3获取iName

        var types = [String]()

        var methodParam = MethodParam()

        //print(“\(arr)”)

        for var tk in arr {

            tk = tk.replacingOccurrences(of: Sb.newLine, with: “”)

            if (tk == Sb.semicolon || tk == Sb.braceL) && step != 1 {

                mtd.params.append(methodParam)

                mtd.pnameId = mtd.pnameId.appending(“\(methodParam.name):”)

            } else if tk == Sb.rBktL {

                bracketCount += 1

                parsingTf = true

            } else if tk == Sb.rBktR {

                bracketCount -= 1

                if bracketCount == 0 {

                    var typeString = “”

                    for typeTk in types {

                        typeString = typeString.appending(typeTk)

                    }

                    if !returnTypeTf {

                        //完成获取返回

                        mtd.returnType = typeString

                        step = 1

                        returnTypeTf = true

                    } else {

                        if step == 2 {

                            methodParam.type = typeString

                            step = 3

                        }

 

                    }

                    //括弧结束后的重置工作

                    parsingTf = false

                    types = []

                }

            } else if parsingTf {

                types.append(tk)

                //todo:返回block类型会使用.设置值的方式,目前获取用过方法方式没有.这种的解析,暂时作为

                if tk == Sb.upArrow {

                    mtd.returnTypeBlockTf = true

                }

            } else if tk == Sb.colon {

                step = 2

            } else if step == 1 {

                methodParam.name = tk

                step = 0

            } else if step == 3 {

                methodParam.iName = tk

                step = 1

                mtd.params.append(methodParam)

                mtd.pnameId = mtd.pnameId.appending(“\(methodParam.name):”)

                methodParam = MethodParam()

            } else if tk != Sb.minus && tk != Sb.add {

                methodParam.name = tk

            }

 

        }//遍历

 

        return mtd

    }

}

这个方法大概的思路就是根据标记符设置不同的状态,然后将获取的信息放入定义的结构中,这个结构我是按照文件作为主体的,文件中定义那些定义方法的列表,然后定义一个方法的结构体,这个结构体里定义一些方法的信息。具体结构如下

enum FileType {

    case fileH

    case fileM

    case fileSwift

}

 

class File: NSObject {

    public var path = “” {

        didSet {

            if path.hasSuffix(“.h”) {

                type = FileType.fileH

            } else if path.hasSuffix(“.m”) {

                type = FileType.fileM

            } else if path.hasSuffix(“.swift”) {

                type = FileType.fileSwift

            }

            name = (path.components(separatedBy: “/”).last?.components(separatedBy: “.”).first)!

        }

    }

    public var type = FileType.fileH

    public var name = “”

    public var methods = [Method]() //所有方法

 

    func des() {

        print(“文件路径:\(path)\n”)

        print(“文件名:\(name)\n”)

        print(“方法数量:\(methods.count)\n”)

        print(“方法列表:”)

        for aMethod in methods {

            var showStr = “- (\(aMethod.returnType)) “

            showStr = showStr.appending(File.desDefineMethodParams(paramArr: aMethod.params))

            print(“\n\(showStr)”)

            if aMethod.usedMethod.count > 0 {

                print(“用过的方法———-“)

                showStr = “”

                for aUsedMethod in aMethod.usedMethod {

                    showStr = “”

                    showStr = showStr.appending(File.desUsedMethodParams(paramArr: aUsedMethod.params))

                    print(“\(showStr)”)

                }

                print(“——————“)

            }

 

        }

        print(“\n”)

    }

 

    //类方法

    //打印定义方法参数

    class func desDefineMethodParams(paramArr:[MethodParam]) -> String {

        var showStr = “”

        for aParam in paramArr {

            if aParam.type == “” {

                showStr = showStr.appending(“\(aParam.name);”)

            } else {

                showStr = showStr.appending(“\(aParam.name):(\(aParam.type))\(aParam.iName);”)

            }

 

        }

        return showStr

    }

    class func desUsedMethodParams(paramArr:[MethodParam]) -> String {

        var showStr = “”

        for aUParam in paramArr {

            showStr = showStr.appending(“\(aUParam.name):”)

        }

        return showStr

    }

 

}

 

struct Method {

    public var classMethodTf = false //+ or –

    public var returnType = “”

    public var returnTypePointTf = false

    public var returnTypeBlockTf = false

    public var params = [MethodParam]()

    public var usedMethod = [Method]()

    public var filePath = “” //定义方法的文件路径,方便修改文件使用

    public var pnameId = “”  //唯一标识,便于快速比较

}

 

class MethodParam: NSObject {

    public var name = “”

    public var type = “”

    public var typePointTf = false

    public var iName = “”

}

 

class Type: NSObject {

    //todo:更多类型

    public var name = “”

    public var type = 0 //0是值类型 1是指针

}

有了文件里定义的方法,接下来就是需要找出所有使用过的方法,这样才能够通过差集得到没有用过的方法。获取使用过的方法,我使用了一种时间复杂度较优的方法,关键在于对方法中使用方法的情况做了计数的处理,这样能够最大的减少遍历,达到一次遍历获取所有方法。

完整代码在:https://github.com/ming1016/SMCheckProject 这里。基于语法层面的分析是比较有想象的,后面完善这个解析,比如说分析各个文件import的头文件递归来判断哪些类没有使用,通过获取的方法结合获取类里面定义的局部变量和全局变量来分析循环引用,通过获取的类的完整结构还能够将其转成JavaScriptCore能解析的js语法文件。

 

事岀无常必有妖-iOS捉妖记之(Runtime)

Runtime是开源的,任何时候你都可以从http://opensource.apple.com获取。事实上查看 Objective-C 源码是我理解它是如何工作的第一种方式,在某些问题上要比读苹果的文档要好。

引言

相信很多从事iOS开发的小伙伴们都听过这样一句形容runtime的话:

runtime就像是iOS开发中的妖怪,谁都听说过,但少有人见(用)到过!

这句话是某知名培训机构内某老师对学生们说的一句话,相信不少人尤其是初学的萌新们还没了解过runtime,听了这句话就被吓到了!直接在心里给runtime打个一打标签[危险,慎用,底层,难,用不到,不用掌握]。以至于很多人做了有一段时间的iOS开发却依然对其一知半解……

定义

Objective-C 的 Runtime 是一个运行时库(Runtime Library),它是一个主要使用 C 和汇编写的库,为 C 添加了面相对象的能力并创造了 Objective-C。这就是说它在类信息(Class information) 中被加载,完成所有的方法分发,方法转发,等等。Objective-C runtime 创建了所有需要的结构体。

其实就在下个人的理解:runtime就是丫Objective-C 的灵魂!Objective-C之所以叫Objective-C是因为他比C语言不同,是面向对象的。但是Objective-C为什么有面相对象的能力?就是因为有runtime这个鬼东西!

进阶

我们为什么要学习runtime?

  • runtime可以遍历对象的属性
  • runtime可以动态添加/修改属性,动态添加/修改/替换方法,动态添加/修改/替换协议
  • runtime可以动态创建类/对象/协议等等
  • runtime可以方法拦截调用

其实runtime所能做的还不止这些,你甚至可以利用它来把一个Class A的实例对象a在程序中当作Class B的实例对象来用。所以很多iOS开发者把runtime叫做obj-C的黑魔法!

常用方法

先来个最简单最基本的也是几乎所有runtime文必备的例子:

obj-C: [obj func];

runtime:objc_msgSend(obj, @selector(func);

很多初学者除了知道runtime把对象的方法调用转化成消息发送的代码之后就不知道其他的了,但是显然仅仅知道上述的转化并没有什么“吡-”用,我们来看runtime中比较常用(实用)的几种基本用法:

  • 遍历对象的属性

首先定义一个简单的类Person

@interface Person : NSObject

@property (nonatomic, copy) NSString *name;

@property (nonatomic, assign) NSInteger age;

@end

然后在需要遍历对象的属性时

id personClass = objc_getClass(“Person”);

unsigned int outCount;

objc_property_t *properties = class_copyPropertyList(personClass, &outCount);

for (int i = 0; i < outCount; ++i) {

objc_property_t property = properties[i];

printf(“%s:%s\n”, property_getName(property), property_getAttributes(property));

}

free(properties);

这时就会打印出这个类对象的属性相关信息:

name:T@”NSString”,C,N,V_name

age:Tq,N,V_age

  • 消息转发

[消息转发]指的就是我上面提到的动态方法解析,重定向以及消息转发,我们先来看一张图:

动态方法解析:

从上图可以知道,当对一个实例对象obj发送一条消息func时[obj func],当前obj如果没有对func实现对应的方法,那么就runtime会调用+ (BOOL)resolveInstanceMethod:(SEL)sel方法允许开发者对当前受到的消息func做出响应,这就是动态方法解析。

继续拿上面的Person举例子,给Person类加一个体重weight属性

@property (nonatomic, assign) NSInteger weight;

然后在.m文件中加入一下代码

@implementation Person

@dynamic weight;  //避免自动生成getter/setter方法

//重写resolveInstanceMethod方法,动态方法解析

+ (BOOL)resolveInstanceMethod:(SEL)sel

{

if (sel == @selector(setWeight:)) {

class_addMethod([self class], sel, (IMP)setPropertyDynamic, “v@:”);

return YES;

}

return [super resolveInstanceMethod:sel];

}

//用来响应setWeight的c语言方法

void setPropertyDynamic(id self, SEL _cmd) {

NSLog(@”Dynamic setWeight”);

}

@end

然后可以在代码就调用Person的setWeight方法

Person *lision = [[Person alloc] init];

lision.weight = 75;

这时候如果不重写+ (BOOL)resolveInstanceMethod:(SEL)sel方法本应该异常的,但是你可以发现程序会打印出信息:

Dynamic setWeight

重定向:

那么还是看图说话,如果没有重写+ (BOOL)resolveInstanceMethod:(SEL)sel方法,那就就会调用- (id)forwardingTargetForSelector:(SEL)aSelector方法,把这个消息让另一个对象来处理,这次叫做重定向。

跟着上面的例子走,先另一个类People用来等待重定向:

@interface People : NSObject

@end

给新写的People类加一个weight方法,但是注意:People没有weight属性!

– (NSInteger)weight

{

return 70;

}

接下来我们重写- (id)forwardingTargetForSelector:(SEL)aSelector方法:

– (id)forwardingTargetForSelector:(SEL)aSelector

{

if (aSelector == @selector(weight)) {

People *people = [[People alloc] init];

return people;

}

return [super forwardingTargetForSelector:aSelector];

}

然后我们在刚才的执行代码中:

NSLog(@”weight = %ld”, lision.weight);

然后运行,经历过上面的例子你肯定知道不会异常啦,而且你会发现虽然你给weight属性赋值明明是75,可是打印结果是:weight = 70。这就是Person类- (id)forwardingTargetForSelector:(SEL)aSelector方法中把这条信息抛给了people对象,调用了People类的weight方法!

消息转发:

那么如果上面的两个方法都没有重写,并且消息依然是当前对象没有实现的方法,runtime才会启用消息转发调用– (void)forwardInvocation:(NSInvocation *)anInvocation,需要注意的是很多文章没有提到这个方法花费代价较大,如果要实现把消息转发类似的功能建议最好使用重定向,而且再调用这个方法前runtime会先调用- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector方法。

我们跟着上面的例子,继续给Person类加入属性:

@property (nonatomic, copy) NSString *ID;

以及上面提到的两个方法:

– (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

{

if (aSelector == @selector(setID:)) {

NSMethodSignature *sig = [NSMethodSignature signatureWithObjCTypes:”v@:”];

//”v@:”代表的意思参见Objective-C Type Encodings,这里的意思是返回值为空

return sig;

}

return nil;

}

– (void)forwardInvocation:(NSInvocation *)anInvocation

{

People *people = [[People alloc] init];

if ([people respondsToSelector:anInvocation.selector]) {

[anInvocation invokeWithTarget:people];

}

}

别忘记了在People类中添加对应的方法:

– (void)setID:(NSString *)ID

{

NSLog(@”People setID: %@”, ID);

}

最后,我门只需要在执行代码块中加入代码:

lision.ID = @”xxxx”;

结果显而易见,相信各位都知道将会打印信息:

People setID: xxxx

写在最后

其实runtime就是我们无时无刻不在用的东西,只是人们习惯对看不到的东西怀有恐惧心理而已。我们平时的obj-C代码都是被runtime转译为c和汇编语言运行的。我个人认为大公司为什么喜欢在面试时问runtime相关的东西是因为大公司往往不仅仅要会干活的人,它还会要求这些会干活的人知道其中的原理!我们自己也应该要求自己或多或少的理解这些原理,知道我们为什么写出的obj-C代码经历了哪些过程run到我们的设备上,不要敲了很多年的代码还是一只只会干活的码农。

最牛B的编码套路

最近,我大量阅读了Steve Yegge的文章。其中有一篇叫“Practicing Programming”(练习编程),写成于2005年,读后令我惊讶不已:

与你所相信的恰恰相反,单纯地每天埋头于工作并不能算是真正意义上的锻炼——参加会议并不能锻炼你的人际交往能力;回复邮件并不能提高你的打字水平。你必须定期留出时间,集中锻炼,这样才能把事情做得更好。

我认识很多杰出的程序员——这是在亚马逊工作最好的额外“福利”之一。如果仔细观察他们,你会发现他们时时都在锻炼。他们已经很优秀了,但他们仍然不忘锻炼。他们锻炼的方法林林总总,而我在这篇文章中只会介绍其中的几种。

据我了解,这些杰出程序员之所以如此成功,就是因为他们一直在锻炼。完美的身材要靠定期的锻炼才能获得,而且必须坚持锻炼才能保持,否则身材就会走形。对于编程和软件工程来说,道理是一样的。

这是一个重要的区别——我每天都开车去上班,但我的驾驶水平远远不如专业车手;类似的情况,天天编程可能并不足以使你成为一名专业的程序员。那么,什么才能把一个普通人变成一名专业车手或者专业程序员呢?你需要锻炼什么呢?

答案就在《科学美国人》的一篇名为“The Expert Mind”(专家思维)的文章里:

爱立信提出,重要的并不是经验本身,而是“努力的学习”,也就是要不断地挑战自身能力之外的东西。一些狂热的爱好者花费了大量的时间去下棋、打高尔夫球或者玩乐器,但他们可能始终停留在业余水平上,而一个训练有素的学生却可以在相对较短的时间里超越他们,原因就在这里。值得注意的是,在提高水平方面,花费在下棋上的大量时间(即使参加各种比赛)似乎还是比不过专门的训练来得更为有效。训练的主要价值在于发现弱点,并有针对性地进行提高。

“努力的学习”意味着,要常常去处理那些刚好在你能力极限上的问题,也就是那些对你来说有很大可能失败的事情。如果不经历一些失败的话,你可能就不会成长。你必须不断地挑战自我,超越自己的极限。

那样的挑战有时会在工作中碰到,但也未必。将锻炼从职业工作中分离出来,这在编程领域常被人称为“编码套路”(Code Kata)。

Code Kata的概念是由David Thomas提出的,他是《程序员修炼之道:从小工到专家》的作者之一。这个概念主要指的是,针对某一种特定技术或技能进行重复性的练习,从而将其熟练掌握。——译者注

所谓套路,就是一系列的招式。这个概念借鉴于武术。

如果你想要看一些编码套路的例子(也就是努力学习和磨练编程技能的方法),SteveYegge的文章里倒是提出了一些不错的建议。他把它们称作为“实践演练”:

1. 写一份自己的简历。把自己所有的相关技能都罗列出来,然后把那些在100年后还用得到的标出来。给每个技能打分,满分为10分。

2. 罗列出你所景仰的程序员。尽量包括那些与你一起工作的人,因为你会在工作中从他们身上获取一些技能。记录下他们身上的1 ~ 2个闪光点,也就是你希望自己有所提高的方面。

3. 查看维基百科上的“计算机科学”栏目,找到“计算机领域先驱者”这个分类,从这个列表中挑选一个人,阅读他的事迹,并且在阅读时打开任何你感兴趣的链接。

4. 花20分钟通读别人的代码。读出色的代码和读糟糕的代码都是有益的,两者都要读,轮流切换。如果你无法感觉出它们之间的区别,可以求助于一位你尊敬的程序员,让他给你展示一下什么是出色的代码、什么是糟糕的代码。把你读过的代码给别人也看看,问问他们的看法。

5. 罗列出你最喜欢的10个编程工具——那些你觉得你用得最多、非有不行的工具。随机挑选其中的一个工具,花一个小时去阅读它的文档。在这一个小时里,努力去学习这个工具的某个你不曾意识到的新功能,或者发现某种新的使用方法。

6. 想一想,除了编程之外你最擅长什么事情?再想一想,你是通过怎样的锻炼才变得如此熟练和专业的?这对于你的编程工作又有什么启发呢?(怎么把这些经验应用到编程方面?)

7. 拿出一叠简历,并和一组面试官在同一个房间里待上一个小时。确保每份简历都至少被3个面试官看过,并且要给出1 ~ 3分的评分。针对那些不同面试官评判大相径庭的简历展开讨论。

8. 参与一个电话面试。事后写下你的反馈,抛出你的观点,然后与主持电话面试的人聊一聊,看看你们是否达成了一致的结论。

9. 进行一次技术面试,并且被面试的人应该是某个你不太了解的领域里的专家。让他假定听众在该领域里一无所知,因此请他从最基础的讲起。努力去理解他所说的,必要时问一些问题。

10. 有机会参与别人的技术面试。期间,你只是认真地听、认真地学。在应聘者努力解决技术问题的同时,你也要在自己脑子里尝试解决这些问题。

11. 找到一个能和你交换实际问题的人,每隔一周,相互交流编程问题。花10 ~ 15分钟来尝试解决这些问题,再用10 ~ 15分钟进行讨论(无论能否解决)。

12. 当你听到任何你一时之间也无法解决的面试问题时,赶紧回到你的座位上,把这个问题用电子邮件发给自己,以留作日后的提醒。在那一周里找出点时间,用自己最喜欢的编程语言来解决它。

我之所以喜欢Steve开出的这个清单,是因为它看上去很全面。有些程序员一想到“锻炼”,总认为就是一些编码上的难题。但在我看来,编程更在于人,而不是代码。因此,通过解决世上所有的、并且晦涩的编程面试题目,在提高你的个人能力方面,这种方法是有局限的。

关于“努力的学习”,我也很喜欢Peter Norvig在《Teach Yourself Programming in TenYears | 花10年时间自学编程》一文中提出的诸多建议:(给主页君发送  自学,可查看全文)

1. 与别的程序员交流。读别人的代码。这比任何书籍或培训课程都更重要。

2. 动手写程序!最好的学习方法就是边做边学。

3. 在本科或研究生的课程中学习编程课程。

4. 找一些项目来做,并且需要与其他程序员形成团队来合作。在项目的进行过程中,学会辨别最出色的程序员以及最糟糕的程序员。

5. 在项目中跟随别的程序员一起工作,了解如何维护那些不是你写的代码,并且学习如何写出利于他人维护的代码。

6. 学习多种不同的编程语言,特别是那些与你现在所熟悉的语言有着不同的世界观和编程模型的。

7. 了解硬件对软件的影响。知道你的电脑执行一条指令需要多少时间,从内存中取出一个字(在有缓存或没缓存的情况下)需要多少时间,在以太网(或者因特网)上传输数据需要多少时间,从磁盘中读取连续的数据或者在磁盘上跳转到另一个位置需要多少时间,等等。

你还可以从Dave Thomas的21种实用的编码套路中获取灵感(CodeKata.com),或者你更愿意加入一个你家当地的“编程武馆”(CodingDojo.org)。

对于“努力的学习”,我无法像Steve,Peter或者Dave那样提供一个长长的建议列表。我远不如他们有耐心。实际上,在我看来,“编程套路”只需两个招式:

1. 写博客。我在2004年初创办了CodingHorror.com博客,作为我自己努力学习的一种形式。它在一开始很不起眼,到后来成为我职业生涯中做过的最重要的一件事。所以,你也应该写博客。最后“闻达于天下”的人,往往就是那些能够有效书写和沟通的人。他们的声音最响亮,是他们在制定游戏规则,并且引领世界的潮流。

2. 积极参与著名的开源项目。所有的高谈阔论听起来都很好,但是,你是一个大话王还是一名实干家呢?别光说不练,这个非常重要,因为人们会用你的行动来衡量你,而不是你的言论。努力在公众面前留下些实实在在有用的东西吧,到时候你就可以说,“我在那个项目中出过力。”

当你能编写精彩的代码、并且能用精彩的言辞向世人解释那些代码时,到那时候,我会觉得你已经掌握了最牛的编码套路!

来自:呦呦鹿鸣 – CSDN博客

译者:呦呦鹿鸣(@豆巴陆其明)

链接:blog.csdn.net/happydeer/article/details/17023229(点击尾部阅读原文前往)

原文:https://sites.google.com/site/steveyegge2/practicing-programming

 

iOS应用之间的跳转,看这篇就够了

来源:夜_阑珊

链接:http://www.jianshu.com/p/6b746f95b568



一、前言

泰国渡了半个月的假,回来发现有段时间没更新博客了,顿时感到浑身焦虑啊,而这段时间也不断有小伙伴关注我的简书账号,让我感到欣慰的同时心理也是沉甸甸的压力,唯有写出高质量、好的博文作为回报了。昨天项目经理问我:能不能从我们的app应用跳转到合作商的app应用?我回答说:完全可以啊,这个不是问题。他听完后非常满意高兴的去合作商进一步商谈了。留下身后的我赶紧谷歌相关资料(开玩笑,哥也不可能什么都去记,很早以前做过也忘的七七八八了),但是谷歌一通下来发现网上五花八门的博文并没有几篇讲的清晰明了、深入浅出的,而且大部分年代久远。于是花了点时间专门研究了下,经过实践和总结,本篇博文将涉及到以下知识点:

  • app应用跳转的原理解析

  • 如何实现两个app应用之间的跳转

  • 如何实现两个app之间跳转到指定界面

二、应用跳转原理

相信从一个应用跳转到另一个应用大家并不陌生,最常见的莫过于第三方登录,支付宝支付等等。这些东西大家都耳熟能详,集成进来也很简单,跟着第三方sdk集成文档一步步走下来就是了,通常sdk集成文档都需要你在工程中配置一堆堆的东西,但是配置的这些东西,你真的明白了吗?比如下面这个,第三方登录或分享需要你配置的URL Schemes:

第三方登录或分享需要你配置

不明白呢没关系,开始我也不明白,但是这篇博文看完后,相信你会明白的,下面正式进入主题:

1、一些概念的补充

  • 协议:双方互相遵守的一种规范,只有遵守共同的协议规范才能进行彼此的通信。比如我们最熟悉的网络协议——http协议。

  • URL:资源的路径或地址。在IOS中有一个专门用于包装资源路径的类——NSURL。

  • 一个完整URL的组成

例如:http://123.0.0.1/path?page=100

“http://”:协议类型

“123.0.0.1”:服务器ip地址

“/path”:资源存放的是路径

“page=100”:请求的参数

  • NSURL包装一个完整地址

NSURL *url = [NSURL URLWithString:@“http://123.0.0.1/path?page=100”];

  NSLog(@“scheme(协议):%@”,url.scheme);

  NSLog(@“host(域名):%@”,url.host);

  NSLog(@“path(路径):%@”,url.path);

  NSLog(@“query(参数):%@”,url.query);

打印结果如下:

2016-12-02 14:50:38.442 TestDemo[5632:406869] scheme(协议):http

2016-12-02 14:50:38.442 TestDemo[5632:406869] host(域名):123.0.0.1

2016-12-02 14:50:38.442 TestDemo[5632:406869] path(路径):/path

2016-12-02 14:50:38.442 TestDemo[5632:406869] query(参数):page=100

2、跳转的原理

在iOS中,从一个app打开另一个app,这必然牵扯到两个app之间的交互和通信,像这种涉及到整个应用程序层面的事情,苹果有一个专门的类来管理——UIApplication。在ios中UIApplication其实就是代表着应用程序,这点从它的命名就可以窥之。而我们要打开另一个应用程序,如何实现呢?

很简单,其实就是UIApplication下面这个 的API

/**

通过应用程序打开一个资源路径

@param url 资源路径的地址

@return 返回成功失败的信息

*/

(BOOL)openURL:(NSURL*)url;

它的一些我们非常熟悉的用法:

//拨打系统电话

NSURL *url = [NSURL URLWithString:@“tel://10086”];

[[UIApplication sharedApplication] openURL:url];

//发送系统短信

NSURL *url = [NSURL URLWithString:@“sms://1383838438”];

[[UIApplication sharedApplication] openURL:url];

看到这里也许有人会有疑问:拨打系统电话、发送系统短信跟我本篇要讲的应用间的跳转有什么关系呢?

呵呵,不要着急,重点来了:你难道不觉得拨打系统电话、发送系统短信其实就是应用间的跳转吗?只要一执行以上两个方法就会从你当前的应用跳转到系统的拨打电话界面、发送短信界面,这难道还不够应用间的跳转吗?其实你也可以这么理解:拨打系统电话、发送短信它俩就是手机本身自带的两个app应用。

写到这里答案已经呼之欲出,上面打电话和发短信的实现代码大同小异,唯一的区别是传递的NSURL参数不一样,导致他们跳转到不同的应用场景。我们再仔细分析下传给它们的NSURL参数,就会发现NSURL的scheme(协议)不一样,打电话时“tel://”协议,发短信是“sms://”协议。(对协议有疑问的童鞋可以拉上去看)

一个总结:一个应用能打开另一个应用的必然条件是,另一个应用必须配置一个scheme(协议),这样应用程序才能根据协议找到需要打开的应用。

三、实现两个app间的跳转

创建两个示例Demo,TestDemo和Test2Demo,现在需要实现从Test2Demo跳转到TestDemo中

1、在被跳转的TestDemo配置一个协议scheme,这里命名为test(名字可随意配置,当然最好是英文并且跟你项目相关)

targets -> info -> URL Types ->URL Scheme ->填写协议

配置协议

注意:不需要填写成“test://”

2、在Test2Demo执行跳转的方法中实现下面方法

(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event

{

    NSURL *url = [NSURL URLWithString:@“test://”];

 

    if ([[UIApplication sharedApplication] canOpenURL:url]) {

 

        [[UIApplication sharedApplication] openURL:url];

 

    }else{

        NSLog(@“没有安装应用”);

    }

}

ok,到这里如果你的系统是ios9.0以下,已经大大功告成了。但是,如果是9.0以后,请看下一步。

3、配置协议白名单

在Test2Demo的info.plist文件中增加一个LSApplicationQueriesSchemes字段,把它设置为数组类型,并配置需要跳转的协议名单

配置协议白名单

到此,两个应用间的跳转已经完全实现,其实说穿了就三步,so easy!但是,很多时候,我不仅要跳转到一个应用上,而且还需要跳转到应用的指定界面,想知道怎么处理请接着往下看。

四、跳转到指定界面

想要跳转到指定界面,必然是上一个app告诉下一个app(被跳转的app)需要跳转到哪个界面,而如何告诉它这里便涉及到两个app的通信。我们从上面可以知道,两个app之间的跳转只需要配置一个scheme,然后通过UIApplication调用它的对象方法openURL:即可实现,除此之外再也没有实现任何代码了。而这之间是如何通信的呢?

答案依然是协议,请看下面步骤:

1、在”test://”协议后面的域名加上一些字段用来标记需要跳转的界面

//进入更多界面

(IBAction)intoMore:(id)sender {

    NSURL *url = [NSURL URLWithString:@“test://more”];

 

    if ([[UIApplication sharedApplication] canOpenURL:url]) {

 

        [[UIApplication sharedApplication] openURL:url];

    }else{

        NSLog(@“没有安装应用”);

    }

 

}

 

//进入设置界面

(IBAction)intoSet:(id)sender {

 

    NSURL *url = [NSURL URLWithString:@“test://set”];

 

    if ([[UIApplication sharedApplication] canOpenURL:url]) {

 

        [[UIApplication sharedApplication] openURL:url];

    }else{

        NSLog(@“没有安装应用”);

    }

 

}

2、来到被跳转的应用TestDemo的AppDelegate类的.m文件中,监听其代理方法application:handleOpenURL:

//当应用程序将要被其他程序打开时,会先执行此方法,并传递url过来

//注:下面这个方法9.0后就过期了,请注意适配,9.0后用这个方法:application:openURL:options:

(BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url

{

    NSLog(@“url:%@”,url.absoluteString);

    NSLog(@“host:%@”,url.host);

    if ([url.host isEqualToString:@“more”]) {

        NSLog(@“进入更多界面”);

        //到此做界面的跳转

    }

 

    if ([url.host isEqualToString:@“set”]) {

        NSLog(@“进入设置界面”);

        //到此做界面的跳转

    }

 

    return YES;

}

当Test2Demo点击进入更多界面打印如下:

2016-12-02 17:11:17.680 TestDemo[6507:495044] url:test://more

2016-12-02 17:11:17.681 TestDemo[6507:495044] host:more

2016-12-02 17:11:17.681 TestDemo[6507:495044] 进入更多界面

当Test2Demo点击进入设置界面打印如下:

2016-12-02 17:10:38.745 TestDemo[6507:495044] url:test://set

2016-12-02 17:10:38.745 TestDemo[6507:495044] host:set

2016-12-02 17:10:38.745 TestDemo[6507:495044] 进入设置界面

五、结束语

到此本篇博文要讲的基本结束,由于自身水平所限,如有疏漏之处望海涵和斧正。写技术博客真的不易,需要消耗大量的时间和精力,本人会持续更新一些自认为有用的技术点。

视频播放器随页面滚动缩放


//

//  ODVideoDetailController.m

//  oudaBuyer

//

//  Created by lixiang on 2016/11/22.

//  Copyright © 2016 ouda. All rights reserved.

//

#import “ODVideoDetailController.h”

#import “LEOHeaderView.h”

#import “ODShopSpecialSingleListCell.h”

@interface ODVideoDetailController ()<UITableViewDelegateUITableViewDataSource>

@property (nonatomicstrongUITableView *tableView;

@property (nonatomicstrongLEOHeaderView *headerView;

@property (nonatomicassignCGFloat smallWH;//播放器最小的时候的宽度 高度

@end

@implementation ODVideoDetailController

– (void)viewWillAppear:(BOOL)animated {

    [super viewWillAppear:animated];

    self.navigationController.navigationBar.hidden = YES;

    [self.headerView reloadWithScrollView:self.tableView];

}

– (void)viewDidLoad {

    [super viewDidLoad];

    

    if (kWidthOfScreen == 320) {

        self.smallWH = 100;

    }else if (kWidthOfScreen == 375){

        self.smallWH = 146;

    }else if (kWidthOfScreen == 414){

        self.smallWH = 174;

    }

    

    self.view.backgroundColor = [UIColor whiteColor];

    self.automaticallyAdjustsScrollViewInsets = NO;

    

    self.tableView = [[UITableView allocinitWithFrame:CGRectMake(0self.smallWHkWidthOfScreenkHeightOfScreen – self.smallWH)];

    self.tableView.dataSource = self;

    self.tableView.delegate = self;

    self.tableView.showsVerticalScrollIndicator = NO;

    self.tableView.contentInset = UIEdgeInsetsMake(kWidthOfScreen – self.smallWH000);

    self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;

    self.tableView.backgroundColor = [UIColor clearColor];

    [self.view addSubview:self.tableView];

    

    self.headerView = [[LEOHeaderView allocinitWithFrame:CGRectMake(00kWidthOfScreenkWidthOfScreenandVideoUrl:[NSURL URLWithString:self.model.videoInfo.pathandWH:self.smallWH];

    self.headerView.tableView = self.tableView;

    [self.view addSubview:self.headerView];

}

– (void)scrollViewDidScroll:(UIScrollView *)scrollView {

    [self.headerView reloadWithScrollView:scrollView];

}

// 有可能成为最终滚动的位置

– (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {

    if (scrollView.contentOffset.y > 0) {

        [self.headerView confirmSmall];

    }

    if (scrollView.contentOffset.y == –kWidthOfScreen+self.smallWH){

        [self.headerView confirmBig];

    }

}

– (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {

    return 2+self.model.goodsList.count;

}

– (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    if (indexPath.row == 0) {

        ODSpecialVideoDetailCell *cell = [ODSpecialVideoDetailCell cellWithTableView:tableView];

        cell.model = self.model;

        return cell;

    }

    

    if (indexPath.row == 1) {

        UITableViewCell *cell = [UITableViewCell new];

        cell.textLabel.text = @”    视频内商品信息;

        cell.textLabel.textColor = ODBlackColor;

        cell.textLabel.font = [UIFont systemFontOfSize:14 weight:UIFontWeightMedium];

        cell.selectionStyle = UITableViewCellSelectionStyleNone;

        UIView *line1 = [[UIView alloc]initWithFrame:CGRectMake(1514.7590.5)];

        line1.backgroundColor = ODFontGrayColor;

        [cell.contentView insertSubview:line1 belowSubview:cell.textLabel];

        

        UIView *line2 = [[UIView alloc]initWithFrame:CGRectMake(13514.75kWidthOfScreen1600.5)];

        line2.backgroundColor = ODFontGrayColor;

        [cell.contentView addSubview:line2];

        

        return cell;

    }

    

    ODShopSpecialSingleListCell *cell = [ODShopSpecialSingleListCell cellWithTableView:tableView];

    cell.model2 = self.model.goodsList[indexPath.row2];

    return cell;

}

– (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {

    if (indexPath.row == 0) {

        return 115+[self.model.videoInfo.desc getLabelHeightWithFont:[UIFont systemFontOfSize:14 weight:UIFontWeightLightwithLineSpacing:6 maxSize:CGSizeMake(kWidthOfScreen30MAXFLOAT)];

    }

    if (indexPath.row == 1) {

        return 30;

    }

    return 120;

}

@end

#import “LEOHeaderView.h”

@interface LEOHeaderView ()<VIMVideoPlayerViewDelegate>

@property (nonatomicassignCGFloat smallWH;//播放器最小的时候的宽度 高度

@end

@implementation LEOHeaderView

– (instancetype)initWithFrame:(CGRect)frame andVideoUrl:(NSURL *)url andWH:(CGFloat)wh{

    if (self = [super initWithFrame:frame]) {

        self.smallWH = wh;

        self.playerView = [[VIMVideoPlayerView alloc]init];

        self.player = [[VIMVideoPlayer alloc]init];

        [self.playerView setPlayer:self.player];

        [self.playerView.player setURL:url];

        self.playerView.backgroundColor = [UIColor blackColor];

        self.playerView.size = CGSizeMake(kWidthOfScreenkWidthOfScreen);

        self.playerView.center = CGPointMake(kWidthOfScreen/2kWidthOfScreen/2);

        [self addSubview:self.playerView];

        self.playerView.delegate = self;

        [self.player play];

    }

    return self;

}

– (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {

    UIView *hitView = [super hitTest:point withEvent:event];

    CGPoint cPoint = [self convertPoint:point toView:self.playerView];

    if ([self.playerView.layer containsPoint:cPoint]) {

        return hitView;

    }else{

        return self.tableView;

    }

}

– (void)reloadWithScrollView:(UIScrollView *)scrollView {

    CGFloat offsetY = scrollView.contentOffset.y + kWidthOfScreen – self.smallWH;

    if (offsetY < 0) {

        

    else if (offsetY > 0 && offsetY <= kWidthOfScreen – self.smallWH) {

        self.top = -offsetY / 2;

        CGFloat kEachPixel = (1 – self.smallWH / kWidthOfScreen) / (kWidthOfScreen – self.smallWH);

        CGFloat kScale = 1 – offsetY * kEachPixel;

        self.playerView.frame = CGRectMake(0, offsetY / 2kWidthOfScreenkWidthOfScreen*kScale);

    }

}

– (void)confirmSmall {

    [UIView animateWithDuration:0.3 animations:^{

        self.top = –self.smallWH;

        self.playerView.frame = CGRectMake(0self.smallWHkWidthOfScreenself.smallWH);

    }];

}

– (void)confirmBig {

    [UIView animateWithDuration:0.3 animations:^{

        self.top = 0;

        self.playerView.frame = CGRectMake(00kWidthOfScreenkWidthOfScreen);

    }];

}

– (void)videoPlayerViewDidReachEnd:(VIMVideoPlayerView *)videoPlayerView {

    [self.player.player seekToTime:kCMTimeZero];

}

@end

二维码扫描动画


#import “ViewController.h”

@interface ViewController ()

@property (nonatomicstrongNSTimer *timer;

@property (nonatomicstrongUIView *line;

@property (nonatomicassignint num;

@property (nonatomicassignBOOL goUp;//从下往上

@end

@implementation ViewController

– (void)viewDidLoad {

    [super viewDidLoad];

    UIView *boxView = [[UIView alloc]initWithFrame:CGRectMake((self.view.bounds.size.width200)/2100200200)];

    boxView.layer.borderColor = [UIColor redColor].CGColor;

    boxView.layer.borderWidth = 0.5;

    [self.view addSubview:boxView];

    

    self.line = [[UIView alloc]initWithFrame:CGRectMake(002001)];

    self.line.backgroundColor = [UIColor redColor];

    [boxView addSubview:self.line];

    

    self.timer = [NSTimer scheduledTimerWithTimeInterval:.01 target:self selector:@selector(scanAnimation) userInfo:nil repeats:YES];

}

– (void)scanAnimation {

    if (self.goUp == NO) {

        self.num ++;

        self.line.frame = CGRectMake(0_num2001);

        if ((2*_num == 200 )) {

            self.goUp = YES;

        }

    else {

        self.num –;

        self.line.frame = CGRectMake(0200_num2001);

        if (self.num == 0) {

            self.goUp = NO;

        }

    }

}

史上最强审核团队起底,妈妈再也不用担心我的App提审被拒了!( 沙铭)

前言

苹果说到审核团队,很多人就冒火,他们拒绝应用的理由花式上百,让人抓狂却无可奈何!他们神秘低调,除了申诉渠道,不和外界做任何的沟通。

他们是群什么样的人呢?又是如何评判App的生死?该怎么跟他们沟通才好?

不理解他们,就无法真正理解App Store是如何运转的。

沙铭并不认识审核团队的人,但这并不妨碍我们从大量的实例和线索中去探究套路,尤其对于iOS开发者来说,花点时间去研究他们不仅必要,也是非常有价值的。

前美国最高法院大法官斯图尔特在回答该如何定义yin*h影片时说:I know it when I see it,成为一句流行的名言,这句话被苹果引用在Guideline中,确实是再恰当不过了!

客观来讲,如果法律都不能对所有行为做明确定义或穷举的话,苹果也没有这个能力,特别是涉黄时,而苹果恰恰是一个在这方面执法特别严厉的“法官”。据苹果离职员工爆料,审核团队大量的时间花在剔除yin*h内容上,尤其是图片,任何形式的擦边球都是不被允许的,哪怕在其它方面妥协,苹果似乎在这点上毫不让步。

另外,此员工还爆料,审核团队的人员配置远远比外界想象的少的多,且都集中在一个地方办公。苹果不愿意降低招聘标准,坚信只有高素质人才才能做这份工作。也许是签署了保密协议,他拒绝透露具体的人数。

听起来不是那么妙是吧,我们再来看看他们的工作量。

Q:审核团队到底有多少人?

对此苹果守口如瓶,在公开场合仅仅回应过一次。那还是在2009年中,Google Voice遭App Store封杀,FCC(美国联邦通信委员会)介入调查并质询审核流程,苹果在2009年7月31日的回复函中提到:

答复非常具有外交辞令,每周的案子有8500个。审核团队有40人,但只有2人会仔细审查每一个App,其他人难道是打酱油的?从当时的语境以及实际情况来看,审核团队应当远远小于40人。

因为在Quora问答社区上,也有人打听这个事情。

有个人回答说他朋友的朋友的同事的亲戚(晕了没)是审核团队的,听说到2013年11月有6个人。

当然,这些信息都很模糊,但却都暗示同一个方向:审核团队的人数比外界猜测的要少的多的多!

Q:每天要审多少案子?

从ASO100了解到,目前全球通过审核的更新数量大概在每月15万左右,这并不包括新发布的App。

加上新发布的App,数量应当是16-20万之间,注意这是指已经通过审核的案例,再考虑到拒审的,实际数量比这个更多。

也就是说,每周的案子至少在5万以上,就算是周末加一天班,每天的案子数量也在8000以上,相信很多人都领教过审核的细致,特别是新App,他们简直就是测试人员。请问,这么大量的工作,苹果需要多少员工来处理?

即使是上百人的团队(我相信远远少于这个数字),也处理不了这么多工作。所以唯一合理的解释是:苹果已经大量的引入了自动化AI,所以人工只要处理其中一小部分,但即使这样,每个成员的工作量也是超负荷的。

Q:中国区审核是不是在中国?有多少人?

可以肯定的是,中国区审核也在美国。有些小伙伴说收到过北京的电话,那是因为苹果中国的员工代为传话,审核人员是禁止跟开发者直接电话沟通的。

好奇心之下,沙铭去苹果官网招聘频道去查看了下,还真发现了一些线索。

首先,查找“review”(审核)仅仅出来两个职位

下面是审核团队招人的页面,关键信息点有:

1、办公地点在Santa Clara Valley(距离Cupertino苹果总部不到20分钟的车程)

2、正常5*8工作制

3、工作量巨大,但是质量需要保证

4、整个审核团队仅仅招收中文审核,而且只要一个(同样印证了人数比外界想象的少得多)

5、广告贴出时间是15年6月,要求本科学历(印证了苹果不愿意降低门槛扩招团队)

也许有的人会质疑:苹果招到人之后就把广告撤下来了呗!

OK,那我们就到招聘网站,社交网络上去找找线索,历史招聘总应该留下一丝痕迹吧。不过任凭我怎么搜索,就只有这么一个职位,不信各位可以自己试试。

无论再如何争辩,一个事实是在发稿时,苹果整个审核团队只招收一个人,所以沙铭根本不相信这是一个很大的团队。

至于“Web应用工程师”是什么职位,我们来看看,关键信息如下:

1、和审核团队一起办公,时长也一样

2、职责是搭建和支持审核团队使用的系统(印证了自动化系统的存在)

3、仅仅招收一名懂Ruby和Java的攻城狮

基于以上信息,沙铭做两个大胆的猜测:

一、正常的提审需要先排队进入自动化系统审核,然后再人工审核。而加速审核则是省去了自动化系统排队流程。

二、5月份审核的速度加快不是因为团队扩招,而是自动化系统一次大的升级所致。

前面提到审核团队需要花大量的时间去筛查涉黄和血腥暴力内容,很容易导致心理上的不适,以至于定时要去接受心理治疗。所以有理由相信:自动化系统的一大要务就是智能过滤这些内容,才有可能最大程度上解放生产力。

Q :八卦:收入如何?

有很多网站提供硅谷的收入报告,我截取了从indeed.com的一份最新平均年薪报告,而红框那一部分就是审核团队的年薪区间。

而苹果开出的薪水要高一些,但仍然达不到硅谷其它职位的平均水平,也不及苹果软件工程师的水平($121,930)。信息来源:Glassdoor,取自苹果员工的薪水调查报告。

OK,相信到这里,大家对这个神秘团队有了更多的了解,原来的羡慕和痛恨也许转变成了同情。让我们一起想象一下身为一个审核人员的感受:

  • 科技含量不高,重复的工作内容极度枯燥。
  • 工作压力巨大,任务堆积如山而且永远不可能完成。
  • 不属于强势部门,甚至被其它部门瞧不起。
  • 饱受外界的指责,但无处宣泄。
  • 整日审查那些恶心的内容,让人抑郁。
  • 薪水不高,硅谷生活压力很大。

然而,沙铭并不是他们的代言人来博眼泪的,真正的价值在于:把他们当成客户,时刻记住那边是一个个有感情的人,节省他们的时间就是帮自己,拒审率就一定会大大降低。

理解他们,就不会出现这种抱怨(来自知乎):

忘记提供测试账号……(审核的大爷们你们就不能自己弄个或者注册个账号么)

如何提高过审率?

苹果官方公布的Top10被拒理由很有参考价值,每一种情况都给出了总占比情况:

  • 18% 缺少信息。
  • 11% 条款2.2:有bug。
  • 6% 条款22.2:使用欺骗、误导性的内容,尤其是名字和icon与其它App相仿。
  • 6% 条款10.6:UI太差,达不到苹果的审美。
  • 5% 条款3.4:iTunes Connect里的App名称和设备上显示的差别太大。
  • 4% 条款3.3:App的名称,描述,截屏或视频跟应用本身不相关。
  • 4% 条款17.2:要求用户输入个人隐私信息。
  • 4% 条款2.1:App崩溃。
  • 3% 条款2.16:多线程App的后台服务使用场景超出规定范围。
  • 3% 条款3.1:App名称或关键词含有其它产品名称的。

大家可以对照自己的情况,看看中了哪一条,然后针对性的改善,因为篇幅已经很长了,沙铭就不一一详述了,仅拿其中第一条来解释下,因为缺少信息占到18%,也非常值得说。

知乎上有个小伙伴就做的很好,是一个典型的把审核当成顾客来服务的例子:

所有在提交前,都会录制一个使用的详细视频放到YouTube中,把所有的功能挨着测试,所有可能的场景都考虑到。 每次In Review都是秒过。他们估计压力也大,一天看这么多,你要他们自己慢慢摸索一个App,不如帮他们做这件事,只要他们打开看看和视频中的一样,就直接通过了。

如果制作视频不方便,另外还有个窍门:想一想应用中有没有可能让用户迷惑的地方,如果想不出来,找一个对App不熟悉的朋友帮你测试下,然后一定要使用审核信息栏右边的“备注”,如下图一样,做一些傻瓜式的指引和解释,4000字符限制,一般够用了。

事实证明,审核人员会很仔细的看备注区域,借这个机会展示你的用心,感动他们,节省了他自己摸索的时间,他就会也方便你,他好你好大家好!

据称,如果按照这些方法做,过审率可以提高60%以上哦!大家快转变思维吧。

此外,沙铭想花点篇幅讲讲另外一个容易被大家忽视的问题,因为常常看到论坛里和群里有小伙伴在问。

通常情况下,长标题和堆砌关键词很容易被拒,但是也许大家没有意识到的是:即使通过了,你的关键词也不是100%都有排名。

这就涉及到苹果内部审核的另外一个机制:关键词屏蔽!即审核人员可以删除或屏蔽他认为不合适的关键词,同时通过应用且并不告知开发者。

注意里面提到两种情况:

● 删除,审核人员在关键词域里直接删除了关键词,如上面那位遇到的情况。

屏蔽,虽然没有删除关键词,但是屏蔽了,导致在该关键词下没有排名。

接下来,为了规避这个陷阱,我会告诉你一些技巧,以及出现这种情况后应该如何应对。

首先,设定关键词的时候不要过分堆砌和自己应用无关的词。当然,你也可以选择碰运气,但最好想办法说服审核人员你的应用和该关键词相关。

有的小伙伴可能会问了,我又没办法跟审核人员对上话,怎么说服啊?没错,你是对不上话,但产品就是你的传声筒啊,描述/截屏都是可以利用的地方,觉得有必要就好好加工一下吧,尤其是描述,会告诉审核人员这是一款什么样的应用,在里面提及关键词,说不定什么时候苹果就开始检索描述了呢。

不过,一定不要走向堆砌关键词的极端,记住:目前描述没有权重,是给人看的,包括审核和用户,如果仅仅因为描述导致被拒,耽误了上线时间就得不偿失了!

其次,当审核通过后,第一件事马上去查看一下自己提交的关键词是不是都有排名了!

两种方式:

1. 通过ASO工具查询,快但是不那么精准;

2. 手工查询,慢但是精准。

推荐两种方式结合使用,先通过ASO工具查询,没有排名的关键词再通过手工查询,不过如果遇到一些结果上千的热词怎么办,翻到手抽筋都出不来啊!这里有个小窍门,把它和你的开发者账号名称组合起来查,因为开发者账号名称的权重和关键词域是相当的,算法会自动将其与其它关键词进行组合。

比方说开发者账号是“oopsing”,想查的词是“天气”,你就输入“oopsing 天气“,这样搜索的结果就大大减少了,如果发现哪个词搜不到你的应用,恭喜,你中彩了!

最后,中彩之后怎么办?

可以选择到苹果申诉渠道哭诉你的应用和该关键词是多么相关,往下翻到App Review Board(如下图),选择“ask about something else”,用英文填写申诉内容,然后就等复审结果吧。

或者,认栽先放弃这个关键词吧,下次更新时换个其它的词试试,别浪费空间。

结语

现状远非完美,但去责备审核团队显得有些不近人情,毕竟他们某种程度上也是受害者,有苦说不出。

如果你能换个角度,加上好的沟通技巧,说不定善变的双子就会变成温柔的双鱼。——这就是苹果    by 沙铭

我对成功学的一点看法

我最早是从高中时期接触成功学这个概念的。

当时比较流行陈安之的视频演讲,我当时看得挺兴奋的,觉得自己发现了什么不得了的东西,于是跟很多朋友推荐。

直到现在,很多陈安之的经典语录,我还耳熟能详。

我还记得有一次,我在朝阳的晨读早会上,问大家一个问题,

“你们知道成功最重要的因素是什么吗?”

然后我告诉大家,“成功最重要的,是要有强烈的企图心”。

现在想想,那时的自己真是挺稚嫩的。

为什么零几年的时候,成功学能够大行其道,受到那么多人的追捧。

而最近几年网络上却对成功学大肆批判,微信朋友圈再也不流行发鸡汤文了呢?

我想这跟中国社会经济的大环境是密切相关的。

陈安之的成功,不是他自己真的多么精通所谓的成功学学术本身,

而是他的创业迎合了当时社会上培训市场的需要,

迎合了当时人们经济水平达到一定程度之后,自尊型人格开始觉醒的需要。

而如今,自尊型人格早已普遍形成,我们需要的是更高层次的自我实现型人格。

从一定程度上说,陈安之的成功学是有其积极意义的。因为他的确激励了很多人,满足了很多人。

但问题是,这种成功学存在着两个巨大的缺陷:

第一,他把成功单纯地定义为财富和地位,却忽视了成功应有的其他内涵,这是这种成功学最大的病态。

第二,他认为成功是可以复制的,这是这种成功学最大的谎言。

先说第一点,

为了成功不择手段,甚至忽略了道德,这是所有成功中最不可取的。

中国的食品安全问题那么严重,传销那么猖獗,都与这种心态密切相关。

当然自私是人的天性,这让我联想到《哈佛公开课:公平与正义》中对于功利主义思想的讨论,

不管你追求的是个人利益的最大化,还是大多数人利益的最大化,你都是以单纯的利益为导向思考问题。

于是忽略了人性中对于更高层次的价值需求。世界上有太多的东西,是无法通过数字去衡量其价值的。

说到这里,我突然想到我的一个朋友,也是你们很多人都熟知的赖军焕大哥。

如果没有赖大哥,也就没有我们朝阳读书团,这一点是毋庸置疑的。

他是我创办社团的精神依靠,也是朝阳一路磕磕绊绊中坚持前行的坚实支柱。

但说起我和赖大哥的相识相知,却要从一次传销会议谈起。

2011年,太平洋直购在南昌搞得如火如荼,我和赖大哥都“有幸”参加了一次他们所谓的的“招商会”。

然后又经朋友介绍坐在了一起,当时会议现场的场面热情高涨,参会人数也很多,

大人,老人,小孩都有。虽然我当时并不清楚这家公司是做传销的,

但我本能地有些反感台上主持人各种空洞浮夸的演讲和说辞。于是没有听完就离开了。

但不管怎样,这让我有幸认识了赖大哥,于是也就有了我们后来一起创办朝阳的事情。

赖大哥和我很像,热爱分享,热爱演说,但不同的是,他的热爱充斥着他每天的生活。

他的热情和激情总能感染着我,让我有勇气尝试以前不敢尝试的事情。

但今天提到赖大哥,我想说的,不是他的热情,而是他在热情追求成功的同时,从来不肯舍弃的仗义和善良。

不管他是否在太平洋直购工作过,也不管他跟我因为观点不同争吵过多少次。

他始终是在任何时候,愿意帮助我指导我的赖大哥,始终是我钦佩和感激的赖大哥。

回到主题,继续说第二点:

成功是可以复制的吗?

网上有个搞笑的段子。有人问:成功是可以复制的吗?有人回答说,是的,成功是可以复制的,但不能粘贴。

其实,从理性上讲,不管你说可以复制,还是说不可以复制,我相信都有你的道理。

但为什么我还要反对“成功可以复制”这个观点呢?

因为所有的“成功学大师”,他们之所以说成功是可以复制的,不是因为他们多么懂得成功的规律,

而是因为他们要给那些渴望迅速成功的人更多的希望,从而花更多的钱来买自己的课程。

我从来不相信迅速成功,不是因为我真的不相信,而是因为我享受脚踏实地,一步一步走向成功的平静和喜悦。

罗永浩曾经写过一篇文章,他说“我和所有人一样,都是懒惰的、容易放弃的、坚持不下去的,没有毅力的。

所有人都这样,你不必自卑,因为我们从基因上就是被设计成这样的。”

他写自己年轻时的经历,总是喜欢买很多很多成功学的书,来激励自己,

虽然他也知道一本书,通常只能激励他两三天罢了。

但他还是愿意买,因为他觉得,有时候哪怕仅仅是激励自己两三天呢,也是值得的。

正如有些人说的“成功学都是骗人的, 却是最好的精神鸦片”。

如果单从激励人的角度出发,我们在缺乏动力的时候,读一读成功学的书,又未尝不可。

不过,最后我还是想说一句,成功学可以学,但首先你要懂得有所为有所不为。

鸡汤也可以喝,但首先你要有喝汤的勺子,不要在鸡汤的香气中迷失,还误以为自己已经喝到了肚子里。