cmd、命令行、终端签名apk

先cd到待签名的apk目录下,将签名文件也放入相同目录,然后执行以下命令,mac系统天然支持。

 

jarsigner -verbose -keystore android123.keystore -signedjar android123_signed.apk android123.apk android123.keystore

微信小程序架构解析,工作原理解析

作者介绍:

渠宏伟,腾讯高级工程师,从事Web前端开发5年,先后负责企鹅电竞、腾讯视频VIP、腾讯OA开发框架、腾讯微信HR助手等项目。对Web前端架构、.NET架构有丰富的经验。

| 导语 微信小程序的公测掀起了学习小程序开发的浪潮,天生跨平台,即用即走、媲美原生体验、完善的文档、高效的开发框架,小程序给开发者带来了很多惊喜。通过这篇文章和大家一起分析小程序的架构,分享开发经验。

一、小程序介绍

1、小程序特点

微信小程序原理解析

2、小程序演示


视频地址:https://v.qq.com/x/page/w0353d7co6y.html

3、小程序为什么那么快

微信小程序原理解析
Page Frame

Native预先额外加载一个WebView
当打开指定页面时,用默认数据直接渲染,请求数据回来时局部更新
返回显示历史View
退出小程序,View状态不销毁

4、小程序入口

微信小程序原理解析

扫码进入小程序

搜索小程序

小程序发送到桌面(Android)

发送给朋友

二、小程序架构

微信小程序的框架包含两部分View视图层、App Service逻辑层,View层用来渲染页面结构,AppService层用来逻辑处理、数据请求、接口调用,它们在两个线程里运行。

视图层使用WebView渲染,逻辑层使用JSCore运行。

视图层和逻辑层通过系统层的JSBridage进行通信,逻辑层把数据变化通知到视图层,触发视图层页面更新,视图层把触发的事件通知到逻辑层进行业务处理。

微信小程序原理解析

 

小程序启动时会从CDN下载小程序的完整包

微信小程序原理解析

 

三、View (页面视图)

视图层由 WXML 与 WXSS 编写,由组件来进行展示。

将逻辑层的数据反应成视图,同时将视图层的事件发送给逻辑层。

1、View – WXML

WXML(WeiXin Markup Language)

支持数据绑定

支持逻辑算术、运算

支持模板、引用

支持添加事件(bindtap)

微信小程序原理解析

wxml编译器:wcc  把wxml文件 转为 js   执行方式:wcc index.wxml

2、View – WXSS

WXSS(WeiXin Style Sheets)

支持大部分CSS特性

添加尺寸单位rpx,可根据屏幕宽度自适应

使用@import语句可以导入外联样式表

不支持多层选择器-避免被组件内结构破坏

微信小程序原理解析

wxss编译器:wcsc 把wxss文件转化为 js 执行方式: wcsc index.wxss

3、View – WXSS Selectors

WXSS目前支持如下选择器:

微信小程序原理解析
4、View – Component

小程序提供了一系列组件用于开发业务功能,按照功能与HTML5的标签进行对比如下:

微信小程序原理解析

小程序的组件基于Web Component标准

使用Polymer框架实现Web Component

微信小程序原理解析

 

5、View – Native Component

目前Native实现的组件有 <canvas/> <video/> <map/> <textarea/>

Native组件层在WebView层之上

微信小程序原理解析

 

四、App Service(逻辑层)

逻辑层将数据进行处理后发送给视图层,同时接受视图层的事件反馈

1、App( ) 小程序的入口;Page( ) 页面的入口

3、提供丰富的 API,如微信用户数据,扫一扫,支付等微信特有能力。

4、每个页面有独立的作用域,并提供模块化能力。

5、数据绑定、事件分发、生命周期管理、路由管理

运行环境

IOS – JSCore

Android – X5 JS解析器

DevTool – nwjs Chrome 内核

1、App Service – Binding

数据绑定使用 Mustache 语法(双大括号)将变量包起来,动态数据均来自对应 Page 的 data,可以通过setData方法修改数据。
事件绑定的写法同组件的属性,以 key、value 的形式,key 以bind或catch开头,然后跟上事件的类型,如bindtap, catchtouchstart,value 是一个字符串,需要在对应的 Page 中定义同名的函数。

微信小程序原理解析

微信小程序原理解析

 

2、App Service – Life Cylce

微信小程序原理解析

 

3、App Service – API

API通过JSBridge和Native 进行通信

微信小程序原理解析

 

4、App Service – Router

navigateTo(OBJECT)

保留当前页面,跳转到应用内的某个页面,使用navigateBack可以返回到原页面。页面路径只能是五层

redirectTo(OBJECT)

关闭当前页面,跳转到应用内的某个页面。

navigateBack(OBJECT)

关闭当前页面,返回上一页面或多级页面。可通过 getCurrentPages()) 获取当前的页面栈,决定需要返回几层。

 

五、小程序开发经验

1、小程序存在的问题

小程序仍然使用WebView渲染,并非原生渲染
需要独立开发,不能在非微信环境运行。
开发者不可以扩展新组件。
服务端接口返回的头无法执行,比如:Set-Cookie。
依赖浏览器环境的js库不能使用,因为是JSCore执行的,没有window、document对象。
WXSS中无法使用本地(图片、字体等)。
WXSS转化成js 而不是css,为了兼容rpx。
WXSS不支持级联选择器。
小程序无法打开页面,无法拉起APP。

小程序不能和公众号重名,于是小程序的名字就成了:自选股+、滴滴出行DiDi 。

 

2、小程序可以借鉴的优点

提前新建WebView,准备新页面渲染。
View层和逻辑层分离,通过数据驱动,不直接操作DOM。
使用Virtual DOM,进行局部更新。
全部使用https,确保传输中安全。
使用离线能力。
前端组件化开发。
加入rpx单位,隔离设备尺寸,方便开发。

 

3、脱离微信的“小程序”:PWA 渐进式应用

PWA 全称是 Progressive Web Apps ,译成中文就是渐进式应用,是 Google 在 2015 年 6 月 15 日提出的概念。
Progressive Web Apps 是结合了 web 和 原生应用中最好功能的一种体验。对于首次访问的用户它是非常有利的, 用户可以直接在浏览器中进行访问,不需要安装应用。随着时间的推移当用户渐渐地和应用建立了联系,它将变得越来越强大。它能够快速地加载,即使在弱网络环境下,能够推送相关消息, 也可以像原生应用那样添加至主屏,能够有全屏浏览的体验。

微信小程序原理解析
PWA具有如下特点:

渐进增强 – 支持的新特性的浏览器获得更好的体验,不支持的保持原来的体验。
离线访问 – 通过 service workers 可以在离线或者网速差的环境下工作。
类原生应用 – 使用app shell model做到原生应用般的体验。
可安装 – 允许用户保留对他们有用的应用在主屏幕上,不需要通过应用商店。
容易分享 – 通过 URL 可以轻松分享应用。
持续更新 – 受益于 service worker 的更新进程,应用能够始终保持更新。
安全 – 通过 HTTPS 来提供服务来防止网络窥探,保证内容不被篡改。
可搜索 – 得益于 W3C manifests 元数据和 service worker 的登记,让搜索引擎能够找到 web 应用。
再次访问 – 通过消息推送等特性让用户再次访问变得容易。

Web App Manifest使Web更像Native

Web App Manifest以JSON的格式定义Web应用的相关配置(应用名称、图标或图像连接、启动URL、自定义特性、启动默认配置、全屏设置等)。

微信小程序原理解析

Service Workers增强Web能力

通过Service Works实现资源离线缓存和更新

微信小程序原理解析

App Shell 提升显示效率

App Shell(应用外壳)是应用的用户界面所需的最基本的 HTML、CSS 和 JavaScript,首次加载后立刻被缓存下来,不需要每次使用时都被下载,而是只异步加载需要的数据,以达到UI保持本地化。

微信小程序原理解析

了解更多pwa资料:
https://developers.google.com/web/progressive-web-apps/

 

Android的MVP框架讲解及MVP与RxJava配合

内容大纲:

  1. Android 开发框架的选择
  2. 如何一步步搭建分层框架
  3. 使用 RxJava 来解决主线程发出网络请求的问题
  4. 结语

一、Android开发框架的选择

由于原生 Android 开发应该已经是一个基础的 MVC 框架,所以在初始开发的时候并没有遇到太多框架上的问题,可是一旦项目规模到了一定的程度,就需要对整个项目的代码结构做一个总体上的规划,最终的目的是使代码可读,维护性好,方便测试。’

只有项目复杂度到了一定程度才需要使用一些更灵活的框架或者结构,简单来说,写个 Hello World 并不需要任何第三方的框架

原生的 MVC 框架遇到大规模的应用,就会变得代码难读,不好维护,无法测试的囧境。因此,Android 开发方面也有很多对应的框架来解决这些问题。

构建框架的最终目的是增强项目代码的可读性维护性方便测试 ,如果背离了这个初衷,为了使用而使用,最终是得不偿失的

从根本上来讲,要解决上述的三个问题,核心思想无非两种:一个是分层 ,一个是模块化 。两个方法最终要实现的就是解耦,分层讲的是纵向层面上的解耦,模块化则是横向上的解耦。下面我们来详细讨论一下 Android 开发如何实现不同层面上的解耦。

解耦的常用方法有两种:分层模块化

横向的模块化对大家来可能并不陌生,在一个项目建立项目文件夹的时候就会遇到这个问题,通常的做法是将相同功能的模块放到同一个目录下,更复杂的,可以通过插件化来实现功能的分离与加载。

纵向的分层,不同的项目可能就有不同的分法,并且随着项目的复杂度变大,层次可能越来越多。

对于经典的 Android MVC 框架来说,如果只是简单的应用,业务逻辑写到 Activity 下面并无太多问题,但一旦业务逐渐变得复杂起来,每个页面之间有不同的数据交互和业务交流时,activity 的代码就会急剧膨胀,代码就会变得可读性,维护性很差。

所以这里我们就要介绍 Android 官方推荐的 MVP 框架,看看 MVP 是如何将 Android 项目层层分解。

二、如何一步步搭建分层框架

如果你是个老司机,可以直接参考下面几篇文章(可在 google 搜到):

  1. Android Application Architecture
  2. Android Architecture Blueprints – Github
  3. Google 官方 MVP 示例之 TODO-MVP – 简书
  4. 官方示例1-todo-mvp – github
  5. dev-todo-mvp-rxjava – github

当然如果你觉得看官方的示例太麻烦,那么本文会通过最简洁的语言来讲解如何通过 MVP 来实现一个合适的业务分层。

对一个经典的 Android MVC 框架项目来讲,它的代码结构大概是下面这样(图片来自参考文献)

pic_001

简单来讲,就是 Activity 或者 Fragment 直接与数据层交互,activity 通过 apiProvider 进行网络访问,或者通过 CacheProvider 读取本地缓存,然后在返回或者回调里对 Activity 的界面进行响应刷新。

这样的结构在初期看来没什么问题,甚至可以很快的开发出来一个展示功能,但是业务一旦变得复杂了怎么办?

我们作一个设想,假如一次数据访问可能需要同时访问 api 和 cache,或者一次数据请求需要请求两次 api。对于 activity 来说,它既与界面的展示,事件等有关系,又与业务数据层有着直接的关系,无疑 activity 层会极剧膨胀,变得极难阅读和维护。

在这种结构下, activity 同时承担了 view 层和 controller 层的工作,所以我们需要给 activity 减负

所以,我们来看看 MVP 是如何做这项工作的(图片来自参考文献)

pic_002

这是一个比较典型的 MVP 结构图,相比于第一张图,多了两个层,一个是 Presenter 和 DataManager 层。

所谓自古图片留不住,总是代码得人心。下面用代码来说明这个结构的实现。

首先是 View 层的 Activity,假设有一个最简单的从 Preference 中获取字符串的界面

public class MainActivity extends Activity implements MainView {

    MainPresenter presenter;
    TextView mShowTxt;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mShowTxt = (TextView)findViewById(R.id.text1);
        loadDatas();
    }

    public void loadDatas() {
        presenter = new MainPresenter();
        presenter.addTaskListener(this);
        presenter.getString();
    }

    @Override
    public void onShowString(String str) {
        mShowTxt.setText(str);
    }
}

Activity 里面包含了几个文件,一个是 View 层的对外接口 MainView,一个是P层 Presenter

首先对外接口 MainView 文件

public interface MainView {
    void onShowString(String json);
}

因为这个界面比较简单,只需要在界面上显示一个字符串,所以只有一个接口 onShowString,再看P层代码

public class MainPresenter {

    MainView mainView;
    TaskManager taskData;

    public MainPresenter() {
        this.taskData = new TaskManager(new TaskDataSourceImpl());
    }

    public MainPresenter test() {
        this.taskData = new TaskManager(new TaskDataSourceTestImpl());
        return this;
    }

    public MainPresenter addTaskListener(MainView viewListener) {
        this.mainView = viewListener;
        return this;
    }

    public void getString() {
        String str = taskData.getTaskName();
        mainView.onShowString(str);
    }

}

可以看到 Presenter 层是连接 Model 层和 View 层的中间层,因此持有 View 层的接口和 Model 层的接口。这里就可以看到 MVP 框架的威力了,通过接口的形式将 View 层和 Model 层完全隔离开来。

接口的作用类似给层与层之间制定的一种通信协议,两个不同的层级相互交流,只要遵守这些协议即可,并不需要知道具体的实现是怎样

看到这里,有人可能就要问,这跟直接调用有什么区别,为什么要大费周章的给 view 层和 Model 层各设置一个接口呢?具体原因,我们看看 Model 层的实现类就知道了。

下面这个文件是 DataManager.java,对应的是图中的 DataManager 模块

/**
 * 从数据层获取的数据,在这里进行拼装和组合
 */
public class TaskManager {
    TaskDataSource dataSource;

    public TaskManager(TaskDataSource dataSource) {
        this.dataSource = dataSource;
    }

    public String getShowContent() {
        //Todo what you want do on the original data
        return dataSource.getStringFromRemote() + dataSource.getStringFromCache();
    }
}

TaskDataSource.java 文件

/**
 * data 层接口定义
 */
public interface TaskDataSource {
    String getStringFromRemote();
    String getStringFromCache();
}

TaskDataSourceImpl.java 文件

public class TaskDataSourceImpl implements TaskDataSource {
    @Override
    public String getStringFromRemote() {
        return "Hello ";
    }

    @Override
    public String getStringFromCache() {
        return "World";
    }
}

TaskDataSourceTestImpl.java 文件

public class TaskDataSourceTestImpl implements TaskDataSource {
    @Override
    public String getStringFromRemote() {
        return "Hello ";
    }

    @Override
    public String getStringFromCache() {
        return " world Test ";
    }
}

从上面几个文件来看, TaskDataSource.java 作为数据层对外的接口, TaskDataSourceImpl.java 是数据层,直接负责数据获取,无论是从api获得,还是从本地数据库读取数据,本质上都是IO操作。 TaskManager 是作为业务层,对获取到的数据进行拼装,然后交给调用层。

这里我们来看看分层的作用

首先来讲业务层 TaskManager,业务层的上层是 View 层,下层是 Data 层。在这个类里,只有一个 Data 层的接口,所以业务层是不关心数据是如何取得,只需要通过接口获得数据之后,对原始的数据进行组合和拼装。因为完全与其上层和下层分离,所以我们在测试的时候,可以完全独立的是去测试业务层的逻辑。

TaskManager 中的 construct 方法的参数是数据层接口,这意味着我们可以给业务层注入不同的数据层实现。 正式线上发布的时候注入 TaskDataSourceImpl 这个实现,在测试业务层逻辑的时候,注入 TaskDataSourceTestImpl.java 实现。

这也正是使用接口来处理每个层级互相通信的好处,可以根据使用场景的不用,使用不同的实现

到现在为止一个基于 MVP 简单框架就搭建完成了,但其实还遗留了一个比较大的问题。

Android 规定,主线程是无法直接进行网络请求,会抛出 NetworkOnMainThreadException 异常

我们回到 Presenter 层,看看这里的调用。因为 presenter 层并不知道业务层以及数据层到底是从网络获取数据,还是从本地获取数据(符合层级间相互透明的原则),因为每次调用都可能存在触发这个问题。并且我们知道,即使是从本地获取数据,一次简单的IO访问也要消耗10MS左右。因此多而复杂的IO可能会直接引发页面的卡顿。

理想的情况下,所有的数据请求都应当在线程中完成,主线程只负责页面渲染的工作

当然,Android 本身提供一些方案,比如下面这种:

public void getString() {
    final Handler mainHandler = new Handler(Looper.getMainLooper());
    new Thread(){
        @Override
        public void run() {
            super.run();
            final String str = taskData.getShowContent();
            mainHandler.post(new Runnable() {
                @Override
                public void run() {
                    mainView.onShowString(str);
                }
            });
        }
    }.start();
}

通过新建子线程进行IO读写获取数据,然后通过主线程的 Looper 将结果通过传回主线程进行渲染和展示。

但每个调用都这样写,首先是新建线程会增加额外的成功,其次就是代码看起来很难读,缩进太多。

好在有了 RxJava ,可以比较方便的解决这个问题。

三、使用RxJava来解决主线程发出网络请求的问题

RxJava 是一个天生用来做异步的工具,相比 AsyncTask, Handler 等,它的优点就是简洁,无比的简洁。

在 Android 中使用 RxJava 需要加入下面两个依赖

compile 'io.reactivex:rxjava:1.0.14' 
compile 'io.reactivex:rxandroid:1.0.1'

这里我们直接介绍如何使用 RxJava 解决这个问题,直接在 presenter 中修改调用方法 getString

public class MainPresenter {

    MainView mainView;
    TaskManager taskData;

    public MainPresenter() {
        this.taskData = new TaskManager(new TaskDataSourceImpl());
    }

    public MainPresenter test() {
        this.taskData = new TaskManager(new TaskDataSourceTestImpl());
        return this;
    }

    public MainPresenter addTaskListener(MainView viewListener) {
        this.mainView = viewListener;
        return this;
    }

    public void getString() {
        Func1 dataAction = new Func1<String,String>() {
                @Override
                public String call(String param) {
                    return  taskData.getTaskName();
                }
            }    
        Action1 viewAction = new Action1<String>() {
                @Override
                public void call( String str) {
                    mainView.onShowString(str);
                }
            };        
        Observable.just("")
            .observeOn(Schedulers.io())
            .map(dataAction)
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(view);

    }

}

简单说明一下,与业务数据层的交互被定义到 Action1 里,然后交由 rxJava,指定 Schedulers.io() 获取到的线程来执行。Shedulers.io() 是专门用来进行IO访问的线程,并且线程会重复利用,不需要额外的线程管理。而数据返回到 View 层的操作是在 Action1 中完全,由 rxJava 交由 AndroidSchedulers.mainThread() 指定的UI主线程来执行。

从代码量上来讲,似比上一种方式要更多了,但实际上,当业务复杂度成倍增加的时候,RxJava 可以采用这种链式编程方式随意的增加调用和返回,而实现方式要比前面的方法灵活得多,简洁得多。

具体的内容就不在这里讲了,大家可以看参考下面的文章(可在 google 搜到):

  1. 给 Android 开发者的 RxJava 详解
  2. RxJava 与 Retrofit 结合的最佳实践
  3. RxJava使用场景小结
  4. How To Use RxJava

RxJava 的使用场景远不止这些,在上面第三篇文章提到了以下几种使用场景:

  1. 取数据先检查缓存的场景
  2. 需要等到多个接口并发取完数据,再更新
  3. 一个接口的请求依赖另一个API请求返回的数据
  4. 界面按钮需要防止连续点击的情况
  5. 响应式的界面
  6. 复杂的数据变换

四、结语

至此为止,通过 MVP+RxJava 的组合,我们已经构建出一个比较灵活的 Android 项目框架,总共分成了四部分:View 层,Presenter 层,Model 业务层,Data 数据持久化层。这个框架的优点大概有以下几点:

  • 每层各自独立,通过接口通信
  • 实现与接口分离,不同场景(正式,测试)可以挂载不同的实现,方便测试和开发写假数据
  • 所有的业务逻辑都在非UI线程中进行,最大限度减少IO操作对UI的影响
  • 使用 RxJava 可以将复杂的调用进行链式组合,解决多重回调嵌套问题

如何高效的使用GITHUB,GITHUB的正确使用方式

转载:http://stormzhang.com/github/2016/07/28/learn-github-from-zero7/

之前发过一系列有关 GitHub 的文章,有同学问了,GitHub 我大概了解了,Git 也差不多会使用了,但是 还是搞不清 GitHub 如何帮助我的工作,怎么提升我的工作效率?

问到点子上了,GitHub 其中一个最重要的作用就是发现全世界最优秀的开源项目,你没事的时候刷刷微博、知乎,人家没事的时候刷刷 GitHub ,看看最近有哪些流行的项目,久而久之,这差距就越来越大,那么如何发现优秀的开源项目呢?这篇文章我就来给大家介绍下。

1. 关注一些活跃的大牛

GitHub 主页有一个类似微博的时间线功能,所有你关注的人的动作,比如 star、fork 了某个项目都会出现在你的时间线上,这种方式适合我这种比较懒的人,不用主动去找项目,而这种基本是我每天获取信息的一个很重要的方式。不知道怎么关注这些人?那么很简单,关注我 stormzhang ,以及我 GitHub 上关注的一些大牛,基本就差不多了。

图片描述

点击下图的 Explore 菜单到“发现”页面

图片描述

紧接着点击 Trending 按钮

图片描述

这个 Trending 页面是干嘛的呢?直译过来就是趋势的意思,就是说这个页面你可以看到最近一些热门的开源项目,这个页面可以算是很多人主动获取一些开源项目最好的途径,可以选择「当天热门」、「一周之内热门」和「一月之内热门」来查看,并且还可以分语言类来查看,比如你想查看最近热门的 Android 项目,那么右边就可以选择 Java 语言。

图片描述

这样页面推荐大家每隔几天就去看下,主动发掘一些优秀的开源项目。

除了 Trending ,还有一种最主动的获取开源项目的方式,那就是 GitHub 的 Search 功能。

举个例子,你是做 Android 的,接触 GitHub 没多久,那么第一件事就应该输入 android 关键字进行搜索,然后右上角选择按照 star 来排序,结果如下图:

图片描述

如果你是学习 iOS 的,那么不妨同样的方法输入 iOS 关键字看看结果:

图片描述

可以看到按照 star 数,排名靠前基本是一些比较火的项目,一定是很有用,才会这么火。值得一提的是左侧依然可以选择语言进行过滤。

而对于实际项目中用到一些库,基本上都会第一时间去 GitHub 搜索下有没有类似的库,比如项目中想采用一个网络库,那么不妨输入 android http 关键字进行搜索,因为我只想找到关于 Android 的项目,所以搜索的时候都会加上 android 关键字,按照 star 数进行排序,我们来看下结果:

图片描述

可以看到 Retrofit、OkHttp、android-async-http 是最流行的网络库,只不过 android-async-http 的作者不维护了,之前很多人问我网络库用哪个比较好?哪怕你对每个网络库都不是很了解,那么单纯的按照这种方式你都该优先选择 Retrofit 或者 OkHttp,而目前绝大部分 Android 开发者确实也都是在用这两个网络库,当然还有部分在用 Volley 的,因为 google 没有选择在 GitHub 开源 volley,所以搜不到 volley 的上榜。

除此之外,GitHub 的 Search 还有一些小技巧,比如你想搜索的结果中 star 数大于1000的,那么可以这样搜索:

android http stars:>1000

当然还有其他小技巧,但是我觉得不是很重要,就不多说了。

有些人如果习惯用 Google 进行搜索,那么想搜索 GitHub 上的结果,不妨前面加 GitHub 关键字就ok了,比如我在 google 里输入 GitHub android http ,每个关键字用空格隔开,然后搜索结果如下:

图片描述

可以看到,基本也是我们想要的结果,只不过排序就不是单纯的按照 star 来排序了。

福利大放送

相信以上三种方法够大家遨游在 GitHub 的海洋了,最后给大家献上一些福利,这些项目是 GitHub 上影响力很大,同时又对你们很有用的项目:

这个项目目前 star 数排名 GitHub 第三,总 star 数超过6w,这个项目整理了所有跟编程相关的免费书籍,而且全球多国语言版的都有,中文版的在这里:free-programming-books-zh,有了这个项目,理论上你可以获取任何编程相关的学习资料,强烈推荐给你们!

俗话说,不会用 shell 的程序员不是真正的程序员,所以建议每个程序员都懂点 shell,有用不说,装逼利器啊!而 oh-my-zsh 毫无疑问就是目前最流行,最酷炫的 shell,不多说了,懂得自然懂,不懂的以后你们会懂的!

GitHub 上有各种 awesome 系列,简单来说就是这个系列搜罗整理了 GitHub 上各领域的资源大汇总,比如有 awesome-android, awesome-ios, awesome-java, awesome-python 等等等,就不截图了,你们自行去感受。

GitHub 的使用有各种技巧,只不过基本的就够我们用了,但是如果你对 GitHub 超级感兴趣,想更多的了解 GitHub 的使用技巧,那么这个项目就刚好是你需要的,每个 GitHub 粉都应该知道这个项目。

这个项目是我一个好朋友 Trinea 整理的一个开源项目,基本囊括了所有 GitHub 上的 Android 优秀开源项目,但是缺点就是太多了不适合快速搜索定位,但是身为 Android 开发无论如何你们应该知道这个项目。

这个项目跟上面的区别是,这个项目只整理了所有跟 Android UI 相关的优秀开源项目,基本你在实际开发终于到的各种效果上面都几乎能找到类似的项目,简直是开发必备。

这个项目是我的邪教群的一位管理员整理的,几乎包括了国内各种学习 Android 的资料,简直太全了,我为这个项目也稍微做了点力,强烈推荐你们收藏起来。

这个就不多说了,之前给大家推荐过的,国内一线互联网公司内部面试题库。

这是一份非常详细的面试资料,涉及 Android、Java、设计模式、算法等等等,你能想到的,你不能想到的基本都包含了,可以说是适应于任何准备面试的 Android 开发者,看完这个之后别说你还不知道怎么面试!

总结

GitHub 上优秀开源项目真的是一大堆,就不一一推荐了,授人以鱼不如授人以渔,请大家自行主动发掘自己需要的开源项目吧,不管是应用在实际项目上,还是对源码的学习,都是提升自己工作效率与技能的很重要的一个渠道,总有一天,你会突然意识到,原来不知不觉你已经走了这么远!

觉得不错,不妨随手转发、点赞,都是对我良心张莫大的鼓励!

最全面的Android热修复技术——Tinker、nuwa、AndFix、Dexposed

这篇文章分为这么几个部分:

  • 一、是什么
  • 二、局限性
  • 三、原理
  • 四、实际案例
  • 五、选择
  • 六、总结

一、热修复技术是什么,怎么出现的呢,为什么需要?

当一个App发布之后,突然发现了一个严重bug需要进行紧急修复,这时候公司各方就会忙得焦头烂额:重新打包App、测试、向各个应用市场和渠道换包、提示用户升级、用户下载、覆盖安装。有时候仅仅是为了修改了一行代码,也要付出巨大的成本进行换包和重新发布。不仅大大增加开发成本也会影响到产品的口碑,造成用户流失。

这时候就提出一个问题:有没有办法以补丁的方式动态修复紧急Bug,不再需要重新发布App,不再需要用户重新下载,覆盖安装?

于是涌现出来很多热补丁方案。

能够让应用能够在无需重新安装的情况实现更新,帮助应用快速建立动态修复能力。

二、局限性与适用场景

  • 补丁只能针对单一客户端版本,随着版本差异变大补丁体积也会增大;
  • 补丁不能支持所有的修改,例如AndroidManifest;
  • 补丁无论对代码还是资源的更新成功率都无法达到100%。

既然补丁技术无法完全代替升级,那它适合使用在哪些场景呢?

1. 轻量而快速的升级

热补丁技术也可以理解为一个动态修改代码与资源的通道,它适合于修改量较少的情况。一般在300k以内,以Android用户的升级习惯,即使是相对活跃的微信也需要10天以上的时间去覆盖50%的用户。使用补丁技术,我们能做到1天覆盖70%以上。这也是基于补丁体积较小,可以直接使用移动网络下载更新。

2.远端调试

一入Android深似海,Android开发的另外一个痛是机型的碎片化。我们也许都会遇到"本地不复现","日志查不出","联系用户不鸟你"的烦恼。所以补丁机制非常适合使用在远端调试上。即我们需要具备只特定用户发送补丁的能力,这对我们查找问题非常有帮助。

3.数据统计

数据统计在微信中也占据着非常重要的位置,我们也非常希望将热补丁与数据统计结合的更好。事实上,热补丁无论在普通的数据统计还是ABTest都有着非常大的优势。例如若我想对同一批用户做两种test, 传统方式无法让这批用户去安装两个版本。使用补丁技术,我们可以方便的对同一批用户不停的更换补丁。

4.其他

事实上,Android官方也使用热补丁技术实现Instant Run。它分为Hot Swap、Warm Swap与Cold Swap三种方式,大家可以参考英文介绍,也可以看参考文章中的翻译稿。最新的Instant App应该也是采用类似的原理,但是Google Play是不允许下发代码的,这个海外App需要注意一下。

三、热修复的原理

1、通过更改dex加载顺序实现热修复

其核心原理就是通过更改含有bug的dex文件的加载顺序。在dex的加载中,若以找到方法则不会继续查找,所以如果能让修复之后的方法在含有bug的方法之前加载就能达到修复bug的目的。

lassLoader

原腾讯空间Android工程师,陈钟老师发明的热补丁方案,是他在看源码的时候偶然发现的切入点。

我们知道,multidex方案的实现,其实就是把多个dex放进app的classloader之中,从而使得所有dex的类都能被找到。而实际上findClass的过程中,如果出现了重复的类,参照下面的类加载的实现,是会使用第一个找到的类的。

public Class findClass(String name, List<Throwable> suppressed) {  

    for (Element element : dexElements) {  //每个Element就是一个dex文件
        DexFile dex = element.dexFile;
        if (dex != null) {
            Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
            if (clazz != null) {
              return clazz;
            }
        }
    }
    if (dexElementsSuppressedExceptions != null) {  
        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
    return null;
}

该热补丁方案就是从这一点出发,只要把有问题的类修复后,放到一个单独的dex,通过反射插入到dexElements数组的最前面,不就可以让虚拟机加载到打完补丁的class了吗。

说到此处,似乎已经是一个完整的方案了,但在实践中,会发现运行加载类的时候报preverified错误,原来在DexPrepare.cpp,将dex转化成odex的过程中,会在DexVerify.cpp进行校验,验证如果直接引用到的类和clazz是否在同一个dex,如果是,则会打上CLASS_ISPREVERIFIED标志。通过在所有类(Application除外,当时还没加载自定义类的代码)的构造函数插入一个对在单独的dex的类的引用,就可以解决这个问题。空间使用了javaassist进行编译时字节码插入。

开源实现有Nuwa, HotFix, DroidFix。

2、通过Native替换方法指针的方式实现热修复

这里主要是阿里开源的两个热修复框架:Dexpost AndFix都是通过Native层使用指针替换的方法替换bug,达到修复bug的目的的,具体可参考其github文章。

Dexposed

基于Xposed的AOP框架,方法级粒度,可以进行AOP编程、插桩、热补丁、SDK hook等功能。

Xposed需要Root权限,是因为它要修改其他应用、系统的行为,而对单个应用来说,其实不需要root。 Xposed通过修改Android Dalvik运行时的Zygote进程,并使用Xposed Bridge来hook方法并注入自己的代码,实现非侵入式的runtime修改。比如蜻蜓fm和喜马拉雅做的事情,其实就很适合这种场景,别人反编译市场下载的代码是看不到patch的行为的。小米(onVmCreated里面还未小米做了资源的处理)也重用了dexposed,去做了很多自定义主题的功能,还有沉浸式状态栏等。

我们知道,应用启动的时候,都会fork zygote进程,装载class和invoke各种初始化方法,Xposed就是在这个过程中,替换了app_process,hook了各种入口级方法(比如handleBindApplication、ServerThread、ActivityThread、ApplicationPackageManager的getResourcesForApplication等),加载XposedBridge.jar提供动态hook基础。

其具体native实现则在Xposed的libxposed_common.cpp里面有注册,根据系统版本分发到libxposed_dalvik和libxposed_art里面,以dalvik为例大致来说就是记录下原来的方法信息,并把方法指针指向我们的hookedMethodCallback,从而实现拦截的目的。

方法级的替换是指,可以在方法前、方法后插入代码,或者直接替换方法。只能针对java方法做拦截,不支持C的方法。

来说说硬伤吧,不支持art,不支持art,不支持art。
重要的事情要说三遍。尽管在6月,项目网站的roadmap就写了7、8月会支持art,但事实是现在还无法解决art的兼容。

另外,如果线上release版本进行了混淆,那写patch也是一件很痛苦的事情,反射+内部类,可能还有包名和内部类的名字冲突,总而言之就是写得很痛苦。

Dexpost:(未测试)
1)原理:在底层虚拟机运行时hoop方法;
2)地址:https://github.com/alibaba/dexposed
3)缺点:适配方面存在一些问题,目前不支持android6.0,5,1;art运行时;
4)优点:无需重启就可以达到修复bug的目的;

AndFix

同样是方法的hook,AndFix不像Dexposed从Method入手,而是以Field为切入点。

先看Java入口,AndFixManager.fix:

/**
 * fix
 *
 * @param file        patch file
 * @param classLoader classloader of class that will be fixed
 * @param classes     classes will be fixed
 */
public synchronized void fix(File file, ClassLoader classLoader, List<String> classes) {
        // 省略...判断是否支持,安全检查,读取补丁的dex文件

        ClassLoader patchClassLoader = new ClassLoader(classLoader) {
            @Override
            protected Class<?> findClass(String className) throws ClassNotFoundException {
                Class<?> clazz = dexFile.loadClass(className, this);
                if (clazz == null && className.startsWith("com.alipay.euler.andfix")) {
                    return Class.forName(className);// annotation’s class not found
                }
                if (clazz == null) {
                    throw new ClassNotFoundException(className);
                }
                return clazz;
            }
        };
        Enumeration<String> entrys = dexFile.entries();
        Class<?> clazz = null;
        while (entrys.hasMoreElements()) {
            String entry = entrys.nextElement();
            if (classes != null && !classes.contains(entry)) {
                continue;// skip, not need fix
            }
      // 找到了,加载补丁class
            clazz = dexFile.loadClass(entry, patchClassLoader);
            if (clazz != null) {
                fixClass(clazz, classLoader);
            }
        }
    } catch (IOException e) {
        Log.e(TAG, "pacth", e);
    }
}

看来最终fix是在fixClass方法:



private void fixClass(Class<?> clazz, ClassLoader classLoader) {
  Method[] methods = clazz.getDeclaredMethods();
  MethodReplace methodReplace;
  String clz;
  String meth;
  // 遍历补丁class里的方法,进行一一替换,annotation则是补丁包工具自动加上的
  for (Method method : methods) {
    methodReplace = method.getAnnotation(MethodReplace.class);
    if (methodReplace == null)
      continue;
    clz = methodReplace.clazz();
    meth = methodReplace.method();
    if (!isEmpty(clz) && !isEmpty(meth)) {
      replaceMethod(classLoader, clz, meth, method);
    }
  }
}

private void replaceMethod(ClassLoader classLoader, String clz, String meth, Method method) {
  try {
    String key = clz + "@" + classLoader.toString();
    Class<?> clazz = mFixedClass.get(key);
    if (clazz == null) {// class not load
      // 要被替换的class
      Class<?> clzz = classLoader.loadClass(clz);
      // 这里也很黑科技,通过C层,改写accessFlags,把需要替换的类的所有方法(Field)改成了public,具体可以看Method结构体
      clazz = AndFix.initTargetClass(clzz);
    }
    if (clazz != null) {// initialize class OK
      mFixedClass.put(key, clazz);
      // 需要被替换的函数
      Method src = clazz.getDeclaredMethod(meth, method.getParameterTypes());
      // 这里是调用了jni,art和dalvik分别执行不同的替换逻辑,在cpp进行实现
      AndFix.addReplaceMethod(src, method);
    }
  } catch (Exception e) {
    Log.e(TAG, "replaceMethod", e);
  }
}

在dalvik和art上,系统的调用不同,但是原理类似,这里我们尝个鲜,以6.0为例art_method_replace_6_0:



// 进行方法的替换
void replace_6_0(JNIEnv* env, jobject src, jobject dest) {
    art::mirror::ArtMethod* smeth = (art::mirror::ArtMethod*) env->FromReflectedMethod(src);
    art::mirror::ArtMethod* dmeth = (art::mirror::ArtMethod*) env->FromReflectedMethod(dest);

    dmeth->declaring_class_->class_loader_ =
            smeth->declaring_class_->class_loader_; //for plugin classloader
    dmeth->declaring_class_->clinit_thread_id_ =
            smeth->declaring_class_->clinit_thread_id_;
    dmeth->declaring_class_->status_ = smeth->declaring_class_->status_-1;

  // 把原方法的各种属性都改成补丁方法的
    smeth->declaring_class_ = dmeth->declaring_class_;
    smeth->dex_cache_resolved_types_ = dmeth->dex_cache_resolved_types_;
    smeth->access_flags_ = dmeth->access_flags_;
    smeth->dex_cache_resolved_methods_ = dmeth->dex_cache_resolved_methods_;
    smeth->dex_code_item_offset_ = dmeth->dex_code_item_offset_;
    smeth->method_index_ = dmeth->method_index_;
    smeth->dex_method_index_ = dmeth->dex_method_index_;

  // 实现的指针也替换为新的
    smeth->ptr_sized_fields_.entry_point_from_interpreter_ =
            dmeth->ptr_sized_fields_.entry_point_from_interpreter_;
    smeth->ptr_sized_fields_.entry_point_from_jni_ =
            dmeth->ptr_sized_fields_.entry_point_from_jni_;
    smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_ =
            dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_;

    LOGD("replace_6_0: %d , %d",
            smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_,
            dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_);
}

// 这就是上面提到的,把方法都改成public的,所以说了解一下jni还是很有必要的,java世界在c世界是有映射关系的
void setFieldFlag_6_0(JNIEnv* env, jobject field) {
    art::mirror::ArtField* artField =
            (art::mirror::ArtField*) env->FromReflectedField(field);
    artField->access_flags_ = artField->access_flags_ & (~0x0002) | 0x0001;
    LOGD("setFieldFlag_6_0: %d ", artField->access_flags_);
}

在dalvik上的实现略有不同,是通过jni bridge来指向补丁的方法。

使用上,直接写一个新的类,会由补丁工具会生成注解,描述其与要打补丁的类和方法的对应关系。

四、实际案例

QQ空间:

空间Android独立版5.2发布后,收到用户反馈,结合版无法跳转到独立版的访客界面,每天都较大的反馈。在以前只能紧急换包,重新发布。成本非常高,也影响用户的口碑。最终决定使用热补丁动态修复技术,向用户下发Patch,在用户无感知的情况下,修复了外网问题,取得非常好的效果。

**解决方案** 该方案基于的是android dex分包方案的,关于dex分包方案,网上有几篇解释了,所以这里就不再赘述,具体可以看这里

简单的概括一下,就是把多个dex文件塞入到app的classloader之中,但是android dex拆包方案中的类是没有重复的,如果classes.dex和classes1.dex中有重复的类,当用到这个重复的类的时候,系统会选择哪个类进行加载呢?

一个ClassLoader可以包含多个dex文件,每个dex文件是一个Element,多个dex文件排列成一个有序的数组dexElements,当找类的时候,会按顺序遍历dex文件,然后从当前遍历的dex文件中找类,如果找类则返回,如果找不到从下一个dex文件继续查找。
理论上,如果在不同的dex中有相同的类存在,那么会优先选择排在前面的dex文件的类,如下图:

20160725234620720
在此基础上,我们构想了热补丁的方案,把有问题的类打包到一个dex(patch.dex)中去,然后把这个dex插入到Elements的最前面,如下图:

20160725234649251

好,该方案基于第二个拆分dex的方案,方案实现如果懂拆分dex的原理的话,大家应该很快就会实现该方案,如果没有拆分dex的项目的话,可以参考一下谷歌的multidex方案实现。然后在插入数组的时候,把补丁包插入到最前面去。

好,看似问题很简单,轻松的搞定了,让我们来试验一下,修改某个类,然后打包成dex,插入到classloader,当加载类的时候出现了一个错误,需要我们打上一个标志 : 如果引用者(也就是ModuleManager)这个类被打上了CLASS_ISPREVERIFIED标志,那么就会进行dex的校验。

这里写图片描述
这段代码是dex转化成odex(dexopt)的代码中的一段,我们知道当一个apk在安装的时候,apk中的classes.dex会被虚拟机(dexopt)优化成odex文件,然后才会拿去执行。

虚拟机在启动的时候,会有许多的启动参数,其中一项就是verify选项,当verify选项被打开的时候,上面doVerify变量为true,那么就会执行dvmVerifyClass进行类的校验,如果dvmVerifyClass校验类成功,那么这个类会被打上CLASS_ISPREVERIFIED的标志

概括一下就是如果以上方法中直接引用到的类(第一层级关系,不会进行递归搜索)和clazz都在同一个dex中的话,那么这个类就会被打上CLASS_ISPREVERIFIED:

20160725235144478

所以为了实现补丁方案,所以必须从这些方法中入手,防止类被打上CLASS_ISPREVERIFIED标志。
最终空间的方案是往所有类的构造函数里面插入了一段代码,代码如下**

if (ClassVerifier.PREVENT_VERIFY) {
    System.out.println(AntilazyLoad.class);
}

20160725235328644

其中AntilazyLoad类会被打包成单独的hack.dex,这样当安装apk的时候,classes.dex内的类都会引用一个在不相同dex中的AntilazyLoad类,这样就防止了类被打上CLASS_ISPREVERIFIED的标志了,只要没被打上这个标志的类都可以进行打补丁操作。

然后在应用启动的时候加载进来.AntilazyLoad类所在的dex包必须被先加载进来,不然AntilazyLoad类会被标记为不存在,即使后续加载了hack.dex包,那么他也是不存在的,这样屏幕就会出现茫茫多的类AntilazyLoad找不到的log。 所以Application作为应用的入口不能插入这段代码。(因为载入hack.dex的代码是在Application中onCreate中执行的,如果在Application的构造函数里面插入了这段代码,那么就是在hack.dex加载之前就使用该类,该类一次找不到,会被永远的打上找不到的标志)

20160725235505545

之所以选择构造函数是因为他不增加方法数,一个类即使没有显式的构造函数,也会有一个隐式的默认构造函数。

空间使用的是在字节码插入代码,而不是源代码插入,使用的是javaassist库来进行字节码插入的。

隐患:
虚拟机在安装期间为类打上CLASS_ISPREVERIFIED标志是为了提高性能的,我们强制防止类被打上标志是否会影响性能?这里我们会做一下更加详细的性能测试.但是在大项目中拆分dex的问题已经比较严重,很多类都没有被打上这个标志。
如何打包补丁包:
1. 空间在正式版本发布的时候,会生成一份缓存文件,里面记录了所有class文件的md5,还有一份mapping混淆文件。
2. 在后续的版本中使用-applymapping选项,应用正式版本的mapping文件,然后计算编译完成后的class文件的md5和正式版本进行比较,把不相同的class文件打包成补丁包。
备注:该方案现在也应用到我们的编译过程当中,编译不需要重新打包dex,只需要把修改过的类的class文件打包成patch dex,然后放到sdcard下,那么就会让改变的代码生效。

微信热补丁方案:

有没有那么一种方案,能做到开发透明,但是却没有QZone方案的缺陷呢?Instant Run的冷插拔与buck的exopackage或许能给我们灵感,它们的思想都是全量替换新的Dex。即我们完全使用了新的Dex,那样既不出现Art地址错乱的问题,在Dalvik也无须插桩。当然考虑到补丁包的体积,我们不能直接将新的Dex放在里面。但我们可以将新旧两个Dex的差异放到补丁包中,最简单我们可以采用BsDiff算法

20160726000830168

简单来说,在编译时通过新旧两个Dex生成差异path.dex。在运行时,将差异patch.dex重新跟原始安装包的旧Dex还原为新的Dex。这个过程可能比较耗费时间与内存,所以我们是单独放在一个后台进程:patch中。为了补丁包尽量的小,微信自研了DexDiff算法,它深度利用Dex的格式来减少差异的大小。它的粒度是Dex格式的每一项,可以充分利用原本Dex的信息,而BsDiff的粒度是文件,AndFix/QZone的粒度为class。

20160726000934706

这块后面我希望后面用单独的文章来讲述,这里先做一个铺垫,大致的效果如下图。在最极端的情况,由于利用了原本dex的信息完全替换一个13M的Dex,我们的补丁大小也仅仅只有6.6M。

但是这套方案并非没有缺点,它带来的问题有两个:

占用Rom体积;这边大约是你修改Dex数量的1.5倍(dexopt与dex压缩成jar)的大小。
一个额外的合成过程;虽然我们单独放在一个进程上处理,但是合成时间的长短与内存消耗也会影响最终的成功率。

微信的热补丁方案叫做Tinker,也算缅怀一下Dota中的地精修补匠,希望能做到无限刷新。

20160726001103473

限于篇幅,这里对Dex、library以及资源的更多技术细节并没有详细的论述,这里希望放在后面的单独文章中。我们最后从整体比较一下这几种方案:

20160726001221420

若不care性能损耗与补丁包大小,QZone方案是最简单且成功率最高的方案(没有单独的合成过程)。相对Tinker来说,它的占用Rom体积也更小。另一方面,QZone与Tinker的成功率大约相差3%左右。

事实上,一个完整的框架应该也是一个容易使用的框架。Tinker对补丁版本管理、进程管理、安全校验等都有着很好的支持。同时我们也支持gradle与命名行两种接入方式。希望在不久的将来,它可以很快的跟大家见面。

五、我的选择

最终我们App的热修复方案选择的是AndFix,原因有三:

(1)AndFix支持android2.3-6.0,所以在机型上的是适配上是没问题的;

(2)AndFix是由阿里开源的,并且持续维护中,目前不少公司已经使用其作为自身App的热修复方案;

(3)通过修改Dex加载顺序的方式实现热修复需要重新启动App,并且相应的开源框架多多少少存在着问题,没有持续的维护;不需要重启App

因此我们最终选择了AndFix作为我们的开源方案。具体的AndFix集成方式可参考github中AndFix的介绍

(1)在App的Application的onCreate方法中执行AndFix的初始化操作;

(2)判断服务器端是否有可更新的热修复差异包

(3)若无则直接退出,若有则下载并执行修复动作

(4)修复完成之后删除下载的补丁差异包

(5)在判断服务器端是否有可更新的补丁包的时候可添加灰度,如版本,渠道,用户等,实现对补丁包定制化的修复

另外需要说明的是:若一个版本中存在着多个bug,则一般的都是让后一个补丁包覆盖前一个补丁包,并删除前一个补丁包,简单来说就是对于每一个版本至多有一个补丁包

最后贴上App端AndFix的实现源码:

public class AndfixManager {
    public static final String TAG = AndfixManager.class.getSimpleName();

    // AndfixManager单例对象
    private static AndfixManager instance = null;
    // 补丁文件名称
    public static final String PATCH_FILENAME = "/patchname.apatch";

    public static PatchManager patchManager = null;
    private AndfixManager() {}

    /**     * 线程安全之懒汉模式实现单例模型     * @return     */
    public static synchronized AndfixManager getInstance() {
        return instance == null ? new AndfixManager() : instance;
    }

    /**     * 执行andfix初始化操作     */
    public static void init(Context mContext) {
        if (mContext == null) {
            L.i("初始化热修复框架,参数错误!!!");
            return;
        }
        patchManager = new PatchManager(mContext);
        // 初始化patch版本,这里初始化的是当前的App版本;
        patchManager.init(VersionUtils.getVersionName(mContext));
        // 加载已经添加到PatchManager中的patch
        patchManager.loadPatch();


        downLoadAndAndPath(mContext);

    }

    /**     * 请求服务器获取补丁文件并加载     */
    public static void downLoadAndAndPath(final Context mContext) {
        // 请求服务器获取差异包
        ExtInterface.GetShContent.Request.Builder request = ExtInterface.GetShContent.Request.newBuilder();

        // 获取本地保存的补丁包版本号
        final String patchVersion = AndfixSp.getPatchVersion(mContext);
        L.i(TAG, "patchVersion:" + patchVersion);
        if (!TextUtils.isEmpty(patchVersion)) {
            request.setShVersion(patchVersion);
        } else {
            request.setShVersion("0");
        }
        NetworkTask task = new NetworkTask(Cmd.CmdCode.GetShContent_SSL_VALUE);
        task.setBusiData(request.build().toByteArray());
        NetworkUtils.executeNetwork(task, new HttpResponse.NetWorkResponse<UUResponseData>() {
            @Override
            public void onSuccessResponse(UUResponseData responseData) {
                if (responseData.getRet() == 0) {
                    try {
                        ExtInterface.GetShContent.Response response = ExtInterface.GetShContent.Response.parseFrom(responseData.getBusiData());
                        // 若返回成功,则更新脚本下载补丁包
                        if (response.getRet() == 0) {
                            ByteString zipDatas = response.getContent();
                            // 数据解压缩
                            byte[] oriDatas = GZipUtils.decompress(zipDatas.toByteArray());
                            String patchFileName = mContext.getCacheDir() + PATCH_FILENAME;
                            L.i(TAG, "patchFileName:" + response.getShVersion());
                            // 将byte数组数据写入文件
                            boolean boolResult = getFileFromBytes(patchFileName, oriDatas);
                            // 写入文件成功则加载
                            if (boolResult) {
                                patchManager.removeAllPatch();
                                patchManager.addPatch(patchFileName);

                                // 保存补丁版本号
                                AndfixSp.putPatchVersion(mContext, response.getShVersion());
                                // 删除补丁文件
                                File files = new File(patchFileName);
                                if (files.exists()) {
                                    files.delete();
                                }
                            }

                        } else {
                            // -1 请求失败
                            // 1 请求成功,但是没有更新版本的脚本
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }

            @Override
            public void onError(VolleyError errorResponse) {
            }

            @Override
            public void networkFinish() {
            }
        });

    }


    /**     * 根据数组获取文件     * @param path     * @param oriDatas     */
    public static boolean getFileFromBytes(String path, byte[] oriDatas) {
        boolean result = false;
        if (TextUtils.isEmpty(path)) {
            return result;
        }
        if (oriDatas == null || oriDatas.length == 0) {
            return result;
        }

        try {
            FileOutputStream fos = new FileOutputStream(path);
            fos.write(oriDatas);
            fos.close();
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

    }
}            

总结:

android的热修复原理大体上分为两种,其一是通过dex的执行顺序实现Apk热修复的功能,但是其需要将App重启才能生效,其二是通过Native修改函数指针的方式实现热修复。

图解AndFix原理:

20160726005949549

使用apkPatch 工具进行差分生成补丁

具体语法:

apkpatch.bat -f version2-fixed.apk -t version1.apk -o patch -k itheima.jks -p
testtest -a it -e testtest
具体含义
-f 新版本apk 即修复后的apk
-t 旧版本apk 即修复前的apk
-o 差分包即patch 补丁文件存放的文件夹当前为patch 文件夹
-k 证书eclipse 下为.keystore 而在androidstudio 下是.jks 实验证明可以混用
-p 使用密码当前是testest
-a 别名当前是it
-e 别名对应的密码当前是testest

Android热修复技术选型——HotFix、AndFix、Tinker、nuwa

2015年以来,Android开发领域里对热修复技术的讨论和分享越来越多,同时也出现了一些不同的解决方案,如QQ空间补丁方案、阿里AndFix以及微信Tinker,它们在原理各有不同,适用场景各异,到底采用哪种方案,是开发者比较头疼的问题。本文希望通过介绍QQ空间补丁、Tinker以及基于AndFix的阿里百川HotFix技术的原理分析和横向比较,帮助开发者更深入了解热修复方案。

技术背景

一、正常开发流程

21

从流程来看,传统的开发流程存在很多弊端:

  • 重新发布版本代价太大

  • 用户下载安装成本太高
  • BUG修复不及时,用户体验太差

二、热修复开发流程

72

而热修复的开发流程显得更加灵活,优势很多:

  • 无需重新发版,实时高效热修复
  • 用户无感知修复,无需下载新的应用,代价小
  • 修复成功率高,把损失降到最低

业界热门的热修复技术

热修复作为当下热门的技术,在业界内比较著名的有阿里巴巴的AndFix、Dexposed,腾讯QQ空间的超级补丁技术和微信的Tinker。最近阿里百川推出的HotFix热修复服务就基于AndFix技术,定位于线上紧急BUG的即时修复,所以AndFix技术这块我们重点分析阿里百川HotFix。下面,我们就分别介绍QQ空间超级热补丁技术和微信的Tinker以及阿里百川HotFix技术。

一、QQ空间超级补丁技术

超级补丁技术基于DEX分包方案,使用了多DEX加载的原理,大致的过程就是:把BUG方法修复以后,放到一个单独的DEX里,插入到dexElements数组的最前面,让虚拟机去加载修复完后的方法。

53

当patch.dex中包含Test.class时就会优先加载,在后续的DEX中遇到Test.class的话就会直接返回而不去加载,这样就达到了修复的目的。

但是有一个问题是,当两个调用关系的类不在同一个DEX时,就会产生异常报错。我们知道,在APK安装时,虚拟机需要将classes.dex优化成odex文件,然后才会执行。在这个过程中,会进行类的verify操作,如果调用关系的类都在同一个DEX中的话就会被打上CLASS_ISPREVERIFIED的标志,然后才会写入odex文件。

所以,为了可以正常的进行打补丁修复,必须避免类被打上CLASS_ISPREVERIFIED标志,具体的做法就是单独放一个类在另外DEX中,让其他类调用。

我们来逆向手机QQ空间APK看一下具体的实现:

先进入程序入口QZoneRealApplication,在attachBaseContext中进行了两步操作:修复CLASS_ISPREVERIFIED标志导致的unexpected DEX problem异常、加载修复的DEX。

24

1. 修复unexpectedDEX problem异常

先看代码:

25

可以看到,这里是要加载一个libs目录下的dalvikhack.jar。在项目的assets/libs找到该文件,解压得到classes.dex文件,逆向打开该DEX文件,

124

通过不同的DEX加载进来,然后在每一个类的构造方法中引用其他DEX中的唯一类AnitLazyLoad,避免类被打上CLASS_ISPREVERIFIED标志。

17

在无修复的情况下,将DO_VERIFY_CLASSES设置为false,提高性能。只有在需要修复的时候,才设置为true。

8

至于如何加载进来,与接下来第二个步骤基本相同。

2. 加载修复的DEX

从loadPatchDex()方法进入,经过几次跳转,到达核心的代码段,SystemClassLoaderInjector.c()。由于进行了混淆和多次方法的跳转,于是将核心代码段做了如下整理:

29

修复的步骤为:

1. 可以看出是通过获取到当前应用的Classloader,即为BaseDexClassloader

2. 通过反射获取到他的DexPathList属性对象pathList

3. 通过反射调用pathList的dexElements方法把patch.dex转化为Element[]

4. 两个Element[]进行合并,把patch.dex放到最前面去

5. 加载Element[],达到修复目的

整体的流程图如下:

110

从流程图来看,可以很明显的找到这种方式的特点:

优势:

  1. 没有合成整包(和微信Tinker比起来),产物比较小,比较灵活
  2. 可以实现类替换,兼容性高。(某些三星手机不起作用)

不足:

1. 不支持即时生效,必须通过重启才能生效。

2. 为了实现修复这个过程,必须在应用中加入两个DEX!dalvikhack.dex中只有一个类,对性能影响不大,但是对于patch.dex来说,修复的类到了一定数量,就需要花不少的时间加载。对手淘这种航母级应用来说,启动耗时增加2s以上是不能够接受的事。

3. 在ART模式下,如果类修改了结构,就会出现内存错乱的问题。为了解决这个问题,就必须把所有相关的调用类、父类子类等等全部加载到patch.dex中,导致补丁包异常的大,进一步增加应用启动加载的时候,耗时更加严重。

二、微信Tinker

微信针对QQ空间超级补丁技术的不足提出了一个提供DEX差量包,整体替换DEX的方案。主要的原理是与QQ空间超级补丁技术基本相同,区别在于不再将patch.dex增加到elements数组中,而是差量的方式给出patch.dex,然后将patch.dex与应用的classes.dex合并,然后整体替换掉旧的DEX,达到修复的目的。

211

我们来逆向微信APK看一下具体的实现:

先找到应用入口TinkerApplication,在onBaseContextAttached()调用了loadTinker(),

112

进入TinkerLoader的tryLoad()方法中,

1122

从方法名可以预见,在tryLoadPatchFilesInternal()中尝试加载本地的补丁,再经过跳转进入核心修复功能类SystemClassLoaderAdder.class中。

123

代码中可以看出,根据Android版本的不同,分别采取具体的修复操作,不过原理都是一样的。我们以V19为例:

213

从代码中可以看到,通过反射操作得到PathClassLoader的DexPatchList,反射调用patchlist的makeDexElements()方法吧本地的DEX文件直接替换到Element[]数组中去,达到修复的目的。

对于如何进行patch.dex与classes.dex的合并操作,这里微信开启了一个新的进程,开启新进程的服务TinkerPatchService进行合并。

114

整体的流程如下:

115

从流程图来看,同样可以很明显的找到这种方式的特点:

优势:

  1. 合成整包,不用在构造函数插入代码,防止verify,verify和opt在编译期间就已经完成,不会在运行期间进行
  2. 性能提高。兼容性和稳定性比较高。
  3. 开发者透明,不需要对包进行额外处理。

不足:

  1. 与超级补丁技术一样,不支持即时生效,必须通过重启应用的方式才能生效。
  2. 需要给应用开启新的进程才能进行合并,并且很容易因为内存消耗等原因合并失败。
  3. 合并时占用额外磁盘空间,对于多DEX的应用来说,如果修改了多个DEX文件,就需要下发多个patch.dex与对应的classes.dex进行合并操作时这种情况会更严重,因此合并过程的失败率也会更高。

三、阿里百川HotFix

阿里百川推出的热修复HotFix服务,相对于QQ空间超级补丁技术和微信Tinker来说,定位于紧急bug修复的场景下,能够最及时的修复bug,下拉补丁立即生效无需等待。

16

1、AndFix实现原理

AndFix不同于QQ空间超级补丁技术和微信Tinker通过增加或替换整个DEX的方案,提供了一种运行时在Native修改Filed指针的方式,实现方法的替换,达到即时生效无需重启,对应用无性能消耗的目的。

原理图如下:

117

2、AndFix实现过程

对于实现方法的替换,需要在Native层操作,经过三个步骤:

18

接下来以Dalvik设备为例,来分析具体的实现过程:

2.1 setup()

119

对于Dalvik来说,遵循JIT即时编译机制,需要在运行时装载libdvm.so动态库,获取以下内部函数:

1) dvmThreadSelf( ):查询当前的线程;

2)dvmDecodeIndirectRef():根据当前线程获得ClassObject对象。

2.2 setFieldFlag

020

该操作的目的:让private、protected的方法和字段可被动态库看见并识别。原因在于动态库会忽略非public属性的字段和方法。

2.3 replaceMethod

021

该步骤是方法替换的核心,替换的流程如下:

022

AndFix对ART设备同样支持,具体的过程与Dalvik相似,这里不再赘述。

从技术原理,不难看出阿里百川HotFix的几个特点:

最大的优势在于

  1. BUG修复的即时性
  2. 补丁包同样采用差量技术,生成的PATCH体积小
  3. 对应用无侵入,几乎无性能损耗

不足:

  1. 不支持新增字段,以及修改<init>方法,也不支持对资源的替换。
  2. 由于厂商的自定义ROM,对少数机型暂不支持。

综合分析如下:

023

热修复的坑和解

我们可以看到,QQ空间超级补丁技术和微信Tinker的修复原理都基于类加载,在功能上已经支持类、资源的替换和新增,功能非常强大。既然已经有了这么强大的热修复技术,为什么阿里百川还要推出自己的热修复方案HotFix呢?

一、多DEX带来的性能问题和影响

我们知道,多DEX方案用来解决应用方法数65k的问题,现在Google也官方支持了MultiDex的实现方案。但是,这实在是应用因方法数超出而作出的不得已的下策,但是超级补丁技术和Tinker作为一种热修复的方案,平生给应用增加了多个DEX,而多DEX技术最大的问题在于性能上的坑,因此基于这种方案的补丁技术影响应用的性能是无疑的。

1. 启动加载时间过长

我们可以看到,超级补丁技术和Tinker都选择在Application的attachBaseContext()进行补丁DEX的加载,即使这是加载dex的最佳时机,但是依然会带来很大的性能问题,首当其冲的就是启动时间太长。

对于补丁DEX来说,应用启动时虚拟机会进行dexopt操作,将patch.dex文件转换成odex文件,这个过程非常耗时。而这个过程,又要求需要在主线程中,以同步的方式执行,否则无法成功进行修复。就DEX的加载时间,大概做了以下的时间测试。

024

随着patch.dex的增加,在不做任何优化的情况下,启动时间也直线增长。对于一个应用来说,这简直是灾难性的。

2. 易造成应用的ANR和Crash

正是尤其多DEX加载导致了启动时间过长,很容易就会引发应用的ANR。我们知道当应用在主线程等待超过5s以后,就会直接导致长时间无响应而退出。超级补丁技术为保证ART不出现地址错乱问题,需要将所有关联的类全部加入到补丁中,而微信Tinker采取一种差量包合并加载的方式,都会使要加载的DEX体积变得很大。这也很大程度上容易导致ANR情况的出现。

除了应用ANR以外,多DEX模式也同样很容易导致Crash情况的出现。我们知道,超级补丁技术为了保证ART设备下不出现地址错乱问题,需要把修改类的所有相关类全部加入到补丁中,这里会出现一个问题,为了保证补丁包的体积最小,能否保证引入全部的关联类而不引入无关的类呢?一旦没有引入关联的类,就会出现以下的异常:

  • NoClassDefFoundError
  • Could not find class
  • Could not find method

出现这些异常,就会直接导致应用的Crash退出。

所以,不难看出如果我们需要修复一个不是Crash的BUG,但是因为未加入相关类而导致了更严重的Crash,就更加的得不偿失。

总的来说,热修复本质的目的是为了保证应用更加稳定,而不是为了更强大的功能引入更大的风险和不稳定性。

二、热修复 or 插件化?

我们经常提到热修复和插件化,这都是当下热门的新兴技术。在讲述之前,需要对这两个概念进行一下解释。

  • 插件化:一个程序划分为不同的部分,以插件的形式加载到应用中去,本质上它使用的技术还是热修复技术,只是加入了更多工程实践,让它支持大规模的代码更新以及资源和SO包的更新。
  • 热修复:当线上应用出现紧急BUG,为了避免重新发版,并且保证修复的及时性而进行的一项在线推送补丁的修复方案。

显然,从概念上我们可以看到,插件化使用场景更多是功能,热修复使用常见在于修复。从这个层面来说,插件化必然功能更加强大,能做的事情也更多。QQ空间超级补丁技术和微信Tinker从类、资源的替换和更新上来看,与其说是热修复,不如说是插件化。

当然,强大的功能也就增加了不稳定的因素。比如上文提到的增加启动时间,导致ANR、Crash的问题。

QQ空间超级补丁技术和微信Tinker提供了更加强大的功能,但是对应用的性能和稳定有较大的影响,就BUG修复的这个使用场景上还不够明确,并且显得过重。

针对应用的性能损耗,我们可以举例做一个对比。

某APP的启动载入时间为3s左右,本身就是基于多DEX模式的实现。

分别接入三种热修复服务,根据腾讯提供超级补丁技术和Tinker的数据,那么会变成以下的场景:

  1. 阿里百川HotFix:启动时间几乎无增加,不增加运行期额外的磁盘消耗。
  2. QQ空间超级补丁技术:如果应用有700个类,启动耗时增加超过2.5s,达到5.5s以上。
  3. 微信Tinker:假设应用有5个DEX文件,分别修改了这5个DEX,产生5个patch.dex文件,就要进行5次的patch合并动作,假设每个补丁1M,那么就要多占用7.5M的磁盘空间。

显然对于修复紧急BUG这个场景,阿里百川HotFix的更为合适,它更加轻量,可以在不重启的情况下生效,且对性能几乎没有影响。微信Tinker、QQ空间超级补丁技术更多地把场景定位在发布小的新功能上,采用ClassLoader的模式,牺牲较高的性能代价去实现类、资源新增或替换的功能。阿里百川HotFix对应用本身做到无侵入,无性能损耗。

总结

QQ空间超级补丁技术和微信Tinker 支持新增类和资源的替换,在一些功能化的更新上更为强大,但对应用的性能和稳定会有的一定的影响;阿里百川HotFix虽然暂时不支持新增类和资源的替换,对新功能的发布也有所限制,但是作为一项定位为线上紧急BUG的热修复的服务来说,能够真正做到BUG即时修复用户无感知,同时保证对应用性能不产生不必要的损耗,在热修复方面不失为一个好的选择。

目前阿里百川HotFix已经开始公测,点击立即使用,即可开始你的热修复之旅。

iOS 10 适配 ATS(app支持https通过App Store审核)

iOS 10 适配 ATS

一. HTTPS

其实HTTPS从最终的数据解析的角度,与HTTP没有任何的区别,HTTPS就是将HTTP协议数据包放到SSL/TSL层加密后,在TCP/IP层组成IP数据报去传输,以此保证传输数据的安全;而对于接收端,在SSL/TSL将接收的数据包解密之后,将数据传给HTTP协议层,就是普通的HTTP数据。HTTP和SSL/TSL都处于OSI模型的应用层。从HTTP切换到HTTPS是一个非常简单的过程,在做具体的切换操作之前,我们需要了解几个概念:

SSL/TLS

为了保证这些隐私数据能加密传输,于是网景公司设计了SSL(Secure Sockets Layer)协议用于对HTTP协议传输的数据进行加密,从而就诞生了HTTPS。SSL目前的版本是3.0,被IETF(Internet Engineering Task Force)定义在RFC 6101中,之后IETF对SSL 3.0进行了升级,于是出现了TLS(Transport Layer Security) 1.0,定义在RFC 2246。实际上我们现在的HTTPS都是用的TLS协议,但是由于SSL出现的时间比较早,并且依旧被现在浏览器所支持,因此SSL依然是HTTPS的代名词,但无论是TLS还是SSL都是上个世纪的事情,SSL最后一个版本是3.0,今后TLS将会继承SSL优良血统继续为我们进行加密服务。

简单的来说,SSL/TSL通过四次握手。SSL协议的工作流程:

服务器认证阶段:

客户端向服务器发送一个开始信息“Hello”以便开始一个新的会话连接;
服务器根据客户的信息确定是否需要生成新的主密钥,如需要则服务器在响应客户的“Hello”信息时将包含生成主密钥所需的信息;
客户根据收到的服务器响应信息,产生一个主密钥,并用服务器的公开密钥加密后传给服务器;
服务器恢复该主密钥,并返回给客户一个用主密钥认证的信息,以此让客户认证服务器。

用户认证阶段:

在此之前,服务器已经通过了客户认证,这一阶段主要完成对客户的认证。
经认证的服务器发送一个提问给客户,客户则返回(数字)签名后的提问和其公开密钥,从而向服务器提供认证。

二. App Transport Security

iOS9中新增App Transport Security(简称ATS)特性, 主要使到原来请求的时候用到的HTTP,都转向TLS1.2协议进行传输。这也意味着所有的HTTP协议都强制使用了HTTPS协议进行传输。在 iOS 9 和 OS X 10.11 中,默认情况下非 HTTPS 的网络访问是被禁止的。当然,因为这样的推进影响面非常广,作为缓冲,我们可以在 Info.plist 中添加 NSAppTransportSecurity 字典并且将 NSAllowsArbitraryLoads 设置为 YES 来禁用 ATS。

不过,WWDC 16 中,Apple 表示将继续在 iOS 10 和 macOS 10.12 里收紧对普通 HTTP 的访问限制。从 2017 年 1 月 1 日起,所有的新提交 app 默认是不允许使用 NSAllowsArbitraryLoads 来绕过 ATS 限制的,也就是说,我们最好保证 app 的所有网络请求都是 HTTPS 加密的,否则可能会在应用审核时遇到麻烦。

三. iOS10 NSAppTransportSecurity

通过在info.plist中配置这个键,开发者可以自定义网络安全策略。例如:

允许针对个别服务器的不安全访问。
允许不安全的 web 或媒体内容访问,但不影响整个app的ATS策略。
启用新的安全特性,例如Certificate Transparency。
对NSAppTransportSecurity的支持自 iOS9.0,OS X v10.11 开始,适用于 app 和 app extension。

自 iOS10.0,macOS 10.12 开始,增加了对下列子键的支持:

  • NSAllowsArbitraryLoadsInMedia
  • NSAllowsArbitraryLoadsInWebContent
  • NSRequiresCertificateTransparency
  • NSAllowsLocalNetworking

ATS Configuration Basics / ATS 配置基础知识

对于使用 iOS9.0, OS X v10.11 SDK 及以上的 app 来说,ATS(App Transport Security)默认开启,NSAllowsArbitraryLoads是字典NSAppTransportSecurity的根键,默认值NO。

在启用 ATS 的情况下,所有的 HTTP 请求必须为 HTTPS(RFC 2818) 连接。任何不安全的 HTTP 请求都将失败。ATS 使用 TLS(Transport Layer Security)v1.2(RFC 5246)。

下面是字典NSAppTransportSecurity的总体结构,所有键都是非必填项:

NSAppTransportSecurity : Dictionary {
    NSAllowsArbitraryLoads : Boolean
    NSAllowsArbitraryLoadsInMedia : Boolean
    NSAllowsArbitraryLoadsInWebContent : Boolean
    NSAllowsLocalNetworking : Boolean
    NSExceptionDomains : Dictionary {
        <domain-name-string> : Dictionary {
            NSIncludesSubdomains : Boolean
            NSExceptionAllowsInsecureHTTPLoads : Boolean
            NSExceptionMinimumTLSVersion : String
            NSExceptionRequiresForwardSecrecy : Boolean   // Default value is YES
            NSRequiresCertificateTransparency : Boolean
        }
    }
}

可以看出,所有键可以分为两类:主键,这些键用来定义 app 的总体 ATS 策略;子键,即NSExceptionDomains下面的键,使用这些键针对某个域名单独配置。

主键包括:

  • NSAllowsArbitraryLoads

    设置为 YES,解除整个 app 的 ATS 限制;但是,通过NSExceptionDomains进 行的配置依然有效。默认值为 NO。
    注意:设置为 YES,会引发 App Stroe 的审查,开发者必须说明原因。

  • NSAllowsArbitraryLoadsInMedia

    设置为 YES,解除通过 AV Foundation 框架访问媒体内容时的 ATS 限制;启用这个 键,务必确保载入的媒体内容已经被加密,例如受FairPlay保护的文件,或者是安全的 HLS流媒,其中不包含敏感的个人信息。默认为 NO。

  • NSAllowsArbitraryLoadsInWebContent

    设置为 YES,解除通过 web view 发出的网络请求的 ATS 限制。启用这个键,可以使 app 访问任意网页内容,但不影响 app 的总体 ATS 策略。此键值默认为 NO。

  • NSAllowsLocalNetworking

    设置为 YES,使得 app 可以载入任意本地资源,但不影响 app 的总体 ATS 策略。默 认为 NO。

  • NSExceptionDomains

    为一个或多个域名单独配置 ATS。
    被单独配置的域名,默认受到完全的 ATS 限制,不管NSAllowsArbitraryLoads的值 如何;需要通过子键,进一步配置

所有的子键都属于NSExceptionDomain。向Info.plist中添加这一主键:

  • 创建字典,针对一个或多个域名,以便进行 ATS 配置。
  • 这意味着之前使用主键所做的设置,对于这个域名来说,已经无效。

例如,及时之前设置NSAllowsArbitraryLoadsInMedia为 YES,然而NSExceptionDomain所代表的域名依然不能访问不安全的媒体内容。

基于这样的设定,可以针对域名进行 ATS 配置,增加或减少安全措施。例如:

  • 将NSExceptionAllowsInsecureHTTPLoads设置为 YES,就 ;这样做会引发 App Store 的审查,详情见App Store Review for ATS。
  • 通过配置NSExceptionRequiresForwardSecrecy为 NO,取消正向保密。
  • 通过配置NSExceptionMinimumTLSVersion,更改 TLS 最低版本。

NSExceptionDomains字典构成:

  • <域名字符串>
    代表想要配置的特定域名。可以添加多个域名(即添加多个这样的键),为它们统一配置 ATS 策略。这个键对应一个字典,包含以下子键:

    • NSIncludesSubdomains
      * 设置为 YES,当前域名的 ATS 策略适用于其所有子域名。默认为 NO
    • NSExceptionAllowsInsecureHTTPLoads
      * 设置为 YES,可以同时通过 HTTP 和 HTTPS 访问当前域名。默认为 NO。
      注意,配置这个键值,将引发 App Store 的审查,开发者必须说明原因。
    • NSExceptionMinimumTLSVersion
      * 指定 TLS 的最低版本,因此可以使用版本较低,有安全漏洞的 TLS 协议。
      注意,配置这个键值,将引发 App Store 的审查,开发者必须说明原因。
    • NSExceptionRequiresForwardSecrecy
      * 设置为 NO,允许针对当前域名使用不支持正向保密的 TLS 加密算法。默认为 YES
    • NSRequiresCertificateTransparency
      * 设置为 YES,将验证域名服务器证书的Certificate Transparency时间戳 。默认为 NO

Requirements for Connecting Using ATS / 使用 ATS 的前提条件

在 ATS 完全开启的情况下,系统要求 app 的 HTTPS 连接必须满足以下要求:

X.509 数字证书必须满足下列标准中的一项:

  • 由操作系统内嵌的根证书颁发机构签发
    • 由通过操作系统管理员或用户主动安装的根证书颁发机构签发
      • TLS 版本必须为1.2,任何不使用或使用较低版本 TLS / SSL 的连接,都将失败。
  • 连接必须使用 AES-128 或 AES-256 对称加密算法。 TLS 算法套装必须以 ECDSA 密钥交换的形式支持正向保密,加密算法必须为下面之一:
    • TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
    • TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
    • TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384
    • TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA
    • TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256
    • TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA
    • TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
    • TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
    • TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384
    • TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256
    • TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
  • 服务端的叶证书签名密钥必须为下面之一:
    • 至少2048位的 RSA 密钥
    • 至少256位的 ECC 密钥
    • 此外,服务器证书的哈希算法必须为 SHA-2,其摘要长度至少位256位(即 SHA-256 及以上)。
      上面的标准,未来可能会发生变化。但不会影响到 app 二进制包的兼容性。

App Store Review for ATS / App Store 对于 ATS 相关项的审核

某些对 ATS 的配置会引发 App Store 的审核,开发者必须说明原因。这些键有:

  • NSAllowsArbitraryLoads
  • NSExceptionAllowsInsecureHTTPLoads
  • NSExceptionMinimumTLSVersion

以下是一些原因说明例子,供参考:

  • 必须连接由其他机构控制的服务器,其还不支持安全连接。
  • 必须支持那些还未升级至可使用安全连接,不得不通过公共域名访问网络的设备。
  • 必须通过 web 展示来源不一的各种网络内容,但又不能完全使用NSAllowsArbitraryLoadsInWebContent所管理的类。

向 App Store 提交审核时,开发者应主动提供足够的信息,以便解释 app 无法使用安全连接的原因。

四. 实现支持安全ATS策略

<font color=red size=5>ATS相关设置对iOS8及以下系统无效</font>

需要解决的问题(iOS 9、iOS10及以上)

1、app内服务器网络请求访问支持https(即一般的请求)

2、webview内支持任意http访问

3、第三方sdk接入与支持http访问

主要是围绕info.pilst配置文件作相关的安全ATS策略

Info.plist文件是向操作系统描述应用程序的XML属性列表,是iPhone应用程序文件夹包含所有重要的Info.plist文件

你可能注意到一些关键字像是使用了一些其他关键字中的词但是在前面加上了”ThirdParty”字样,在功能上,这些关键字与不含有”ThirdParty”的关键字有同样的效果。而且实际运行中所调用的代码将会完全忽略是否使用”ThirdParty”关键字。

简单粗暴的方案:

--------------------------------------------

NSExceptionDomains 的设置方法如下, 比如我们要将 weibo.com 这个域名排除在 ATS 验证之外,就可以这样:

key>NSAppTransportSecurity</key>
<dict>
    <key>NSExceptionDomains</key>
    <dict>
        <key>weibo.com</key>
        <dict>
            <key> NSIncludesSubdomains </key>
            <true/>
            <key> NSExceptionRequiresForwardSecrecy </key>
            <false/>
            <key>NSExceptionAllowsInsecureHTTPLoads</key>
            <true/>
        </dict>
    </dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
</dict>

注意:每个需添加的域都需要设置此三个属性。如果请求的网络图片是HTTP,也是需要设置的图片的域。

注意⚠️这个方案风险较大,有可能被拒绝。“需要访问的域名是第三方服务器,他们没有进行 HTTPS 对应”会是审核时的一个可选理由,但是这应该只需要针对特定域名,而非全面开放。如果访问的是自己的服务器的话,可能这个理由会无法通过。

------------------------------------------------

实现方案

1、app内服务器网络请求访问支持https

解决方案:

搭建https服务器

搭建https服务器需要ssl证书

  1. ssl自制证书:称自签名ssl证书,容易被假冒伪造,浏览器不信任。(审核不通过)
  2. 免费CA证书:部分CA机构提供免费的SSL证书,如wosign,starts等(App Store pass掉不通过)
  3. 付费CA证书:多指企业级及以上的数字证书。

HTTPS服务器满足ATS默认的条件,而且SSL证书是通过权威的CA机构认证过的,那么我们在使用Xcode开发的时候,对网络的适配什么都不用做,我们也能正常与服务器通信。但是,当我们对安全性有更高的要求时或者我们自建证书时,我们需要本地导入证书来进行验证。

使用AFNetworking来支持https

AFNetworking是iOS/OSX开发最流行的第三方开源库之一,现在iOS oc 代码90%以上都是用这个框架网络请求。AFNetworking已经将上面的逻辑代码封装好,甚至更完善,在AFSecurityPolicy文件中,有兴趣可以阅读这个模块的代码;以下就是在AFNetworking 2.6.0以前版本和3.0.0版本基于支持https的验证方式

步骤:

  1. 新建一个manager
  2. 在mainBundle中寻找我们刚才拖进项目中的https.cer, 并且将相关的数据读取出来
  3. 新建一个AFSecurityPolicy,并进行相应的配置
  4. 将这个AFSecurityPolicy 实例赋值给manager

代码实现:

NSURL * url = [NSURL URLWithString:@"https://www.google.com"];
AFHTTPRequestOperationManager * requestOperationManager = [[AFHTTPRequestOperationManager alloc] initWithBaseURL:url];
dispatch_queue_t requestQueue = dispatch_create_serial_queue_for_name("kRequestCompletionQueue");
requestOperationManager.completionQueue = requestQueue;

AFSecurityPolicy * securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];

//allowInvalidCertificates 是否允许无效证书(也就是自建的证书),默认为NO
//如果是需要验证自建证书,需要设置为YES
securityPolicy.allowInvalidCertificates = YES;

//validatesDomainName 是否需要验证域名,默认为YES;
//假如证书的域名与你请求的域名不一致,需把该项设置为NO;如设成NO的话,即服务器使用其他可信任机构颁发的证书,也可以建立连接,这个非常危险,建议打开。
//置为NO,主要用于这种情况:客户端请求的是子域名,而证书上的是另外一个域名。因为SSL证书上的域名是独立的,假如证书上注册的域名是www.google.com,那么mail.google.com是无法验证通过的;当然,有钱可以注册通配符的域名*.google.com,但这个还是比较贵的。
//如置为NO,建议自己添加对应域名的校验逻辑。
securityPolicy.validatesDomainName = YES;
//validatesCertificateChain 是否验证整个证书链,默认为YES
//设置为YES,会将服务器返回的Trust Object上的证书链与本地导入的证书进行对比,这就意味着,假如你的证书链是这样的:
//GeoTrust Global CA 
//    Google Internet Authority G2
//        *.google.com
//那么,除了导入*.google.com之外,还需要导入证书链上所有的CA证书(GeoTrust Global CA, Google Internet Authority G2);
//如是自建证书的时候,可以设置为YES,增强安全性;假如是信任的CA所签发的证书,则建议关闭该验证,因为整个证书链一一比对是完全没有必要(请查看源代码);
securityPolicy.validatesCertificateChain = NO;

requestOperationManager.securityPolicy = securityPolicy;

另afnetworking 3.0.0以上版本用的是AFHTTPSessionManager

 AFHTTPSessionManager * manager = [AFHTTPSessionManager manager];
    NSString * cerPath = [[NSBundle mainBundle] pathForResource:@"server" ofType:@"cer"];
    NSData * cerData = [NSData dataWithContentsOfFile:cerPath];
    NSLog(@"%@", cerData);
    manager.securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate withPinnedCertificates:[[NSArray alloc] initWithObjects:cerData, nil]];
    manager.securityPolicy.allowInvalidCertificates = YES;
    [manager.securityPolicy setValidatesDomainName:NO];
    manager.requestSerializer = [AFJSONRequestSerializer serializer];
    manager.responseSerializer = [AFJSONResponseSerializer serializer];
    NSDictionary * parameter = @{@"username":self.username, @"password":self.password};
    [manager POST:@"https://192.168.1.4:9777" parameters:parameter success:^(NSURLSessionDataTask * task, id responseObject) {
        NSLog(@"success %@", responseObject);
        }
        failure:^(NSURLSessionDataTask * task, NSError * error) {
            NSLog(@"failure %@", error);
        }]
    <key>NSAppTransportSecurity</key>
    <dict>
        <key>NSAllowsArbitraryLoads</key> //设置为 YES,解除整个app的ATS限制;但是通过NSExceptionDomains进行的配置依然有效
        <false/>
        <key>NSAllowsArbitraryLoadsInMedia</key> //设置为 YES,解除通过AVFoundation框架访问媒体内容时的 ATS 限制
        <true/>
        <key>NSAllowsArbitraryLoadsInWebContent</key> //设置为 YES,解除通过webview发出的网络请求的ATS限制
        <true/>
        <key>NSAllowsLocalNetworking</key> //设置为 YES,使得app可以载入任意本地资源,但不影响app的总体 ATS 策略
        <true/>

2、webview内支持任意http访问

对于网页浏览和视频播放的行为,iOS 10 中新加入了 NSAllowsArbitraryLoadsInWebContent 键。通过将它设置为 YES,可以让 app 中的 WKWebView 和使用 AVFoundation 播放的在线视频不受 ATS 的限制。这也应该是绝大多数使用了相关特性的 app 的选择。但是坏消息是这个键在 iOS 9 中并不会起作用。

如果app只支持 iOS 10,并且有用户可以自由输入网址进行浏览的功能,或者是在线视频音频播放功能的话,简单地加入 NSAllowsArbitraryLoadsInWebContent,并且将组件换成 WKWebKit 或者 AVFoundation 就可以了。如果你还需要支持 iOS 9,并且需要访问网页和视频的话,可能只能去开启 NSAllowsArbitraryLoads 然后提交时进行说明,并且看 Apple 审核员决定让不让通过了。

另外,当 NSAllowsArbitraryLoads 和 NSAllowsArbitraryLoadsInWebContent 同时存在时,根据系统不同,表现的行为也会不一样。简单说,iOS 9 只看 NSAllowsArbitraryLoads,而 iOS 10 会先看 NSAllowsArbitraryLoadsInWebContent。在 iOS 10 中,要是 NSAllowsArbitraryLoadsInWebContent 存在的话,就忽略掉 NSAllowsArbitraryLoads,如果它不存在,则遵循 NSAllowsArbitraryLoads 的设定

UIWebView 在 NSAllowsArbitraryLoadsInWebContent 为 YES 时访问 HTTP,无效。WKWebView 在 NSAllowsArbitraryLoadsInWebContent 为 YES 时在iOS 10 中访问 HTTP,有效,iOS 9无效。如果用WkWebView替换UIWebView,iOS 7 将无法使用WkWebView,可做适配加载,没有特殊的什么需求的话,尽早将 UIWebView 全部换为 WkWebView 会比较好。所以为了能让WebView在所有版本都能访问非https内容,只能把NSAllowsArbitraryLoads设置为YES。

解决方案一:

开启 NSAllowsArbitraryLoads 为 YES,然后提交时进行说明

解决方案二:

设置 NSExceptionDomains 属性来访问指定域名,然后提交时进行说明

3、第三方sdk接入与支持http访问

但是按照国内的现状,关闭这个限制也许是更实际的做法。至于原因就太多了,第三方SDK(几乎都是访问http),合作伙伴接入(不能要求它们一定要支持https)

第三方sdk,同样需要遵守ATS规则,即第三方sdk也有被ATS过滤的风险,微信,qq,分享,登陆功能都能正常,微博登陆不能正常通过。另在网上找到了一些可能存在有问题的sdk,目前已知的有:

  • 友盟 (已经有最新的v1.4.0版本sdk,支持https,待验证)
  • 百度地图

解决方案一:

更新最新sdk,接入并测试

解决方案二:

可以设置 NSExceptionDomains属性来将需要排除强制验证的域名写进来:

五. 总结

开启 NSAllowsArbitraryLoads 为 YES

对第三方访问的服务器设置NSExceptionDomains方式添加白名单

提交审核说明:

  • 必须连接由其他机构控制的服务器,其还不支持安全连接。
  • 必须通过 web 展示来源不一的各种网络内容,但又不能完全使用NSAllowsArbitraryLoadsInWebContent所管理的类。

 

文/SN_Simon(简书作者)
原文链接:http://www.jianshu.com/p/36ddc5b009a7
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。

android 常见分辨率(mdpi、hdpi 、xhdpi、xxhdpi )及屏幕适配注意事项

1 Android手机目前常见的分辨率

1.1 手机常见分辨率:

4:3
VGA     640*480 (Video Graphics Array)
QVGA  320*240 (Quarter VGA)
HVGA  480*320 (Half-size VGA)
SVGA  800*600 (Super VGA)

5:3
WVGA  800*480 (Wide VGA)

16:9
FWVGA 854*480 (Full Wide VGA)
HD        1920*1080 High Definition
QHD     960*540
720p    1280*720  标清
1080p  1920*1080 高清

手机:
iphone 4/4s    960*640 (3:2)
iphone5         1136*640
小米1             854*480(FWVGA)
小米2             1280*720

1.2 分辨率对应DPI

“HVGA    mdpi”
“WVGA   hdpi ”
“FWVGA hdpi ”
“QHD      hdpi ”
“720P     xhdpi”
“1080P   xxhdpi ”

2 屏幕适配的注意事项

2.1 基本设置

2.1.1 AndroidManifest.xml设置

在中Menifest中添加子元素

android:anyDensity=”true”时,应用程序安装在不同密度的终端上时,程序会分别加载xxhdpi、xhdpi、hdpi、mdpi、ldpi文件夹中的资源。

相反,如果设为false,即使在文件夹下拥有相同资源,应用不会自动地去相应文件夹下寻找资源:

1) 如果drawable-hdpi、drawable-mdpi、drawable-ldpi三个文件夹中有同一张图片资源的不同密度表示,那么系统会去加载drawable_mdpi文件夹中的资源;

2) 如果drawable-hpdi中有高密度图片,其它两个文件夹中没有对应图片资源,那么系统会去加载drawable-hdpi中的资源,其他同理;

3) 如果drawable-hdpi,drawable-mdpi中有图片资源,drawable-ldpi中没有,系统会加载drawable-mdpi中的资源,其他同理,使用最接近的密度级别。

2.1.2 横屏竖屏目录区分

1) drawable

a) drawable-hdpi该图片即适用于横屏,也适用于竖屏;

b) drawable-land-hdpi,当屏幕为横屏,且为高密度时,加载此文件夹的资源;

c) drawable-port-hdpi,当屏幕为竖屏,且为高密度时,加载此文件夹中的资源。其他同理。

2) layout

在res目录下建立layout-port和layout-land两个目录,里面分别放置竖屏和横屏两种布局文件,以适应对横屏竖屏自动切换。

2.2 多屏幕适配的4条黄金原则

1) 在layout文件中设置控件尺寸时应采用fill_parent、wrap_content、match_parent和dp;

具体来说,设置view的属性android:layout_width和android:layout_height的值时,wrap_content,match_parent或dp比px更好,文字大小应该使用sp来定义

2) 在程序的代码中不要出现具体的像素值,在dimens.xml中定义;

为了使代码简单,android内部使用pix为单位表示控件的尺寸,但这是基于当前屏幕基础上的。为了适应多种屏幕,android建议开发者不要使用具体的像素来表示控件尺寸。

3) 不使用AbsoluteLayout(android1.5已废弃) ,可以使用RelativeLayout替代;

4) 对不同的屏幕提供合适大小的图片。

不同大小屏幕用不同大小的图片,low:medium:high:extra-high图片大小的比例为3:4:6:8;举例来说,对于中等密度(medium)的屏幕你的图片像素大小为48×48,那么低密度(low)屏幕的图片大小应为36×36,高(high)的为72×72,extra-high为96×96。

2.3 使用9-patch PNG图片

使用图片资源时,如果出现拉伸,因为图片处理的原因,会变形,导致界面走形。9-patch PNG图片也是一种标准的PGN图片,在原生PNG图片四周空出一个像素间隔,用来标识PNG图片中哪些部分可以拉伸、哪些不可以拉伸、背景上的边框位置等。

“上、左”定义可拉伸区域

“右、下”定义显示区域,如果用到完整填充的背景图,建议不要通过android:padding来设置边距,而是通过9-patch方式来定义。

Android SDK中提供了编辑9-Patch图片的工具,在tools目录下draw9patch.bat,能够立刻看到编辑后的拉伸效果,也可以直接用其他图片编辑工具编辑,但是看不到效果。

2.4 不同的layout

Android手机屏幕大小不一,有480×320, 640×360, 800×480……

怎样才能让Application自动适应不同的屏幕呢?

其实很简单,只需要在res目录下创建不同的layout文件夹,比如:layout-640×360、layout-800×480……所有的layout文件在编译之后都会写入R.java里,而系统会根据屏幕的大小自己选择合适的layout进行使用。