(入门篇 NettyNIO开发指南)第四章-分隔符和定长解码器使用

TCP    以流的方式进行数据传输上层的应用协议为了对消息进行区分,往往采用如下4种方式。

(1)消息长度固定,累计读取到长度总和为定长LEN 的报文后,就认为读取到了一个完整的消息,将计数器置位,重新开始读取下一个数据报;
(2)将回车换行符作为消息结束符,例如FTP协议,这种方式在文本协议中应用比较广泛:
(3)将特殊的分隔符作为消息的结束标志,回车换行符就是一种特殊的结束分隔符:
(4)通过在消息头中定义长度字段来标识消息的总长度。

Netty对上面四种应用做了统一的抽象提供了4种解码器来解决对应的问题,使用起来非常方便。有了这些解码器,用户不需要自己对读取的报文进行人工解码,也不需要考虑TCP的粘包和拆包。

第4章我们介绍了如何利用LineBasedFrameDecoder解决TCP的粘包问题,本章我们继续学习另外两种实用的解码器一一DelimiterBasedFrameDecoder和FixedLengthFrameDecoer,前者可以自动完成以分隔符做结束标志的消息的解码,后者可以自动完成对定长消息的解码,它们都能解决TCP粘包/拆包导致的读半包问题。
本章主要内容包括:

1.DelimiterBasedFrameDecoder服务端开发
2.DelimiterBasedFrameDecoder客户端开发
3.运行DelimiterBasedFrameDecoder服务端和客户端
4.FixedLengthFrameDecoer服务端开发
5.通过telnet命令行调试FixedLengtllFrameDecoder服务端



5.1 DelimiterBasedFrameDecoder应用开发

通过对DelimiterBasedFrameDecoder的使用,我们可以自动完成以分隔符作为码流结束标识的消息的解码,下面通过一个演示程序来学习下如何DelimiterBasedFrameDecoder进行开发。

演示程序以经典的Echo服务为例。EchoServer接收到EchoClient的请求消息后,将其打印出来,然后将原始消息返回给客户端,消息以“$”作为分隔符。

5.1.1 DelimiterBasedFrameDecoder  服务端开发
下面我们直接看EchoServer的源代码:

EchoServer服务端EchoServer

package lqy4_delimiter_101;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;

/**
 * @author lilinfeng
 * @date 2014年2月14日
 * @version 1.0
 */
public class EchoServer {
    public void bind(int port) throws Exception {
    // 配置服务端的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)
                throws Exception {
                ByteBuf delimiter = Unpooled.copiedBuffer("$_"
                    .getBytes());
                ch.pipeline().addLast(
                    new DelimiterBasedFrameDecoder(1024,
                        delimiter));
                ch.pipeline().addLast(new StringDecoder());
                ch.pipeline().addLast(new EchoServerHandler());
            }
            });

        // 绑定端口,同步等待成功
        ChannelFuture f = b.bind(port).sync();

        // 等待服务端监听端口关闭
        f.channel().closeFuture().sync();
    } finally {
        // 优雅退出,释放线程池资源
        bossGroup.shutdownGracefully();
        workerGroup.shutdownGracefully();
    }
    }

    public static void main(String[] args) throws Exception {
    int port = 8080;
    if (args != null && args.length > 0) {
        try {
        port = Integer.valueOf(args[0]);
        } catch (NumberFormatException e) {
        // 采用默认值
        }
    }
    new EchoServer().bind(port);
    }
}

我们重点看3741行,首先创建分隔符缓冲对象ByteBuf,本例程中使用$作为分隔符。第40行,创建DelimiterBasedFrameDecoder对象,将其加入到ChannelPipeline中。DelimiterBasedFrameDecoder有多个构造方法,这里我们传递两个参数,第一个1024表示单条消息的最大长度,当达到该长度后仍然没有查找到分隔符,就抛出TooLongFrameException异常,防止由于异常码流缺失分隔符导致的内存溢出,这是Netty解码器的可靠
性保护:第二个参数就是分隔符缓冲对象。

下面继续看EcboServerHandler的实现。

package lqy4_delimiter_101;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;

/**
 * @author lilinfeng
 * @date 2014年2月14日
 * @version 1.0
 */
@Sharable
public class EchoServerHandler extends ChannelHandlerAdapter {
    int counter = 0;

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg)
        throws Exception {
    String body = (String) msg;
    System.out.println("This is " + ++counter + " times receive client : ["
        + body + "]");
    body += "$_";
    ByteBuf echo = Unpooled.copiedBuffer(body.getBytes());
    ctx.writeAndFlush(echo);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
    cause.printStackTrace();
    ctx.close();// 发生异常,关闭链路
    }
}

第21~23行直接将接收的消息打印出来,由于DelimiterBasedFrameDecoder自动对请求消息进行了解码,后续的ChannelHandler接收到的msg对象就是个完整的消息包;第二个ChannelHandler是StringDecoder,它将ByteBuf解码成字符串对象:第三个EchoServerHandler接收到的msg消息就是解码后的字符串对象。
由于我们设置DelimiterBasedFrameDecoder过滤掉了分隔符,所以,返回给客户端时需要在请求消息尾部拼接分隔符“$_”,最后创建ByteBuf,将原始消息重新返回给客户端。

5.1.2    DelimiterBasedFrameDecoder客户端开发

首先看下EchoClient的实现。

EchoClient客户端EchoClient

 1 package lqy4_delimiter_101;
 2
 3 import io.netty.bootstrap.Bootstrap;
 4 import io.netty.buffer.ByteBuf;
 5 import io.netty.buffer.Unpooled;
 6 import io.netty.channel.ChannelFuture;
 7 import io.netty.channel.ChannelInitializer;
 8 import io.netty.channel.ChannelOption;
 9 import io.netty.channel.EventLoopGroup;
10 import io.netty.channel.nio.NioEventLoopGroup;
11 import io.netty.channel.socket.SocketChannel;
12 import io.netty.channel.socket.nio.NioSocketChannel;
13 import io.netty.handler.codec.DelimiterBasedFrameDecoder;
14 import io.netty.handler.codec.string.StringDecoder;
15
16 /**
17  * @author lilinfeng
18  * @date 2014年2月14日
19  * @version 1.0
20  */
21 public class EchoClient {
22
23     public void connect(int port, String host) throws Exception {
24     // 配置客户端NIO线程组
25     EventLoopGroup group = new NioEventLoopGroup();
26     try {
27         Bootstrap b = new Bootstrap();
28         b.group(group).channel(NioSocketChannel.class)
29             .option(ChannelOption.TCP_NODELAY, true)
30             .handler(new ChannelInitializer<SocketChannel>() {
31             @Override
32             public void initChannel(SocketChannel ch)
33                 throws Exception {
34                 ByteBuf delimiter = Unpooled.copiedBuffer("$_"
35                     .getBytes());
36                 ch.pipeline().addLast(
37                     new DelimiterBasedFrameDecoder(1024,
38                         delimiter));
39                 ch.pipeline().addLast(new StringDecoder());
40                 ch.pipeline().addLast(new EchoClientHandler());
41             }
42             });
43
44         // 发起异步连接操作
45         ChannelFuture f = b.connect(host, port).sync();
46
47         // 当代客户端链路关闭
48         f.channel().closeFuture().sync();
49     } finally {
50         // 优雅退出,释放NIO线程组
51         group.shutdownGracefully();
52     }
53     }
54
55     /**
56      * @param args
57      * @throws Exception
58      */
59     public static void main(String[] args) throws Exception {
60     int port = 8080;
61     if (args != null && args.length > 0) {
62         try {
63         port = Integer.valueOf(args[0]);
64         } catch (NumberFormatException e) {
65         // 采用默认值
66         }
67     }
68     new EchoClient().connect(port, "127.0.0.1");
69     }
70 }

与服务端类似,分别将DelimiterBasedFrameDecoder和StringDecoder添加到客户端ChannelPipeline中,最后添加客户端1/0事件处理类EchoClientHandler,下面继续看EchoClientHandler的实现。

EchoClient客尸端    EchoClientHandler

package lqy4_delimiter_101;

import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
/**
 * @author lilinfeng
 * @date 2014年2月14日
 * @version 1.0
 */
public class EchoClientHandler extends ChannelHandlerAdapter {
    private int counter;

    static final String ECHO_REQ = "Hi, Lilinfeng. Welcome to Netty.$_";
    /**
     * Creates a client-side handler.
     */
    public EchoClientHandler() {
    }
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
    // ByteBuf buf = UnpooledByteBufAllocator.DEFAULT.buffer(ECHO_REQ
    // .getBytes().length);
    // buf.writeBytes(ECHO_REQ.getBytes());
    for (int i = 0; i < 10; i++) {
        ctx.writeAndFlush(Unpooled.copiedBuffer(ECHO_REQ.getBytes()));
    }
    }
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg)
        throws Exception {
    System.out.println("This is " + ++counter + " times receive server : ["
        + msg + "]");
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
    ctx.flush();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
    cause.printStackTrace();
    ctx.close();
    }
}

第25~26行在TCP链路建立成功之后循环发送请求消息给服务端,第32~33行打印接收到的服务端应答消息同时进行计数。

5.1.3    运行DelimiterBasedFrameDecoder服务端和客户端

服务端运行结果如下。

 1 This is 1 times receive client : [Hi, Lilinfeng. Welcome to Netty.]
 2 This is 2 times receive client : [Hi, Lilinfeng. Welcome to Netty.]
 3 This is 3 times receive client : [Hi, Lilinfeng. Welcome to Netty.]
 4 This is 4 times receive client : [Hi, Lilinfeng. Welcome to Netty.]
 5 This is 5 times receive client : [Hi, Lilinfeng. Welcome to Netty.]
 6 This is 6 times receive client : [Hi, Lilinfeng. Welcome to Netty.]
 7 This is 7 times receive client : [Hi, Lilinfeng. Welcome to Netty.]
 8 This is 8 times receive client : [Hi, Lilinfeng. Welcome to Netty.]
 9 This is 9 times receive client : [Hi, Lilinfeng. Welcome to Netty.]
10 This is 10 times receive client : [Hi, Lilinfeng. Welcome to Netty.]

客户端运行结果如下。

 1 This is 1 times receive server : [Hi, Lilinfeng. Welcome to Netty.]
 2 This is 2 times receive server : [Hi, Lilinfeng. Welcome to Netty.]
 3 This is 3 times receive server : [Hi, Lilinfeng. Welcome to Netty.]
 4 This is 4 times receive server : [Hi, Lilinfeng. Welcome to Netty.]
 5 This is 5 times receive server : [Hi, Lilinfeng. Welcome to Netty.]
 6 This is 6 times receive server : [Hi, Lilinfeng. Welcome to Netty.]
 7 This is 7 times receive server : [Hi, Lilinfeng. Welcome to Netty.]
 8 This is 8 times receive server : [Hi, Lilinfeng. Welcome to Netty.]
 9 This is 9 times receive server : [Hi, Lilinfeng. Welcome to Netty.]
10 This is 10 times receive server : [Hi, Lilinfeng. Welcome to Netty.]

服务端成功接收到了客户端发送的10条“Hi,Lilinfeng.WelcometoNetty.”
请求消息,客户端成功接收到了服务端返回的10条“Hi,Lilinfe口g.WelcometoNetty.”应答消息。测试结果表明使用Delin1iterBasedFrameDecoder可以自动对采用分隔符做码流结束标识的消息进行解码。

本例程运行10次的原因是模拟TCP粘包/拆包,在笔者的机器上,连续发送10条Echo请求消息会发生粘包,如果没有DelimiterBasedFrameDecoder解码器的处理,服务端和客户端程序都将运行失败。

下面我们将服务端的DelimiterBasedFrameDecoder注释掉,最终代码如下

服务端结果

This is 1 times receive client : [Hi, Lilinfeng. Welcome to Netty.$_Hi, Lilinfeng. Welcome to Netty.$_Hi, Lilinfeng. Welcome to Netty.$_Hi, Lilinfeng. Welcome to Netty.$_Hi, Lilinfeng. Welcome to Netty.$_Hi, Lilinfeng. Welcome to Netty.$_Hi, Lilinfeng. Welcome to Netty.$_Hi, Lilinfeng. Welcome to Netty.$_Hi, Lilinfeng. Welcome to Netty.$_Hi, Lilinfeng. Welcome to Netty.$_]

由于没有分隔符解码器,导致服务端一次读取了客户端发送的所有消息,这就是典型的没有考虑TCP粘包导致的问题。

5.2    FixedLengthFrameDecoder应用开发

FixedLengtl1FrameDecoder是固定长度解码器,它能够按照指定的长度对消息进行自动解码,开发者不需要考虑TCP的粘包/拆包问题,非常实用。下面我们通过一个应用实例对其用法进行讲解。

5.2.1  FixedlengthFrameDecoder服务端开发

在服务端的ChannelPipeline中新增FixedLengthFrameDecoder,长度设置为20,然后再依次增加字符串解码器和EchoServerHandler,代码如下。

EcboServer服务端    EchoServer

package lqy5_fixlengthframe_108;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.FixedLengthFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;

/**
 * @author lilinfeng
 * @date 2014年2月14日
 * @version 1.0
 */
public class EchoServer {
    public void bind(int port) throws Exception {
    // 配置服务端的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)
                throws Exception {
                ch.pipeline().addLast(
                    new FixedLengthFrameDecoder(20));
                ch.pipeline().addLast(new StringDecoder());
                ch.pipeline().addLast(new EchoServerHandler());
            }
            });

        // 绑定端口,同步等待成功
        ChannelFuture f = b.bind(port).sync();

        // 等待服务端监听端口关闭
        f.channel().closeFuture().sync();
    } finally {
        // 优雅退出,释放线程池资源
        bossGroup.shutdownGracefully();
        workerGroup.shutdownGracefully();
    }
    }

    public static void main(String[] args) throws Exception {
    int port = 8080;
    if (args != null && args.length > 0) {
        try {
        port = Integer.valueOf(args[0]);
        } catch (NumberFormatException e) {
        // 采用默认值
        }
    }
    new EchoServer().bind(port);
    }
}

EchoServerHandler 的功能 比较简单 ,直接将 读取到的消息打印 出来 ,代码如下 。

EchoServer  服务端    EchoServerHandler

 1 package lqy5_fixlengthframe_108;
 2
 3 import io.netty.channel.ChannelHandler.Sharable;
 4 import io.netty.channel.ChannelHandlerAdapter;
 5 import io.netty.channel.ChannelHandlerContext;
 6
 7 /**
 8  * @author lilinfeng
 9  * @date 2014年2月14日
10  * @version 1.0
11  */
12 @Sharable
13 public class EchoServerHandler extends ChannelHandlerAdapter {
14
15     @Override
16     public void channelRead(ChannelHandlerContext ctx, Object msg)
17         throws Exception {
18     System.out.println("Receive client : [" + msg + "]");
19     }
20
21     @Override
22     public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
23     cause.printStackTrace();
24     ctx.close();// 发生异常,关闭链路
25     }
26 }

利用FixedLengthFrameDecoder解码器,无论一次接收到多少数据报,它都会按照构造函数中设置的固定长度进行解码,如果是半包消息,FixedLengthFrameDecoder会缓存半包消息并等待下个包到达后进行拼包,直到读取到一个完整的包。

下面的章节我们通过telnet命令行来测试EchoServer服务端,看它能否按照预期进行工作。

5.2.1 利 用 telnet 命令行测试 EchoServer 服务端

5.3    总结

本章我们学习了两个非常实用的解码器:DelimiterBasedFrameDecoder和FixedLengthFrameDecoder。
DelimiterBasedFrameDecoder用于对使用分隔符结尾的消息进行自动解码,FixedLengthFrameDecoder用于对固定长度的消息进行自动解,码。有了上述两种解码器,再结合其他的解码器,如字符串解码器等,可以轻松地完成对很多消息的自动解码,而且不再需要考虑TCP粘包/拆包导致的读半包问题,极大地提升了开发效率。
应用DelimiterBasedFrameDecoder和FixedLengthFrameDecoder进行开发非常简单,在绝大数情况下,只要将DelimiterBasedFrameDecoder或FixedLengthFran1eDecoder添加到对应ChanneIPipeline的起始位即可。
熟悉了Netty的NIO基础应用开发之后,从第三部分开始,我们继续学习编解码技术。在了解编解码基础知识之后,继续学习Netty内置的编解码框架的使用,例如Java序列化、二进制编解码、谷歌的protobuf和JBoss的Marshalling序列化框架。

时间: 2024-08-14 09:31:59

(入门篇 NettyNIO开发指南)第四章-分隔符和定长解码器使用的相关文章

(入门篇 NettyNIO开发指南)第四章-TIP黏包/拆包问题解决之道

熟悉TCP编程的读者可能都知道,无论是服务端还是客户端,当我们读取或者发送消息的时候,都需要考虑TCP底层的粘包/拆包机制.木章开始我们先简单介绍TCP粘包/拆包的基础知识,然后模拟一个没有考虑TCP粘包/拆包导致功能异常的案例,最后通过正确例米探讨Netty是如何解决这个问题的.如果你已经熟悉了TCP粘包和拆包的相知识,建议你直接跳到代码讲解小节,看Netty是如何解决这个问题的.本章主要内容包: TCP粘包/拆包的基础知识 没考虑TCP粘包/拆包的问题案例 使用Netty解决读半包问题 4.

(入门篇 NettyNIO开发指南)第三章-Netty入门应用

作为Netty的第一个应用程序,我们依然以第2章的时间服务器为例进行开发,通过Netty版本的时间服务报的开发,让初学者尽快学到如何搭建Netty开发环境和!运行Netty应用程序. 如果你已经熟悉Netty    的基础应用,可以跳过本章,继续后面知识的学习.本章主要内容包括:.Netty开发环境的搭建.服务端程序TimeServer开发.客户端程序TimeClient开发时间服务器的运行和调试 3.1    Netty开发环境的搭建 首先假设你已经在本机安装了JDKI.7贯配置了JDK的环境

IOS开发指南第四章 IOS8多分辨率屏幕适配 学习

1 获取IOS设备屏幕信息 CGSize iOSDeviceScreenSize = [UIScreen mainScreen].bounds.size; NSString *s = [NSString stringWithFormat:@"%.0f x %.0f", iOSDeviceScreenSize.width, iOSDeviceScreenSize.height]; 获取设备信息判断是否是ipone-判断横屏还是竖屏-判断设备型号 属性userInterfaceIdiom是

Knockout应用开发指南 第四章:模板绑定

模板绑定The template binding 目的 template绑定通过模板将数据render到页面.模板绑定对于构建嵌套结构的页面非常方便.默认情况, Knockout用的是流行的jquery.tmpl模板引擎.使用它的话,需要在安装页面下载和引用jquery.tmpl和jQuery框架.或者你也可以集成其它的模板引擎(虽然需要了解Knockout 内部知识才行). 例子 <div data-bind='template: "personTemplate"'> &

Android艺术开发探索第四章——View的工作原理(下)

Android艺术开发探索第四章--View的工作原理(下) 我们上篇BB了这么多,这篇就多多少少要来点实战了,上篇主席叫我多点自己的理解,那我就多点真诚,少点套路了,老司机,开车吧! 我们这一篇就扯一个内容,那就是自定义View 自定义View 自定义View的分类 自定义View的须知 自定义View的实例 自定义View的思想 一.自定义View的分类 自定义View百花齐放,没有什么具体的分类,不过可以从特性大致的分为4类,其实在我看来,就三类,继承原生View,继承View和继承Vie

MiS603开发板 第四章 流水灯实验

作者:MiS603开发团队 日期:20150911 公司:南京米联电子科技有限公司 论坛:www.osrc.cn 网址:www.milinker.com 网店:http://osrc.taobao.com EAT博客:http://blog.chinaaet.com/whilebreak 博客园:http://www.cnblogs.com/milinker/ MiS603开发板 第四章 流水灯实验 关于流水灯,网上有太多的例子了.其实只要掌握上面分频计数的技巧,设计流水灯是件极其简单的事情.从

Python编程从入门到实践(第三、四章的列表和元祖) &#142015;

原文: http://blog.gqylpy.com/gqy/414 置顶:来自一名75后老程序员的武林秘籍--必读(博主推荐) 来,先呈上武林秘籍链接:http://blog.gqylpy.com/gqy/401/ 你好,我是一名极客!一个 75 后的老工程师! 我将花两分钟,表述清楚我让你读这段文字的目的! 如果你看过武侠小说,你可以把这个经历理解为,你失足落入一个山洞遇到了一位垂暮的老者!而这位老者打算传你一套武功秘籍! 没错,我就是这个老者! 干研发 20 多年了!我也年轻过,奋斗过!我

蚂蚁区块链BaaS平台应用开发指南(四):JavaSDK的接入

在尝试本节的样例代码前,需要保证目标智能合约已经按照蚂蚁区块链BaaS平台应用开发指南(三):从一个简单合约开始中的做法编译部署成功. 基于JavaSDK的接入 在上一节里,我们通过Cloud IDE部署了一个最简单的智能合约,并且通过Cloud IDE成功的调用了合约的方法.拿传统应用的开发来类比,这就像在数据库上增加了一个存储过程,然后通过外部应用来触发这个存储过程的执行.那么,对于区块链来说,外部应用又如何来调用部署好的智能合约?在这一节中,我们将会通过蚂蚁区块链提供到JavaSDK来接入

Android艺术开发探索第四章——View的工作原理(上)

这章就比较好玩了,主要介绍一下View的工作原理,还有自定义View的实现方法,在Android中,View是一个很重要的角色,简单来说,View是Android中视觉的呈现,在界面上Android提供了一套完整的GUI库,里面有很多控件,但是有时候往往并不能满足于需求,所以只有自定义View了,我们会简单的说下流程,然后再去实践除了View的三大流程之外,View常见的回调方法也是必须掌握的,比如构造方法,onAttach,onVisibilityChanged,onDetach,另外对于一些