77.9K 的 Axios 项目有哪些值得借鉴的地方

Axios 是一个基于 Promise 的 HTTP 客户端,同时支持浏览器和 Node.js 环境。它是一个优秀的 HTTP 客户端,被广泛地应用在大量的 Web 项目中。


由上图可知,Axios 项目的 Star 数为 「77.9K」,Fork 数也高达 「7.3K」,是一个很优秀的开源项目,所以接下来阿宝哥将带大家一起来分析 Axios 项目中一些值得借鉴的地方。

阅读完本文,你将了解以下内容:

  • HTTP 拦截器的设计与实现;
  • HTTP 适配器的设计与实现;
  • 如何防御 CSRF 攻击。

下面我们从简单的开始,先来了解一下 Axios。

一、Axios 简介

Axios 是一个基于 Promise 的 HTTP 客户端,拥有以下特性:

  • 支持 Promise API;
  • 能够拦截请求和响应;
  • 能够转换请求和响应数据;
  • 客户端支持防御 CSRF 攻击;
  • 同时支持浏览器和 Node.js 环境;
  • 能够取消请求及自动转换 JSON 数据。

在浏览器端 Axios 支持大多数主流的浏览器,比如 Chrome、Firefox、Safari 和 IE 11。此外,Axios 还拥有自己的生态:


(数据来源 —— https://github.com/axios/axios/blob/master/ECOSYSTEM.md)

简单介绍完 Axios,我们来分析一下它提供的一个核心功能 —— 拦截器。

二、HTTP 拦截器的设计与实现

2.1 拦截器简介

对于大多数 SPA 应用程序来说, 通常会使用 token 进行用户的身份认证。这就要求在认证通过后,我们需要在每个请求上都携带认证信息。针对这个需求,为了避免为每个请求单独处理,我们可以通过封装统一的 request 函数来为每个请求统一添加 token 信息。

但后期如果需要为某些 GET 请求设置缓存时间或者控制某些请求的调用频率的话,我们就需要不断修改 request 函数来扩展对应的功能。此时,如果在考虑对响应进行统一处理的话,我们的 request 函数将变得越来越庞大,也越来越难维护。那么对于这个问题,该如何解决呢?Axios 为我们提供了解决方案 —— 拦截器。

Axios 是一个基于 Promise 的 HTTP 客户端,而 HTTP 协议是基于请求和响应:


所以 Axios 提供了请求拦截器和响应拦截器来分别处理请求和响应,它们的作用如下:

  • 请求拦截器:该类拦截器的作用是在请求发送前统一执行某些操作,比如在请求头中添加 token 字段。
  • 响应拦截器:该类拦截器的作用是在接收到服务器响应后统一执行某些操作,比如发现响应状态码为 401 时,自动跳转到登录页。

在 Axios 中设置拦截器很简单,通过 axios.interceptors.requestaxios.interceptors.response 对象提供的 use 方法,就可以分别设置请求拦截器和响应拦截器:

// 添加请求拦截器
axios.interceptors.request.use(function (config) {
  config.headers.token = 'added by interceptor';
  return config;
});

// 添加响应拦截器
axios.interceptors.response.use(function (data) {
  data.data = data.data + ' - modified by interceptor';
  return data;
});

那么拦截器是如何工作的呢?在看具体的代码之前,我们先来分析一下它的设计思路。Axios 的作用是用于发送 HTTP 请求,而请求拦截器和响应拦截器的本质都是一个实现特定功能的函数。

我们可以按照功能把发送 HTTP 请求拆解成不同类型的子任务,比如有用于处理请求配置对象的子任务,用于发送 HTTP 请求的子任务和用于处理响应对象的子任务。当我们按照指定的顺序来执行这些子任务时,就可以完成一次完整的 HTTP 请求。

了解完这些,接下来我们将从 「任务注册、任务编排和任务调度」 三个方面来分析 Axios 拦截器的实现。

2.2 任务注册

通过前面拦截器的使用示例,我们已经知道如何注册请求拦截器和响应拦截器,其中请求拦截器用于处理请求配置对象的子任务,而响应拦截器用于处理响应对象的子任务。要搞清楚任务是如何注册的,就需要了解 axiosaxios.interceptors 对象。

// lib/axios.js
function createInstance(defaultConfig) {
  var context = new Axios(defaultConfig);
  var instance = bind(Axios.prototype.request, context);

  // Copy axios.prototype to instance
  utils.extend(instance, Axios.prototype, context);
  // Copy context to instance
  utils.extend(instance, context);
  return instance;
}

// Create the default instance to be exported
var axios = createInstance(defaults);

在 Axios 的源码中,我们找到了 axios 对象的定义,很明显默认的 axios 实例是通过 createInstance 方法创建的,该方法最终返回的是 Axios.prototype.request 函数对象。同时,我们发现了 Axios 的构造函数:

// lib/core/Axios.js
function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

在构造函数中,我们找到了 axios.interceptors 对象的定义,也知道了 interceptors.requestinterceptors.response 对象都是 InterceptorManager 类的实例。因此接下来,进一步分析 InterceptorManager 构造函数及相关的 use 方法就可以知道任务是如何注册的:

// lib/core/InterceptorManager.js
function InterceptorManager() {
  this.handlers = [];
}

InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected
  });
  // 返回当前的索引,用于移除已注册的拦截器
  return this.handlers.length - 1;
};

通过观察 use 方法,我们可知注册的拦截器都会被保存到 InterceptorManager 对象的 handlers 属性中。下面我们用一张图来总结一下 Axios 对象与 InterceptorManager 对象的内部结构与关系:

2.3 任务编排

现在我们已经知道如何注册拦截器任务,但仅仅注册任务是不够,我们还需要对已注册的任务进行编排,这样才能确保任务的执行顺序。这里我们把完成一次完整的 HTTP 请求分为处理请求配置对象、发起 HTTP 请求和处理响应对象 3 个阶段。

接下来我们来看一下 Axios 如何发请求的:

axios({
  url: '/hello',
  method: 'get',
}).then(res =>{
  console.log('axios res: ', res)
  console.log('axios res.data: ', res.data)
})

通过前面的分析,我们已经知道 axios 对象对应的是 Axios.prototype.request 函数对象,该函数的具体实现如下:

// lib/core/Axios.js
Axios.prototype.request = function request(config) {
  config = mergeConfig(this.defaults, config);

  // 省略部分代码
  var chain = [dispatchRequest, undefined];
  var promise = Promise.resolve(config);

  // 任务编排
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });

  // 任务调度
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
};

任务编排的代码比较简单,我们来看一下任务编排前和任务编排后的对比图:

2.4 任务调度

任务编排完成后,要发起 HTTP 请求,我们还需要按编排后的顺序执行任务调度。在 Axios 中具体的调度方式很简单,具体如下所示:

 // lib/core/Axios.js
Axios.prototype.request = function request(config) {
  // 省略部分代码
  var promise = Promise.resolve(config);
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }
}

因为 chain 是数组,所以通过 while 语句我们就可以不断地取出设置的任务,然后组装成 Promise 调用链从而实现任务调度,对应的处理流程如下图所示:


下面我们来回顾一下 Axios 拦截器完整的使用流程:

// 添加请求拦截器 —— 处理请求配置对象
axios.interceptors.request.use(function (config) {
  config.headers.token = 'added by interceptor';
  return config;
});

// 添加响应拦截器 —— 处理响应对象
axios.interceptors.response.use(function (data) {
  data.data = data.data + ' - modified by interceptor';
  return data;
});

axios({
  url: '/hello',
  method: 'get',
}).then(res =>{
  console.log('axios res.data: ', res.data)
})

介绍完 Axios 的拦截器,我们来总结一下它的优点。Axios 通过提供拦截器机制,让开发者可以很容易在请求的生命周期中自定义不同的处理逻辑。

此外,也可以通过拦截器机制来灵活地扩展 Axios 的功能,比如 Axios 生态中列举的 axios-response-logger 和 axios-debug-log 这两个库。

参考 Axios 拦截器的设计模型,我们就可以抽出以下通用的任务处理模型:

三、HTTP 适配器的设计与实现

3.1 默认 HTTP 适配器

Axios 同时支持浏览器和 Node.js 环境,对于浏览器环境来说,我们可以通过 XMLHttpRequestfetch API 来发送 HTTP 请求,而对于 Node.js 环境来说,我们可以通过 Node.js 内置的 httphttps 模块来发送 HTTP 请求。

为了支持不同的环境,Axios 引入了适配器。在 HTTP 拦截器设计部分,我们看到了一个 dispatchRequest 方法,该方法用于发送 HTTP 请求,它的具体实现如下所示:

// lib/core/dispatchRequest.js
module.exports = function dispatchRequest(config) {
  // 省略部分代码
  var adapter = config.adapter || defaults.adapter;

  return adapter(config).then(function onAdapterResolution(response) {
    // 省略部分代码
    return response;
  }, function onAdapterRejection(reason) {
    // 省略部分代码
    return Promise.reject(reason);
  });
};

通过查看以上的 dispatchRequest 方法,我们可知 Axios 支持自定义适配器,同时也提供了默认的适配器。对于大多数场景,我们并不需要自定义适配器,而是直接使用默认的适配器。因此,默认的适配器就会包含浏览器和 Node.js 环境的适配代码,其具体的适配逻辑如下所示:

// lib/defaults.js
var defaults = {
  adapter: getDefaultAdapter(),
  xsrfCookieName: 'XSRF-TOKEN',
  xsrfHeaderName: 'X-XSRF-TOKEN',
  //...
}

function getDefaultAdapter() {
  var adapter;
  if (typeof XMLHttpRequest !== 'undefined') {
    // For browsers use XHR adapter
    adapter = require('./adapters/xhr');
  } else if (typeof process !== 'undefined' && 
    Object.prototype.toString.call(process) === '[object process]') {
    // For node use HTTP adapter
    adapter = require('./adapters/http');
  }
  return adapter;
}

getDefaultAdapter 方法中,首先通过平台中特定的对象来区分不同的平台,然后再导入不同的适配器,具体的代码比较简单,这里就不展开介绍。

3.2 自定义适配器

其实除了默认的适配器外,我们还可以自定义适配器。那么如何自定义适配器呢?这里我们可以参考 Axios 提供的示例:

var settle = require('./../core/settle');
module.exports = function myAdapter(config) {
  // 当前时机点:
  //  - config配置对象已经与默认的请求配置合并
  //  - 请求转换器已经运行
  //  - 请求拦截器已经运行

  // 使用提供的config配置对象发起请求
  // 根据响应对象处理Promise的状态
  return new Promise(function(resolve, reject) {
    var response = {
      data: responseData,
      status: request.status,
      statusText: request.statusText,
      headers: responseHeaders,
      config: config,
      request: request
    };

    settle(resolve, reject, response);

    // 此后:
    //  - 响应转换器将会运行
    //  - 响应拦截器将会运行
  });
}

在以上示例中,我们主要关注转换器、拦截器的运行时机点和适配器的基本要求。比如当调用自定义适配器之后,需要返回 Promise 对象。这是因为 Axios 内部是通过 Promise 链式调用来完成请求调度,不清楚的小伙伴可以重新阅读 “拦截器的设计与实现” 部分的内容。

现在我们已经知道如何自定义适配器了,那么自定义适配器有什么用呢?在 Axios 生态中,阿宝哥发现了 axios-mock-adapter 这个库,该库通过自定义适配器,让开发者可以轻松地模拟请求。对应的使用示例如下所示:

var axios = require("axios");
var MockAdapter = require("axios-mock-adapter");

// 在默认的Axios实例上设置mock适配器
var mock = new MockAdapter(axios);

// 模拟 GET /users 请求
mock.onGet("/users").reply(200, {
  users: [{ id: 1, name: "John Smith" }],
});

axios.get("/users").then(function (response) {
  console.log(response.data);
});

对 MockAdapter 感兴趣的小伙伴,可以自行了解一下 axios-mock-adapter 这个库。到这里我们已经介绍了 Axios 的拦截器与适配器,下面阿宝哥用一张图来总结一下 Axios 使用请求拦截器和响应拦截器后,请求的处理流程:

四、CSRF 防御

4.1 CSRF 简介

「跨站请求伪造」(Cross-site request forgery),通常缩写为 「CSRF」 或者 「XSRF」, 是一种挟制用户在当前已登录的 Web 应用程序上执行非本意的操作的攻击方法。

跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。

为了让小伙伴更好地理解上述的内容,阿宝哥画了一张跨站请求攻击示例图:


在上图中攻击者利用了 Web 中用户身份验证的一个漏洞:「简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的」。既然存在以上的漏洞,那么我们应该怎么进行防御呢?接下来我们来介绍一些常见的 CSRF 防御措施。

4.2 CSRF 防御措施

4.2.1 检查 Referer 字段

HTTP 头中有一个 Referer 字段,这个字段用以标明请求来源于哪个地址。「在处理敏感数据请求时,通常来说,Referer 字段应和请求的地址位于同一域名下」

以示例中商城操作为例,Referer 字段地址通常应该是商城所在的网页地址,应该也位于 www.semlinker.com 之下。而如果是 CSRF 攻击传来的请求,Referer 字段会是包含恶意网址的地址,不会位于 www.semlinker.com 之下,这时候服务器就能识别出恶意的访问。

这种办法简单易行,仅需要在关键访问处增加一步校验。但这种办法也有其局限性,因其完全依赖浏览器发送正确的 Referer 字段。虽然 HTTP 协议对此字段的内容有明确的规定,但并无法保证来访的浏览器的具体实现,亦无法保证浏览器没有安全漏洞影响到此字段。并且也存在攻击者攻击某些浏览器,篡改其 Referer 字段的可能。

4.2.2 同步表单 CSRF 校验

CSRF 攻击之所以能够成功,是因为服务器无法区分正常请求和攻击请求。针对这个问题我们可以要求所有的用户请求都携带一个 CSRF 攻击者无法获取到的 token。对于 CSRF 示例图中的表单攻击,我们可以使用 「同步表单 CSRF 校验」 的防御措施。

「同步表单 CSRF 校验」 就是在返回页面时将 token 渲染到页面上,在 form 表单提交的时候通过隐藏域或者作为查询参数把 CSRF token 提交到服务器。比如,在同步渲染页面时,在表单请求中增加一个 _csrf 的查询参数,这样当用户在提交这个表单的时候就会将 CSRF token 提交上来:

<form method="POST" action="/upload?_csrf={{由服务端生成}}" enctype="multipart/form-data">
  用户名: <input name="name" />
  选择头像: <input name="file" type="file" />
  <button type="submit">提交</button>
</form>
4.2.3 双重 Cookie 防御

「双重 Cookie 防御」 就是将 token 设置在 Cookie 中,在提交(POST、PUT、PATCH、DELETE)等请求时提交 Cookie,并通过请求头或请求体带上 Cookie 中已设置的 token,服务端接收到请求后,再进行对比校验。

下面我们以 jQuery 为例,来看一下如何设置 CSRF token:

let csrfToken = Cookies.get('csrfToken');

function csrfSafeMethod(method) {
  // 以下HTTP方法不需要进行CSRF防护
  return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}

$.ajaxSetup({
  beforeSend: function(xhr, settings) {
    if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
      xhr.setRequestHeader('x-csrf-token', csrfToken);
    }
  },
});

介绍完 CSRF 攻击的方式和防御手段,最后我们来看一下 Axios 是如何防御 CSRF 攻击的。

4.3 Axios CSRF 防御

Axios 提供了 xsrfCookieNamexsrfHeaderName 两个属性来分别设置 CSRF 的 Cookie 名称和 HTTP 请求头的名称,它们的默认值如下所示:

// lib/defaults.js
var defaults = {
  adapter: getDefaultAdapter(),

  // 省略部分代码
  xsrfCookieName: 'XSRF-TOKEN',
  xsrfHeaderName: 'X-XSRF-TOKEN',
};

前面我们已经知道在不同的平台中,Axios 使用不同的适配器来发送 HTTP 请求,这里我们以浏览器平台为例,来看一下 Axios 如何防御 CSRF 攻击:

// lib/adapters/xhr.js
module.exports = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    var requestHeaders = config.headers;

    var request = new XMLHttpRequest();
    // 省略部分代码

    // 添加xsrf头部
    if (utils.isStandardBrowserEnv()) {
      var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
        cookies.read(config.xsrfCookieName) :
        undefined;

      if (xsrfValue) {
        requestHeaders[config.xsrfHeaderName] = xsrfValue;
      }
    }

    request.send(requestData);
  });
};

看完以上的代码,相信小伙伴们就已经知道答案了,原来 Axios 内部是使用 「双重 Cookie 防御」 的方案来防御 CSRF 攻击。

好的,到这里本文的主要内容都已经介绍完了,其实 Axios 项目还有一些值得我们借鉴的地方,比如 CancelToken 的设计、异常处理机制等,感兴趣的小伙伴可以自行学习一下。

五、参考资源

  • Github - axios
  • 维基百科 - 跨站请求伪造
  • Egg - 安全威胁 CSRF 的防范

https://mp.weixin.qq.com/s/OWUpLpOOMFG9JJSg6gJKxg

iOS中的内嵌汇编

写一篇在iOS上使用汇编的文章的想法在脑袋里面停留了很久了,但是迟迟没有动手。虽然早前在做启动耗时优化的工作中,也做过通过拦截objc_msgSend并插入汇编指令来统计方法调用耗时的工作,但也只仅此而已。刚好最近的时间项目在做安全加固,需要写更多的汇编来提高安全性(文章内汇编使用指令集为ARM64),也就有了本文

发布于:11天以前  |  58次阅读  |  详细内容 »

不会吧,这也行?iOS后台锁屏监听摇一摇

一般情况下,出于省电、权限、合理性等因素考虑,给人的感觉是很多奇怪的需求安卓可以实现,但是iOS就无法实现!今天要介绍的需求也有这种感觉,就是“当 APP 处于后台或锁屏状态时,依旧可以监听到摇一摇,进而触发某些功能,比如:语音播报”。

发布于:1月以前  |  126次阅读  |  详细内容 »

iOS 稳定性:App 被终止的原因

本次 session 主要内容如下: 介绍了后台应用终止的常见原因,并提供了一些优化建议 介绍了 MetricsKit 提供的在代码中获取诊断和性能数据的方法 介绍了 Xcode Metrics Ogranizer 提供的关于线上用户性能数据的可视化报告

发布于:1月以前  |  155次阅读  |  详细内容 »

Vue中Axios的封装和API接口的管理

在vue项目中,和后台交互获取数据这块,我们通常使用的是axios库,它是基于promise的http库,可运行在浏览器端和node.js中。他有很多优秀的特性,例如拦截请求和响应、取消请求、转换json、客户端防御XSRF等。所以我们的尤大大也是果断放弃了对其官方库vue-resource的维护,直接推荐我们使用axios库。如果还对axios不了解的,可以移步axios文档。

发布于:1月以前  |  158次阅读  |  详细内容 »

iOS 持续集成:更完备的 App Store Connect API

时隔两年 App Store Connect API 有了更新,WWDC 2018 推出了 App Store Connect API ,用于自动化一些 App Store Connect 后台操作。这次更新包含了 app 元数据相关的API,补上了原来缺失的重要一环, 使得几乎可以通过 App Store Connect API 完成 App Store Connect 上的所有操作。今后开发、证书配置、用户管理、测试、发布全流程都可以通过 API 完成。

发布于:1月以前  |  200次阅读  |  详细内容 »

iOS 性能优化:优化 App 启动速度

苹果是一家特别注重用户体验的公司,过去几年一直在优化 App 的启动时间,特别是去年的 WWDC 2019 keynote[1] 上提到,在过去一年苹果开发团队对启动时间提升了 200%

发布于:1月以前  |  182次阅读  |  详细内容 »

让你的应用远离越狱:iOS 14 App Attest 防护功能

当越狱在 iOS 设备第一次流行起来时,iOS 开发人员会尝试各种方法来保护自己的应用程序,以让应用免受盗版等不确定因素的困扰。有许多方法可以做到这一点,包括检查 Cydia 是否存在、检测应用程序是否可读取自身沙箱之外的文件、在检测到调试器时让应用程序崩溃等等。

发布于:1月以前  |  197次阅读  |  详细内容 »

探秘 iOS 14 的 WidgetKit

Widget Extension 提供了 small, medium, large 三个尺寸,不同尺寸可以展示不同的数据、不同的界面,开发者也可以锁定自己APP的 Widget 只有某类尺寸,相同的widget也能重复添加。作为添加在主屏幕上的控件,苹果用了 “At a glance” 来形容 widget ,所以 widget extension 是无法交互的,它能做的只有展示一些信息与点击两个作用,点击后就会引导至app,同时为了性能与耗电量的考虑,Widget extension 也不能展示视频和动态图像。

发布于:1月以前  |  215次阅读  |  详细内容 »

iOS14 Widget 万字指北,先人一步获得顶级流量

2020 年 6 月 22 日,苹果召开了第一次线上的开发者大会 - WWDC20。这次发布会上宣布了ARM架构Mac芯片(拳打Intel)、iOS 14 ATT(脚踢Facebook),可谓是一次载入史册(我是爸爸)的发布会了,当然还发布了被称为下一个顶级流量入口的Widget。踩着八月的尾巴,本次我们就来探究一下Widget。本文会从Widget初窥和Widget开发两个维度和章节来探究一下Widget, 其中初窥章节会带您简单的了解一下Widget,适合应用决策者阅读; 开发章节会带着您一步一步的完成设计开发Widget,适合程序员阅读。

发布于:1月以前  |  276次阅读  |  详细内容 »

OCRunner:完全体的iOS热修复方案

为了能够实现一篇文章的思路:Objective-C源码 -> 二进制补丁文件 ->热更新(具体是哪篇我忘了)。当时刚好开始了oc2mango翻译器的漫漫长路(顺带为了学习编译原理,嘻嘻),等基本完成以后,就开始肝OCRunner:完全兼容struct,enum,系统C函数调用,魔改libffi,生成补丁文件等,尽可能兼容Objective-C,为了做一个直接运行OC的快乐人。

发布于:1月以前  |  228次阅读  |  详细内容 »

iOS APP图标版本化

在我们的项目开发过程中,需要频繁打包给测试人员去测试,有时候我们都不知道测试机上安装的版本是否是最新的,这样会造成很多不必要的麻烦和成本。因此我们需要将buildNumber以水印的方式打在APPIcon上,可以很直观的知道当前是哪一个版本。

发布于:1月以前  |  212次阅读  |  详细内容 »

百度App iOS工程化实践: EasyBox破冰之旅

百度App从单一的搜索工具发展到今天以搜索和Feed流为双引擎的综合性内容消费服务平台,其复杂程度已然不可同日而语矣。 作为一个日活过亿的超级App,业务规模庞大,相关技术人员超过千人,客户端支持主流的移动技术,涉及近百业务方,技术形态复杂,各种组件近三百个,代码百万量级,由此带来的工程化问题是技术团队的一个极大挑战。

发布于:1月以前  |  232次阅读  |  详细内容 »

最多阅读

快速配置 Sign In with Apple 1年以前  |  3447次阅读
开篇 关于iOS越狱开发 1年以前  |  2458次阅读
给数组NSMutableArray排序 1年以前  |  2405次阅读
APP适配iOS11 1年以前  |  2379次阅读
在越狱的iPhone设置上使用lldb调试 1年以前  |  2329次阅读
UITableViewCell高亮效果实现 1年以前  |  2232次阅读
使用 GPUImage 实现一个简单相机 1年以前  |  2225次阅读
App Store 审核指南[2017年最新版本] 1年以前  |  2203次阅读
所有iPhone设备尺寸汇总 1年以前  |  2126次阅读
使用ssh访问越狱iPhone的两种方式 1年以前  |  2043次阅读
关于Xcode不能打印崩溃日志 1年以前  |  2015次阅读
使用ssh 访问越狱iPhone的两种方式 1年以前  |  1901次阅读
UIDevice的简单使用 1年以前  |  1742次阅读
为对象添加一个释放时触发的block 1年以前  |  1703次阅读
使用最高权限操作iPhone手机 1年以前  |  1668次阅读