Java NIO通信框架在电信领域的实践
Netty是业界最流行的NIO框架之一,它的健壮性、功能、性能、可定制性和可扩展性在同类框架中都是首屈一指的,它已经得到成百上千的商用项目验证,例如Hadoop的RPC框架avro使用Netty作为底层通信框架;很多其他业界主流的RPC框架,也使用Netty来构建高性能的异步通信能力。
通过对Netty的分析,我们将它的优点总结如下:
1) API使用简单,开发门槛低;
2) 功能强大,预置了多种编解码功能,支持多种主流协议;
3) 定制能力强,可以通过ChannelHandler对通信框架进行灵活地扩展;
4) 性能高,通过与其他业界主流的NIO框架对比,Netty的综合性能最优;
5) 成熟、稳定,Netty修复了已经发现的所有JDK NIO BUG,业务开发人员不需要再为NIO的BUG而烦恼;
6) 社区活跃,版本迭代周期短,发现的BUG可以被及时修复,同时,更多的新功能会加入;
7) 经历了大规模的商业应用考验,质量得到验证。在互联网、大数据、网络游戏、企业应用、电信软件等众多行业得到成功商用,证明了它已经完全能够满足不同行业的商业应用了。
正是因为这些优点,Netty逐渐成为Java NIO编程的首选框架,它也是华为公司首选的Java NIO通信框架,公司已经将其纳入到公司级的优选开源第三方软件库中。
3. Netty在电信领域的实践
电信行业软件的几个特点:
1) 高可靠性:5个9;
2) 高性能、低时延;
3) 大规模组网:例如中国移动、Telfonica 拉美十三国、沃达丰等,业务组网规模都非常大;
4) 复杂的网络形态:对接不同设备提供商的网元和系统。
3.1. 高性能、低时延
3.1.1. 非阻塞I/O模型
在I/O编程过程中,当需要同时处理多个客户端接入请求时,可以利用多线程或者I/O多路复用技术进行处理。I/O多路复用技术通过把多个I/O的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。与传统的多线程/多进程模型比,I/O多路复用的最大优势是系统开销小,系统不需要创建新的额外进程或者线程,也不需要维护这些进程和线程的运行,降低了系统的维护工作量,节省了系统资源。
我们采用Netty的NIO传输模式来提升I/O操作的效率,节省线程等其它资源开销,它的模型如下所示:
图3-1 Netty的非阻塞I/O调度模型
3.1.2. 高性能的序列化框架
在华为软件,对于序列化框架的选择,我们遵循如下几个原则:
1) 序列化后的码流大小(网络带宽的占用);
2) 序列化&反序列化的性能(CPU、内存等资源占用);
3) 是否支持跨语言(异构系统的对接和开发语言切换);
4) 高并发调用时的性能,是否随着线程并发数线性增长。
基于上述的指标,目前最常用的选择是:Google的ProtoBuf和Apache的Thrift。
Netty原生提供了对ProtoBuf序列化框架的支持,它的优点如下:
1) 在谷歌内部长期使用,产品成熟度高;
2) 跨语言、支持多种语言,包括C++、Java和Python;
3) 编码后的消息更小,更加有利于存储和传输;
4) 编解码的性能非常高;
5) 支持不同协议版本的前向兼容;
6) 支持定义可选和必选字段。
Netty ProtoBuf 服务端开发示例如下:
// 配置服务端的NIO线程组
EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .option(ChannelOption.SO_BACKLOG, 100) .handler(new LoggingHandler(LogLevel.INFO)) .childHandler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) { ch.pipeline().addLast( new ProtobufVarint32FrameDecoder()); ch.pipeline().addLast( new ProtobufDecoder( SubscribeReqProto.SubscribeReq .getDefaultInstance())); ch.pipeline().addLast( new ProtobufVarint32LengthFieldPrepender()); ch.pipeline().addLast(new ProtobufEncoder()); ch.pipeline().addLast(new SubReqServerHandler()); } });
Thrift相对复杂一些,需要将编解码框架从Thrift中剥离出来,然后利用Netty编解码框架的扩展性定制实现,在此不再赘述。
3.1.3. 收敛的Reactor线程模型
Java线程采用抢占的方式争夺CPU等资源,当系统线程数增大到一定量级之后,性能不仅没有提升,反而下降。
对于大型的电信应用,如果使用Tomcat等做Web容器,为了保证吞吐量和性能,HTTP线程池的最大线程数往往配置为1024。在系统运行期间我们Dump线程堆栈,发现大量的线程竞争,这不仅导致HTTP协议栈的性能下降,更影响其它业务处理线程的执行效率。
使用Netty之后,我们通过控制NioEventLoopGroup的NioEventLoop个数来收敛线程,防止线程膨胀。NioEventLoop聚合了一个多路复用器Selector,可以高效的处理N个Channel,它的线程模型如下:
图3-1 Netty Reactor线程模型
3.1.4. 其它优化
为了进一步提升性能,降低时延,我们还采用了其它一些优化措施,总结如下:
1) 使用Netty 4的内存池,减少业务高峰期ByteBuf频繁创建和销毁导致的GC频率和时间;
2) 在程序中充分利用Netty提供的“零拷贝”特性,减少额外的内存拷贝,例如使用CompositeByteBuf而不是分别为Head和Body各创建一个ByteBuf对象;
3) TCP参数的优化,设置合理的Send和Receive Buffer,通常建议值为64K - 128K;
4) 软中断:如果Linux内核版本支持RPS(2.6.35以上版本),开启RPS后可以实现软中断,提升网络吞吐量;
5) 无锁化串行开发理念:使用Netty 4.X版本,天生支持串行化处理;业务开发过程中,遵循Netty 4的线程模型优化理念,防止人为增加线程竞争。
3.2. 高HA
3.2.1. 内存保护
为了提升内存的利用率,Netty提供了内存池和对象池。但是,基于缓存池实现以后需要对内存的申请和释放进行严格的管理,否则很容易导致内存泄漏。
如果不采用内存池技术实现,每次对象都是以方法的局部变量形式被创建,使用完成之后,只要不再继续引用它,JVM会自动释放。但是,一旦引入内存池机制,对象的生命周期将由内存池负责管理,这通常是个全局引用,如果不显式释放JVM是不会回收这部分内存的。
对于Netty的用户而言,使用者的技术水平差异很大,一些对JVM内存模型和内存泄漏机制不了解的用户,可能只记得申请内存,忘记主动释放内存,特别是JAVA程序员。
为了防止因为用户遗漏导致内存泄漏,Netty在Pipe line的尾Handler中自动对内存进行释放。
缓冲区内存溢出保护:做过协议栈的读者都知道,当我们对消息进行解码的时候,需要创建缓冲区。缓冲区的创建方式通常有两种:
1) 容量预分配,在实际读写过程中如果不够再扩展;
2) 根据协议消息长度创建缓冲区。
在实际的商用环境中,如果遇到畸形码流攻击、协议消息编码异常、消息丢包等问题时,可能会解析到一个超长的长度字段。笔者曾经遇到过类似问题,报文长度字段值竟然是2G多,由于代码的一个分支没有对长度上限做有效保护,结果导致内存溢出。系统重启后几秒内再次内存溢出,幸好及时定位出问题根因,险些酿成严重的事故。
Netty提供了编解码框架,因此对于解码缓冲区的上限保护就显得非常重要。下面,我们看下Netty是如何对缓冲区进行上限保护的:
1) 在内存分配的时候指定缓冲区长度上限;
2) 在对缓冲区进行写入操作的时候,如果缓冲区容量不足需要扩展,首先对最大容量进行判断,如果扩展后的容量超过上限,则拒绝扩展;
3) 在解码的时候,对消息长度进行判断,如果超过最大容量上限,则抛出解码异常,拒绝分配内存。
3.2.2. 流量整形
电信系统一般都有多个网元组成,例如参与短信互动,会涉及到手机、基站、短信中心、短信网关、SP/CP等网元。不同网元或者部件的处理性能不同。为了防止因为浪涌业务或者下游网元性能低导致下游网元被压垮,有时候需要系统提供流量整形功能。
流量整形(Traffic Shaping)是一种主动调整流量输出速率的措施。一个典型应用是基于下游网络结点的TP指标来控制本地流量的输出。流量整形与流量监管的主要区别在于,流量整形对流量监管中需要丢弃的报文进行缓存——通常是将它们放入缓冲区或队列内,也称流量整形(Traffic Shaping,简称TS)。当令牌桶有足够的令牌时,再均匀的向外发送这些被缓存的报文。流量整形与流量监管的另一区别是,整形可能会增加延迟,而监管几乎不引入额外的延迟。
流量整形的原理示意图如下:
图3-2 Netty 流量整形原理图
Netty内置两种流量整形策略,可以方便的被用户添加和使用:
1) 全局流量整形的作用范围是进程级的,无论你创建了多少个Channel,它的作用域针对所有的Channel。用户可以通过参数设置:报文的接收速率、报文的发送速率、整形周期;
2) 单链路流量整形与全局流量整形的最大区别就是它以单个链路为作用域,可以对不同的链路设置不同的整形策略,整形参数与全局流量整形相同。
3.2.3. 其它可靠性措施
其它比较重要的可靠性措施如下:
1) 客户端连接超时控制策略;
2) 链路断连重连策略;
3) 链路异常关闭资源释放;
4) 解码失败的异常处理策略;
5) 链路异常的捕获和处理;
6) I/O线程的释放。
参考:
http://www.infoq.com/cn/articles/netty-high-performance
http://normanmaurer.me/presentations/2014-facebook-eng-netty/slides.html
【亲述】Uber容错设计与多机房容灾方案 - 高可用架构系列
此文是根据赵磊在【QCON高可用架构群】中的分享内容整理而成,转发请注明来自公众号高可用架构(ArchNotes)。
赵磊,Uber高级工程师,08年上海交通大学毕业,曾就职于微软,后加入Facebook主要负责Messenger的后端消息服务。这个系统在当时支持Facebook全球5亿人同时在线。目前在Uber负责消息系统的构建并推进核心服务在高可用性方向的发展。
前言
赵磊在7月21号的全球架构师峰会深圳站上,做了主题演讲:Uber高可用消息系统构建,对于这个热门主题,高可用架构群展开了热议,大家对分布式系统中的各种错误处理非常感兴趣。Tim Yang特邀赵磊通过微信群,在大洋彼岸的硅谷给大家进一步分享。
分布式系统单点故障怎么办
non-sharded, stateless 类型服务非常容易解决单点故障。 通常load balancer可以按照固定的时间间隔,去health check每个node, 当某一个node出现故障时,load balancer可以把故障的node从pool中排除。
很多服务的health check设计成简单的TCP connect, 或者用HTTP GET的方式,去ping一个特定的endpoint。当业务逻辑比较复杂时,可能业务endpoint故障,但是health endpoint还能正常返回,导致load balancer无法发现单点故障,这种情况可以考虑在health check endpoint中增加简单的业务逻辑判断。
对于短时间的network故障,可能会导致这段时间很多RPC call failures。 在RPC client端通常会实现backoff retry。 failure可能有几种原因:
-
TCP connect fail,这种情况下retry不会影响业务逻辑,因为Handler还没有执行。
-
receive timeout, client无法确定handler是不是已经收到了request 而且处理了request,如果handler重复执行会产生side effect,比如database write或者访问其他的service, client retry可能会影响业务逻辑。
对于sharded service,关键是如何找到故障点,而且将更新的membership同步到所有的nodes。下面讨论几种sharding的方案:
-
将key space hash到很多个小的shard space, 比如4K个shards。 通过zookeeper (distributed mutex) 选出一个master,来将shard分配到node上,而且health check每一个node。当遇到单点故障时,将已经assigned的shards转移到其他的nodes上。 因为全局只有一个single master, 从而保证了shard map的全局一致。当master故障时,其他的backup node会获得lock成为Master
-
Consistent hashing方式。consistent hashing 通常用来实现cache cluster,不保证一致性。 因为每个client会独立health check每一个node, 同时更新局部的membership。 在network partition的情况或者某一个node不停的重启, 很可能不同的client上的membership不一致,从而将相同的key写在了不同的node上。 当一致性的需求提高时,需要collaborative health check, 即每个node要monitor所有其他node的health。 Uber在这里使用的是gossip protocol,node之间交换health check的信息。
大面积故障怎么办
大面积故障时,比如交换机故障(rack switch failure),可用的机器不足以处理所有的请求。 我们尽可能做的就是用50%的capacity 处理50%的请求或者50%用户的所有请求。而尽量避免整个服务故障。 当设计一个服务的时候,它的throughput应该是可linear scale的。
-
在同样的CPU占用情况下,1个机器应该处理100个请求,那么5个机器应该可以处理500个请求。
-
而且在同样的机器数量下,20%的CPU可以处理200个请求,那么60%的CPU应该可以处理3倍即600个请求。
后者是很难实现的,而且当CPU越高的时候,服务的throughput并不是线性的。 通常在80%CPU以上的情况,throughput会下降非常快。 随着CPU使用增加,request的latency也会提高。 这对上下游的服务可能都是一个挑战,可能会导致cascade failure。
对于nodejs或者java nio一类的async IO框架来说,另外一个问题就是event loop lag。 这两者可能导致connection数量增加。下面举两个例子
-
有些RPC transport支持pipelining但不支持multiplexing (out of order responses), pipelining是指在同一个TCP连接上可以连续发出Req1, Req2, Req3, Response1, Response2, Response3,即Response的顺序必须和Request的顺序是一致。Req1如果需要很长时间,Req2和3就都不能返回。一个Request如果占用太长时间,会导致后面的很多个Request timeout。RPC client通常也会限制在一个TCP connection上面的max pending requests。但timeout发生,或者max pending requests情况下,client会主动创建新的connection。
-
event loop lag 是指程序占用太长时间执行连续的CPU intensive任务。 只有当任务结束时,event loop才会handle IO events,比如从socket上面读数据。否则收到的数据只能保存在kernel 的TCP buffer里,通常这个buffer size小于64KB。当buffer满时(而且service又很长时间没有读buffer),socket的远端就不能发送更多的数据。这时也会导致远端的transport error。同样的,client会主动创建新的connection,当connection增加到预设的fd limit时,service就不能继续accept新的TCP connection了,其实是不能open新的文件了。而且,绝大部分的程序没有测试过达到fd limit的场景。很多API需要open file, 比如logging和core dump. 所以,一旦达到fd limit, 就像out of memory一样,将很难recover,只能crash process. 而这时正是过载的时候,重启实际上减少了capacity。 任何crash在过载的情况下只会更糟。
facebook在这防止过载上做的很好,在C++实现的thrift server上,有一个或者多个threads只负责accept TCP connections. 你可以指定最多的connections for thrift calls。 这个connection limit是远小于fd limit, 当connection太多时,thrift server可以fail fast。所以,这种情况下可以让service能一直保持在max qps。
整个数据中心挂掉怎么办
在Uber的场景中,如果rider已经在一个trip上了,我们通产会等trip结束后才把rider迁移到其他的数据中心,我们叫做soft failover。
否则需要hard failover,我们会把DNS指向其他的数据中心。 而且用户的DNS服务器很可能在一段时间内还是cache以前的ip,而且这个cache的时间是基本没办法控制的,所以我们会在load balancer上返回HTTP redirect,这样手机的客户端收到后会立即转向新的备份数据中心。
惊群问题(thundering herd), 很多服务在provision的时候根据平常的QPS预留了很少的容量空间,当数据中心或者load balancer重启的时候,如果所有的客户端同时发起请求,这时的QPS可以是平时的很多倍。 很可能导致大部分请求都失败。一方面需要在客户端实现exponential backoff, 即请求失败后retry的间隔时间是增长的,比如1秒,5秒,20秒等等。另外在load balancer上实现rate limiting或者global blackhole switch, 后者可以有效的丢掉一部分请求而避免过载,同时尽早触发客户端的backoff逻辑。
如果大家用AWS或者其他云服务的话,AWS的一个region通常包括几个数据中心。各个数据中心甚至在相邻的介个城市,有独立的空调系统和供电。
数据中心之间有独立的网络 high throughput low latency, 但是在region之间的网络通常是共有的 high throughput high lantecy
整个region挂掉很少发生。可以把服务部署在多个可用区(Availability Zone)来保证高可用性。
Q & A
Q1:health check endpoint中实现简单的业务逻辑,这个意思是load balancer中有业务逻辑检查的插件么?这样load balancer会不会很重啊,可以详细说一下么?
load balancer仍然是HTTP GET, health check 没有额外的开销,但是服务本身处理health的方式不同,可加入业务逻辑相关的检查 比如是不是能够访问数据库。
Q2:region切换时,用户的数据是怎么迁移的?
这个是个很好的问题,Uber采取的是个非常特别的方法。 realtime系统会在每次用户state change。state change的时候把新的state下载到手机上,而且是加密的。当用户需要迁移到新的数据中心的时候,手机需要上传之前下载的state,服务就可以从之前的state开始,但是non-realtime系统 比如用户数据是通过sql replication来同步的。是Master-master。而且Uber在上层有个数据抽象,数据是基本上immutable的 append-only 所以基本不存在冲突。
Q3:如果是req timeout,但另外一边已经执行成功了,这时候重试,那不就是产生了两次数据?特别是insert这种类型的。
是的,如果是GET类型的请求可以retry, 但是POST类型的请求 那么只能在conn timeout时可以安全的retry。 但是receive timeout不能重试。(Tim补充看法:对于POST请求,如果service实现了幂等操作也是可以retry)。 有些类型的数据可以自动merge比如set和map
Q4:那receive timeout,这种情况下,只能通过merge或者冲突对比解决?
恩 是的。 需要在逻辑层判断是不是能够retry。 这个我建议在更上层实现, 比如在消息系统中,全程不retry 就可以保证at most once delivery, 如果需要保证at least once delivery 需要加入数据库和client dedupe
Q5:大面积故障时Uber用什么手段来控制只处理部分用户请求?
我们实现了一些rate limiting 和 circuit breaking的库,但是这时针对所有请求的。 我们现在还没有做到只处理某些用户的请求。
Q6:“将key space hash到相对小的shard space, 因为全局只有一个single master, 从而保证了shard map的全局一致” 这个方案每次计算shard node的时候,必须先询问下master么?
是的。 在client端有一个shard map的cache, 每隔几秒钟可以refresh, 如果是复杂的实现,则可以是master 推送shardmap change。
Q7:多个机房的数据是sharding存储(就是每个机房只存储一部分用户数据),还是所有机房都有所有用户全量数据?
Uber现在的做法是每个机房有所有用户的数据。 facebook的做法是一个机房有一部分用户的数据。
Q8:那多个机房的数据同步采用什么方案?
facebook用的就是mysql replication,有些细节我不清楚。 Uber还没有跨数据中心的replication,但是我们考虑买riak的enterprise服务,可以支持跨数据中心的 replication。 对于sql数据 我们就2个方案:大部分用户数据还是在postgresql里的(没有sharding, 是个single node),因为Uber起家的时候就在postgres上,这个数据是用postgres原生支持的replication, 另外有个mysql的, mysql存的是trip的数据, 所以是append only而且不需要merge的。 这个我还需要确认是不是每个数据中心里面有全量的数据还是只有本地产生的trip数据
Uber数据抽象做的比较好,数据分为3类:
最小的 realtime的,跟ongoing trip的个数成正比。 正在迁移到riak
比较大 非realtime的,跟user个数成正比。在postgresql里面 用postgresql的relication,正在迁移到mysql,用mysql的replication
最大 非realtime的,跟trip个数成正比。 在MySQL里面有很多partition,一个用户在一个partitionl里面,一个partition一个全局的master,写都去master。 而且Partition很少迁移,所以当seconary变成Master时,可能没有用户之前的trip的信息,replication是offline的 好像是通过backup-restore实现的。
Q9: 那如何实现“每个机房都有全量数据”的?
不是实时的,是在应用层实现的,而且现在还没开始大规模使用。 另外问下riak 有同学在用么? Uber 的很多系统去年就开始迁移到riak上了,因为riak是保证availability的 。将来在Uber会是重点
Q10:Uber的消息系统是基于nodejs的吗?客户端长链接的性能和效率方面如何优化?
是基于nodejs的。我们没有特别优化性能,不过stress test看起来2个物理机可以保持800K连接
Q11:Uber消息系统协议自己DIY吗? 是否基于TLS? PUSH消息QPS能达到多少?
是的,基于HTTPS。 具体QPS我不太记得了。
Q12:riak的性能如何?主要存储哪些类型的数据呢?存储引擎用什么?raik的二级索引有没有用到呢?
riak性能我没测试过,跟数据类型和consistency level都有关系。 可能差别比较大。 我们现在用的好像是leveldb
Q13:应用层实现多机房数据一致的话,是同时多写吗? 这个latency会不会太长?
sql现在都是用在non-realtime系统里面,所以latency可能会比较长
Q14:Uber rpc用的什么框架,上面提到了Thrift有好的fail fast策略,Uber有没有在rpc框架层面进行fail fast设计?
Uber在RPC方面还刚开始。 我们一直是用http+json的,最近在朝tchannel+thrift发展, tchannel是一个类似http2.0的transport,tchannel 在github上能找到。
我们的nodejs thrift 是自己实现的,因为apache thrift在node上做的不是很好,thrift的实现叫做thriftify https://github.com/Uber/thriftify
正好推荐下我的开源项目哈。 在thrift server上我们没有做fail fast, 如何保护是在routing service中实现的。
Q15:Uber走https协议,有没有考虑spdy/http2.0之类的呢?在中国网速状况不是很好的,Uber有没有一些https连接方面的优化措施?
正在考虑迁移到HTTP2.0,这个主要是手机端有没有相应的client实现。 server端我们用的是nginx,nginx上有个experiemnt quality的extension可以支持spdy。 我们还考虑过用facebook的proxygen https://github.com/facebook/proxygen,proxygen支持spdy。 我在facebook的chat service是用proxygen实现的,而且facebook 几十万台PHP server都在proxygen上,所以可以说是工业级强度的基础设施,不过build起来要花点时间。
Q16:为了避免服务过载和cascade failure,除了在服务链的前端采用一些fail fast 的设计,还有没有其它的实践作法,比如还是想支持一部分用户或特定类型的请求,采用优先级队列等。 就这个问题,Uber,facebook在服务化系统中还有没有其它技术实践?另外出现大规模服务过载后的恢复流程方面,有没有碰到什么坑或建议?
“比如还是想支持一部分用户或特定类型的请求” 这个其实比较难实现 因为当服务过载的时候 在acceptor thread就停止接受新的connection了,那就不知道是哪个用户的请求 。这个需要在应】用层实现,比如feature flag可以针对一些用户关掉一些feature。 我发现有个很有用的东西就是facebook有个global kill switch,可以允许x%的流量,这个当所有service一起crash 重启的时候比较有用。
TIP:为了更好的理解赵磊的分享内容,大家可以参照ArchSummit 2015深圳赵磊的的ppt一起来理解。ppt可以通过stuq.org下载或微信添加”stuq”公众号。
想同赵磊及群专家进一步交流容错设计及多机房容灾方案,可回复arch申请进群。
利用 DexClassLoader 实现 Android 插件化,从而达到动态加载
1、作用
大多数朋友开始接触这个问题是因为 App 爆棚了,方法数超过了一个 Dex 最大方法数 65535 的上限,因而便有了插件化的概念,将一个 App 划分为多个插件(Apk 或相关格式)
常用的其他解决方法还包括:Google Multidex,用 H5 代替部分逻辑,删无用代码,买付费版的 Proguard
当插件化作用不止于此,还包括:(1) 模块解耦,(2) 动态升级,(3) 高效并行开发(编译速度更快) (4) 按需加载,内存占用更低 (5) 节省升级流量
2、概念
Android 插件化 —— 指将一个程序划分为不同的部分,比如一般 App 的皮肤样式就可以看成一个插件
Android 组件化 —— 这个概念实际跟上面相差不那么明显,组件和插件较大的区别就是:组件是指通用及复用性较高的构件,比如图片缓存就可以看成一个组件被多个 App 共用
Android 动态加载 —— 这个实际是更高层次的概念,也有叫法是热加载或 Android 动态部署,指容器(App)在运⾏状态下动态加载某个模块,从而新增功能或改变某⼀部分行为
3、相关资料
插件化的原理实际是 Java ClassLoader 的原理,看其他资料前请先看:Java ClassLoader基础
Android 也有自己的 ClassLoader,分为 dalvik.system.DexClassLoader 和 dalvik.system.PathClassLoader,区别在于 PathClassLoader 不能直接从 zip 包中得到 dex,因此只支持直接操作 dex 文件或者已经安装过的 apk(因为安装过的 apk 在 cache 中存在缓存的 dex 文件)。而 DexClassLoader 可以加载外部的 apk、jar 或 dex文件,并且会在指定的 outpath 路径存放其 dex 文件。
目前开源的插件化框架有:
(1) DynamicLoadApk
GitHub:https://github.com/singwhatiwanna/dynamic-load-apk
这个项目实现了一部分的动态加载,原理是 DexClassLoader 加 Activity 代理,可以看看。即在容器中注册几个代理的 Activity,启动插件的 Activity 时实际启动的都是代理的 Activity,这样就解决了 Activity 必须注册的问题。
当然这个项目里也有不少问题没解决,有兴趣可以加入他们。
(2) AndroidDynamicLoader GitHub:https://github.com/mmin18/AndroidDynamicLoader
这是点评一个工程师介绍的方式,和上面不同的是:他不是用代理 Activity 的方式实现而是用 Fragment 以及 schema 的方式实现
(3) Android PluginManager GitHub:https://github.com/houkx/android-pluginmgr
这个项目的原理实际也是 DexClassLoader 加 Activity 代理,不同的是上面的 dynamic-load-apk 项目中,插件需要依赖框架的 lib,插件组件继承框架 lib 的 Base 组件。而这个框架通过字节码操作动态生成一个子类去继承插件组件解决插件必须依赖框架的问题,从而达到插件无需做任何改动(理论上)即可加载的效果。
(4) 其他资料
淘宝伯奎:Android插件化及动态部署—ATLAS http://v.youku.com/v_show/id_XNTMzMjYzMzM2.html