git中submodule子模块的添加、使用和删除

背景

项目中经常使用别人维护的模块,在git中使用子模块的功能能够大大提高开发效率。
使用子模块后,不必负责子模块的维护,只需要在必要的时候同步更新子模块即可。
本文主要讲解子模块相关的基础命令,详细使用请参考man page。

子模块的添加

添加子模块非常简单,命令如下:
git submodule add <url> <path>

其中,url为子模块的路径,path为该子模块存储的目录路径。
执行成功后,git status会看到项目中修改了.gitmodules,并增加了一个新文件(为刚刚添加的路径)
git diff –cached查看修改内容可以看到增加了子模块,并且新文件下为子模块的提交hash摘要
git commit提交即完成子模块的添加

子模块的使用

克隆项目后,默认子模块目录下无任何内容。需要在项目根目录执行如下命令完成子模块的下载:
git submodule init
git submodule update

或:
git submodule update –init –recursive
执行后,子模块目录下就有了源码,再执行相应的makefile即可。

子模块的更新

子模块的维护者提交了更新后,使用子模块的项目必须手动更新才能包含最新的提交。
在项目中,进入到子模块目录下,执行 git pull更新,查看git log查看相应提交。
完成后返回到项目目录,可以看到子模块有待提交的更新,使用git add,提交即可。

删除子模块

有时子模块的项目维护地址发生了变化,或者需要替换子模块,就需要删除原有的子模块。
删除子模块较复杂,步骤如下:

rm -rf 子模块目录 删除子模块目录及源码
vi .gitmodules 删除项目目录下.gitmodules文件中子模块相关条目
vi .git/config 删除配置项中子模块相关条目
rm .git/module/* 删除模块下的子模块目录,每个子模块对应一个目录,注意只删除对应的子模块目录即可
执行完成后,再执行添加子模块命令即可,如果仍然报错,执行如下:

git rm –cached 子模块名称

完成删除后,提交到仓库即可。

RSYNC 的核心算法

原文来自:http://coolshell.cn/articles/7425.html 
 

rsync是unix/linux下同步文件的一个高效算法,它能同步更新两处计算机的文件与目录,并适当利用查找文件中的不同块以减少数据传输。rsync中一项与其他大部分类似程序或协定中所未见的重要特性是镜像是只对有变更的部分进行传送。rsync可拷贝/显示目录属性,以及拷贝文件,并可选择性的压缩以及递归拷贝。rsync利用由Andrew Tridgell发明的算法。这里不介绍其使用方法,只介绍其核心算法。我们可以看到,Unix下的东西,一个命令,一个工具都有很多很精妙的东西,怎么学也学不完,这就是Unix的文化啊。

本来不想写这篇文章的,因为原先发现有很多中文blog都说了这个算法,但是看了一下,发现这些中文blog要么翻译国外文章翻译地非常烂,要么就是介绍这个算法介绍得很乱让人看不懂,还有错误,误人不浅,所以让我觉得有必要写篇rsync算法介绍的文章。(当然,我成文比较仓促,可能会有一些错误,请指正)

问题

首先, 我们先来想一下rsync要解决的问题,如果我们要同步的文件只想传不同的部分,我们就需要对两边的文件做diff,但是这两个问题在两台不同的机器上,无法做diff。如果我们做diff,就要把一个文件传到另一台机器上做diff,但这样一来,我们就传了整个文件,这与我们只想传输不同部的初衷相背。

于是我们就要想一个办法,让这两边的文件见不到面,但还能知道它们间有什么不同。这就出现了rsync的算法。

算法

rsync的算法如下:(假设我们同步源文件名为fileSrc,同步目的文件叫fileDst

1)分块Checksum算法。首先,我们会把fileDst的文件平均切分成若干个小块,比如每块512个字节(最后一块会小于这个数),然后对每块计算两个checksum,

  • 一个叫rolling checksum,是弱checksum,32位的checksum,其使用的是Mark Adler发明的adler-32算法,
  • 另一个是强checksum,128位的,以前用md4,现在用md5 hash算法。

为什么要这样?因为若干年前的硬件上跑md4的算法太慢了,所以,我们需要一个快算法来鉴别文件块的不同,但是弱的adler32算法碰撞概率太高了,所以我们还要引入强的checksum算法以保证两文件块是相同的。也就是说,弱的checksum是用来区别不同,而强的是用来确认相同。(checksum的具体公式可以参看这篇文章

2)传输算法。同步目标端会把fileDst的一个checksum列表传给同步源,这个列表里包括了三个东西,rolling checksum(32bits)md5 checksume(128bits)文件块编号

我估计你猜到了同步源机器拿到了这个列表后,会对fileSrc做同样的checksum,然后和fileDst的checksum做对比,这样就知道哪些文件块改变了。

但是,聪明的你一定会有以下两个疑问:

  • 如果我fileSrc这边在文件中间加了一个字符,这样后面的文件块都会位移一个字符,这样就完全和fileDst这边的不一样了,但理论上来说,我应该只需要传一个字符就好了。这个怎么解决?
  • 如果这个checksum列表特别长,而我的两边的相同的文件块可能并不是一样的顺序,那就需要查找,线性的查找起来应该特别慢吧。这个怎么解决?

很好,让我们来看一下同步源端的算法。

3)checksum查找算法。同步源端拿到fileDst的checksum数组后,会把这个数据存到一个hash table中,用rolling checksum做hash,以便获得O(1)时间复杂度的查找性能。这个hash table是16bits的,所以,hash table的尺寸是2的16次方,对rolling checksum的hash会被散列到0 到 2^16 – 1中的某个整数值。(对于hash table,如果你不清楚,建议回去看大学时的数据结构教科书)

顺便说一下,我在网上看到很多文章说,“要对rolling checksum做排序”(比如这篇这篇),这两篇文章都引用并翻译了原作者的这篇文章,但是他们都理解错了,不是排序,就只是把fileDst的checksum数据,按rolling checksum做存到2^16的hash table中,当然会发生碰撞,把碰撞的做成一个链表就好了。这就是原文中所说的第二步——搜索有碰撞的情况。

4)比对算法。这是最关键的算法,细节如下:

4.1)取fileSrc的第一个文件块(我们假设的是512个长度),也就是从fileSrc的第1个字节到第512个字节,取出来后做rolling checksum计算。计算好的值到hash表中查。

4.2)如果查到了,说明发现在fileDst中有潜在相同的文件块,于是就再比较md5的checksum,因为rolling checksume太弱了,可能发生碰撞。于是还要算md5的128bits的checksum,这样一来,我们就有 2^-(32+128) = 2^-160的概率发生碰撞,这太小了可以忽略。如果rolling checksum和md5 checksum都相同,这说明在fileDst中有相同的块,我们需要记下这一块在fileDst下的文件编号

4.3)如果fileSrc的rolling checksum 没有在hash table中找到,那就不用算md5 checksum了。表示这一块中有不同的信息。总之,只要rolling checksum 或 md5 checksum 其中有一个在fileDst的checksum hash表中找不到匹配项,那么就会触发算法对fileSrc的rolling动作。于是,算法会住后step 1个字节,取fileSrc中字节2-513的文件块要做checksum,go to (4.1) – 现在你明白什么叫rolling checksum了吧。

4.4)这样,我们就可以找出fileSrc相邻两次匹配中的那些文本字符,这些就是我们要往同步目标端传的文件内容了。

图示

怎么,你没看懂? 好吧,我送佛送上西,画个示意图给你看看(对图中的东西我就不再解释了)。

这样,最终,在同步源这端,我们的rsync算法可能会得到下面这个样子的一个数据数组,图中,红色块表示在目标端已匹配上,不用传输(注:我专门在其中显示了两块chunk #5,相信你会懂的),而白色的地方就是需要传输的内容(注意:这些白色的块是不定长的),这样,同步源这端把这个数组(白色的就是实际内容,红色的就放一个标号)压缩传到目的端,在目的端的rsync会根据这个表重新生成文件,这样,同步完成。

最后想说一下,对于某些压缩文件使用rsync传输可能会传得更多,因为被压缩后的文件可能会非常的不同。对此,对于gzip和bzip2这样的命令,记得开启 “rsyncalbe” 模式。

Updating Homebrew…卡住的解决办法

确保已安装homebrew,更新镜像源

1. 替换brew.git:

cd “$(brew –repo)”
git remote set-url origin https://mirrors.ustc.edu.cn/brew.git

2. 替换homebrew-core.git:

cd “$(brew –repo)/Library/Taps/homebrew/homebrew-core”
git remote set-url origin https://mirrors.ustc.edu.cn/homebrew-core.git

3. 重置brew.git:

cd “$(brew –repo)”
git remote set-url origin https://github.com/Homebrew/brew.git

4. 重置homebrew-core.git:

cd “$(brew –repo)/Library/Taps/homebrew/homebrew-core”
git remote set-url origin https://github.com/Homebrew/homebrew-core.git

implementation和api

implementation 可以在编译时隐藏自己的依赖,但运行时这个依赖对所有模块可见。目的是为了减少编译时间,依赖自己的模块不会因为这个依赖变更重新编译

A- – ->B- – ->C 当C修改后,只要重新编译B和C就好了,A不需要编译

Test

我的车 tradexportprerelease://ads/9d79cd6e-83be-4efa-b8b8-00b63ada76ea/
二手车 tradexportprerelease://ads/ce8976e5-7820-488d-891f-5782caf0bb66/
新车 tradexportprerelease://ads/05bbad0e-e593-4f16-961a-07a5cce39b38/
购买 tradexportprerelease://ads
订单 tradexportprerelease://orders
聊天 tradexportprerelease://messages/83e34e5b-7772-4b6a-ba96-8556486d9eb4/


tradex://tradexport.com/ads
https://tradexport.com/ads

https://u9814280.ct.sendgrid.net/wf/click?upn=yHLBMl79VtwLkvdSNg6rt1wuIcEZxidcfSDB5uJIFdnuOOYw-2FvfTFi6o8R3CtFED-2FxTTphkzGF9-2Fvf0JlSiLvXwr6hz3sDPzwX7A8g8lFA8-3D_rSqg-2FyCuwG-2Bl-2BFw25GPTXspAEz4yJrsYIBrJX-2F9K-2FDLQQScoyA7rjiufIwG3Yz2Xua3FVhCn-2FioTzyqwVi8syHoeJEsK-2BR8RyCPF8bgNYXPFOHaes5aYB7mHrhbBFPGRjYlTIL596PDvYTw6RKiwrrJlMwai76XdmyE4G8qcxS3R4RcypwZSuQUwiu88k-2B413f1FcYGIXejhMLTCu1AMU71a-2FYS736tQcU4YWrP9kZ3iZe0p3Pbr0Fe6U0t9-2F5bELHplcrpFrOjB16kMEgRPMIAEPlma8vN0Yrp8CnoThKc-3D

JVM内存区域的划分,哪些区域会发生 OOM

JVM 的内存区域可以分为两类:线程私有的区域和线程共有的区域。 线程私有的区域:程序计数器、JVM 虚拟机栈、本地方法栈 线程共有的区域:堆、方法区、运行时常量池

  • 程序计数器。 每个线程有有一个私有的程序计数器,任何时间一个线程都只会有一个方法正在执行,也就是所谓的当前方法。程序计数器存放的就是这个当前方法的JVM指令地址
  • JVM虚拟机栈。 创建线程的时候会创建线程内的虚拟机栈,栈中存放着一个个的栈帧,对应着一个个方法的调用。JVM 虚拟机栈有两种操作,分别是压栈和出站。栈帧中存放着局部变量表、方法返回值和方法的正常或异常退出的定义等等。
  • 本地方法栈。 跟 JVM 虚拟机栈比较类似,只不过它支持的是 Native 方法。
  • 堆。 堆是内存管理的核心区域,用来存放对象实例。几乎所有创建的对象实例都会直接分配到堆上。所以堆也是垃圾回收的主要区域,垃圾收集器会对堆有着更细的划分,最常见的就是把堆划分为新生代和老年代
  • 方法区。方法区主要存放类的结构信息,比如静态属性和方法等等
  • 运行时常量池。运行时常量池位于方法区中,主要存放各种常量信息

其实除了程序计数器,其他的部分都会发生 OOM

  • 堆。 通常发生的 OOM 都会发生在堆中,最常见的可能导致 OOM 的原因就是内存泄漏
  • JVM虚拟机栈和本地方法栈。 当我们写一个递归方法,这个递归方法没有循环终止条件,最终会导致 StackOverflow 的错误。当然,如果栈空间扩展失败,也是会发生 OOM 的
  • 方法区。方法区现在基本上不太会发生 OOM,但在早期内存中加载的类信息过多的情况下也是会发生 OOM 的

GC机制

垃圾回收需要完成两件事:找到垃圾,回收垃圾。 找到垃圾一般的话有两种方法:

  • 引用计数法:当一个对象被引用时,它的引用计数器会加一,垃圾回收时会清理掉引用计数为0的对象。但这种方法有一个问题,比方说有两个对象 A 和 B,A 引用了 B,B 又引用了 A,除此之外没有别的对象引用 A 和 B,那么 A 和 B 在我们看来已经是垃圾对象,需要被回收,但它们的引用计数不为 0,没有达到回收的条件。正因为这个循环引用的问题,Java 并没有采用引用计数法。
  • 可达性分析法: 我们把 Java 中对象引用的关系看做一张图,从根级对象不可达的对象会被垃圾收集器清除。根级对象一般包括 Java 虚拟机栈中的对象、本地方法栈中的对象、方法区中的静态对象和常量池中的常量。

回收垃圾的话有这么四种方法:

  • 标记清除算法: 顾名思义分为两步,标记和清除。首先标记到需要回收的垃圾对象,然后回收掉这些垃圾对象。标记清除算法的缺点是清除垃圾对象后会造成内存的[碎片化]。
  • 复制算法: 复制算法是将存活的对象复制到另一块内存区域中,并做相应的内存整理工作。复制算法的优点是可以避免内存碎片化,缺点也显而易见,它需要[两倍的内存]。
  • 标记整理算法: 标记整理算法也是分两步,先标记后整理。它会标记需要回收的垃圾对象,清除掉垃圾对象后会[将存活的对象压缩],避免了内存的碎片化。
  • 分代算法:将对象分为新生代和老年代对象。那么为什么做这样的区分呢?主要是在Java运行中会产生大量对象,这些对象的生命周期会有很大的不同,有的生命周期很长,有的甚至使用一次之后就不再使用。所以针对不同生命周期的对象采用不同的回收策略,这样可以提高GC的效率

新生代对象分为三个区域:Eden 区和两个 Survivor 区。

新创建的对象都放在 Eden区,当 Eden 区的内存达到阈值之后会触发 Minor GC,这时会将存活的对象复制到一个 Survivor 区中,这些存活对象的生命存活计数会加一。这时 Eden 区会闲置,当再一次达到阈值触发 Minor GC 时,会将Eden区和之前一个 Survivor 区中存活的对象复制到另一个 Survivor 区中,采用的是我之前提到的复制算法,同时它们的生命存活计数也会加一。

这个过程会持续很多遍,直到对象的存活计数达到一定的阈值后会触发一个叫做晋升的现象:新生代的这个对象会被放置到老年代中。 老年代中的对象都是经过多次 GC 依然存活的生命周期很长的 Java 对象。当老年代的内存达到阈值后会触发 Major GC,采用的是标记整理算法

介绍下Android应用程序启动过程

一. :Launcher通过Binder进程间通信机制通知ActivityManagerService,它要启动一个Activity;

二.:ActivityManagerService通过Binder进程间通信机制通知Launcher进入Paused状态;

三.:Launcher通过Binder进程间通信机制通知ActivityManagerService,它已经准备就绪进入Paused状态,于是ActivityManagerService就创建一个新的进程,用来启动一个ActivityThread实例,即将要启动的Activity就是在这个ActivityThread实例中运行;

四. :ActivityThread通过Binder进程间通信机制将一个ApplicationThread类型的Binder对象传递给ActivityManagerService,以便以后ActivityManagerService能够通过这个Binder对象和它进行通信;

五 :ActivityManagerService通过Binder进程间通信机制通知ActivityThread,现在一切准备就绪,它可以真正执行Activity的启动操作了。

Android PorterDuffXfermode

PorterDuffXfermod模式的混合效果如图:

@Override
protected void onDraw(Canvas canvas) {
    //背景色
    canvas.drawARGB(255, 255, 156, 161);
    //先绘制的是dst,后绘制的是src
    drawDst(canvas, mPaint);
    drawSrc(canvas, mPaint);
}

private void drawDst(Canvas canvas, Paint p) {
    //画黄色圆形
    p.setColor(0xFFFFCC44);
    canvas.drawOval(new RectF(0, 0, W * 3 / 4, H * 3 / 4), p);
}

private void drawSrc(Canvas canvas, Paint p) {
    //画蓝色矩形
    p.setColor(0xFF66AAFF);
    canvas.drawRect(W / 3, H / 3, W * 19 / 20, H * 19 / 20, p);
}

代码很简单,就是在onDraw()中先画了一个黄色圆形dst,然后再画了一个蓝色矩形src。

结果显示后绘制的蓝色矩形叠加在黄色圆形上了,这是正常的混合模式。

使用PorterDuffXfermod的CLEAR模式。修改onDraw方法如下:

@Override
protected void onDraw(Canvas canvas) {
    //背景色
    canvas.drawARGB(255, 255, 156, 161);
    //先绘制的是dst,后绘制的是src
    drawDst(canvas, mPaint);
    //设置xfermode
    mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
    drawSrc(canvas, mPaint);
    //还原
    mPaint.setXfermode(null);
}

在绘制蓝色矩形src时给paint设置了Xfermode为CLEAR模式;

发现绘制的蓝色矩形src结果成了白色矩形

这是为什么呢?

先看如下代码:

@Override
protected void onDraw(Canvas canvas) {
    //背景色
    canvas.drawARGB(255, 255, 156, 161);
    int sc = canvas.saveLayer(0, 0, canvas.getWidth(), canvas.getHeight(), null, Canvas.ALL_SAVE_FLAG);
    //先绘制的是dst,后绘制的是src
    drawDst(canvas, mPaint);
    //设置xfermode
    mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
    drawSrc(canvas, mPaint);
    //还原
    mPaint.setXfermode(null);
    canvas.restoreToCount(sc);
}

这次我们发现绘制的这个蓝色矩形src好像“消失了”。并且发现dst与src两个图形的相交处显示的是背景色。

比较这两处代码不同之处在于这次绘制图形是放在canvas.saveLayer与canvas.restoreToCount中实现的。

关于savelayout的作用查一下可知:

  1. canvas是支持图层layer渲染这种技术的,canvas默认就有一个layer,当我们平时调用canvas的各种drawXXX()方法时,其实是把所有的东西都绘制到canvas这个默认的layer上面。
  2. 我们还可以通过canvas.saveLayer()新建一个layer,新建的layer放置在canvas默认layer的上部,当我们执行了canvas.saveLayer()之后,我们所有的绘制操作都绘制到了我们新建的layer上,而不是canvas默认的layer。
  3. 用canvas.saveLayer()方法产生的layer所有像素的ARGB值都是(0,0,0,0),即canvas.saveLayer()方法产生的layer初始时时完全透明的。
  4. canvas.saveLayer()方法会返回一个int值,用于表示layer的ID,在我们对这个新layer绘制完成后可以通过调用canvas.restoreToCount(layer)或者canvas.restore()把这个layer绘制到canvas默认的layer上去,这样就完成了一个layer的绘制工作。

分析一下原因:

  1. 我们先将画布设置背景色,此时画布的ARGB值就是背景色值。
  2. 我们通过saveLayer新建一个layer,接下来的绘制都是在这个新建的layer上进行绘制的了。
  3. 我们先绘制了一个黄色的圆形dst。
  4. 设置画笔Xfermode为CLEAR模式画蓝色矩形src,由于CLEAR模式蓝色矩形区域(包括与圆形相交部分)的ARGB值被设置为(0,0,0,0)也就是透明。
  5. 这样新建的layer上就只有一个3/4黄色圆形dst是不透明的(1/4与src叠加部分被置为透明),最后通过restoreToCount将layer绘制到canvas上,绘制的结果就是:透明部分显示canvas背景颜色,不透明部分显示黄色圆形dst部分。也就是上图这种现象。

为什么第一次会出现白色矩形呢?”这个问题也就很好回答了,因为我们一开始是在canvas的默认layer上绘制的,当我们的矩形区域src被CLEAR模式绘制后,该区域变为透明。canvas该部分就成为透明了,但由于Activity背景本身是白色的所以最终显示就为白色了。

通过上面的实验和分析可以得出,CLEAR模式可以使两种图片相交处设置为透明。但我们把API Demo那张图拿出来后一看:“不对啊,不应该是全部透明么?”这里就是那张图给我们挖的坑了。我们仔细看下官方示例的代码是如何实现的:

// create a bitmap with a circle, used for the "dst" image
static Bitmap makeDst(int w, int h) {
    Bitmap bm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
    Canvas c = new Canvas(bm);
    Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);

    p.setColor(0xFFFFCC44);
    c.drawOval(new RectF(0, 0, w*3/4, h*3/4), p);
    return bm;
}

// create a bitmap with a rect, used for the "src" image
static Bitmap makeSrc(int w, int h) {
    Bitmap bm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
    Canvas c = new Canvas(bm);
    Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);

    p.setColor(0xFF66AAFF);
    c.drawRect(w/3, h/3, w*19/20, h*19/20, p);
    return bm;
}

这是其实现代码,我们可以看出这两个方法的目的就是创建两个图形用来演示PorterDuffXfermod的不同效果。但我们仔细一看就能发现有点问题了:

  1. 创建的bitmap的宽高是w,h;但绘制的图形大小只是其中一部分(注意drawXX中的代码),这样bitmap其余部分是透明的。
  2. 这就可以知道最终得到的dst与src是两张大小一致(都为w,h)的bitmap,只是可见区域(绘制区域)导致我们误解是两个不同尺寸的bitmap。
  3. 这样的dst与src叠加就不再是一个黄色圆形与蓝色矩形叠加了,而是两张宽高一致的bitmap叠加了。
  4. 这样的src(蓝色矩形)设置为CLEAR后,其实是绘制的整个bitmap设置为CLEAR。dst与src相交区域也就是整个bitmap区域都会被设置为透明,这也就是为什么我们的实验与官方demo不一致的地方了。

原来如此!所以在开发中万不可看着API Demo图无脑来选择一种自己想要的模式,这样会使你得不到自己想要的结果时摸不着头脑。接下来我们按照图形实际大小修改后可以得出另一张图:

最后

1、关于硬件加速 

在sdkversion>=11时,需要关闭硬件加速,否则 Mode.CLEAR 、 Mode.DARKEN 、 Mode.LIGHTEN 三种模式下绘制效果不正常。

if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB){
//View从API Level 11才加入setLayerType方法
//关闭硬件加速
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}

2、关于saveLayer 

savelayer不是必须的,我们已经知道使用他的目的了;其实我们可以新建一个bitmap并在这上面进行绘制,最后将bitmap绘制到canvas上,其实原理是一致的。

Https相关-HostnameVerifier

[强制]在实现的HostnameVerifier子类中,需要使用verify函数效验服务器主机名的合法性,否则会导致恶意程序利用中间人攻击绕过主机名效验。

说明:
在握手期间,如果URL的主机名和服务器的标识主机名不匹配,则验证机制可以回调此接口实现程序来确定是否应该允许此连接,如果回调内实现不恰当,默认接受所有域名,则有安全风险。

反例:
HostnameVerifier hnv=new HosernameVerifier(){
  @Override
  public boolean verify(String hostname,SSLSession session){
      return true;
  }
}
正例:
HostnameVerifier hnv=new HosernameVerifier(){
@Override
public boolean verify(String hostname,SSLSession session){
    if("youhostname".equals(hostname)){
        return true;
    }else{
          HostnameVerifier        hv=HttpsURLConnection.getDefaultHostnameVerifier();
         return hv.verify(hostname,session);
          }
  }
}

经验

./gradlew clean -i assembleDebug

发现 连接dl.google.com超时

则使用http://ping.chinaz.com/网站ping下,找到dl.google.com对应的ip

在本地hosts中添加一条记录

203.208.43.66 dl.google.com

再编译就好了

bei

recyclerView.setNestedScrollingEnabled(true);

https://www.jianshu.com/p/635970ac603a

https://hfutfei.iteye.com/blog/988374

https://www.cnblogs.com/frankliiu-java/articles/1759460.html

https://www.cnblogs.com/zhaoyan001/p/6365064.html

https://blog.csdn.net/android_gogogo/article/details/53376178

https://my.oschina.net/djone/blog/145057

https://blog.csdn.net/lv_fq/article/details/77836700

https://github.com/lvfaqiang/AndroidTestCode

https://github.com/osmandapp/Osmand

http://www.justlive.vip/blog/article/details/4199

https://github.com/jiaowenzheng/CustomTextView

http://www.cnblogs.com/TerryBlog/archive/2013/04/02/2994815.html

https://www.cnblogs.com/zhujiabin/p/5808232.html

https://github.com/Luction/AndroidRichText

https://blog.csdn.net/u014620028/article/details/54092723

https://www.cnblogs.com/zhujiabin/p/5808232.html

http://www.cnblogs.com/luction/p/3645210.html

https://www.jianshu.com/p/b87dddf02e04

https://juejin.im/post/5b13a5b8f265da6e3128d501

https://tonnyl.io/Spantastic-Text-Styling-With-Spans/

http://melonteam.com/posts/gei_ni_de_spannablestring_she_zhi_dian_ji_tai/

https://blog.51cto.com/kinbos/1348407

https://blog.csdn.net/c16882599/article/details/52913799

https://segmentfault.com/a/1190000006163046

https://blog.csdn.net/natloc/article/details/50849700

http://blog.hacktons.cn/2015/02/03/porterduff/

https://www.jianshu.com/p/d54e24efbd7b

https://github.com/MrAllRight/BezierView/

https://www.jianshu.com/p/a5c2f0359d31

https://www.jianshu.com/p/016534448bfe

https://www.jianshu.com/p/92627f72c707

https://blog.csdn.net/ccpat/article/details/46717573

Androidbug-java.util.zip.ZipException: duplicate entry(重复依赖多版本的类库)

以上大概意思:重复依赖,重复了条目。

1、清除一下缓存:File->Invalidate Caches/Restart..

注意:
除开Gradle依赖类库之外,还可以在项目中的libs下静态方式添加类库。
若是使用静态方式添加jar类库的,请删除重复的jar

2、编译前clean下

https://blog.csdn.net/hexingen/article/details/74065796