前言

在经历了自己设计字库格式、编写字库生成工具、编写纯py版本的字库查询函数库之后,发现几经优化的查询函数库依然没办法满足速度需求。没办法,只好试试原生mpy模块能否提高运行效率。

比较坑的是官方文档对于编写原生模块的介绍只有一篇文档,然后丢了几个example就完事,只能靠自己摸索。这里记录下来方便以后查阅。

原生模块的特点和限制

原生模块的特点:

  • 原生模块可以像正常mpy模块一样被import,只需要将编译出来的.mpy文件放到对应的路径即可。只要CPU架构相同,原生模块可以通用,无需编译整个micropython的固件
  • 使用原生模块能够让你更好地控制内存使用,提高速度和内存利用率。
  • 使用原生模块可以控制mpy中几乎所有的对象。

但是原生模块也有不少限制:

  • 原生模块不能访问系统API,只能使用mpy的动态运行时的API,这也是原生模块能在相同架构CPU的机器下通用的原因。
  • 需要手动管理内存,在动态申请内存之后,要记得尽早释放。(除非是将申请的内存转换成mpy对象并交给mpy管理,比如通过byte数组创建的bytearray作为返回值返回,此时不需要释放byte数组的内存)。
  • 不支持Data Section和静态BSS变量,这个我也没看懂,总之所有的静态对象都要在全局定义,影响不大。

准备工作

首先需要获取一份MicroPython的源码。

git clone --recursive https://github.com/micropython/micropython.git

这里没必要使用国内镜像加速,因为micropython项目有不少git子模块,都是从github获取的,配置一个好一点儿的代理更靠谱。

然后要准备对应平台的编译工具,比如我用的ESP32开发版,就下载官方的ESP-IDF工具(ESP23-PORT配置工具链指南)。确保编译工具链在PATH当中,全局可用。

编译第一个mpy原生模块

复制一份 /micropython/examples/natmod/features0 目录,这是最简单的原生模块的例子。可以看到例子很短,特别是Makefile十分简单。

修改Makefile中MPY_DIR和ARCH部分,改成自己的micropython源码目录,和单片机的架构,在交叉编译工具链全局可用的情况下,直接执行make就可以编译。不出意外的话,会在目录下生成features0.mpy文件。

测试编译出来的原生模块

将原生模块复制到单片机目录下(可以用ampy或者mpfshell工具上传文件),然后执行下面的代码:

import features0
sum = features0.factorial(3) # some int
print(sum)

输出的数字就是斐波那契数列计算的结果,可以查看源代码获取具体的算法。

小结

千里之行,始于足下。到这里,已经成功编译了一个原生mpy模块,并且实现了int型参数的传递与返回。

此时,数据已经可以在mpy运行时和原生运行时之间传递了。接下来所有的工作就是围绕如何丰富这个简单的模块而展开的。

1. 悬浮窗口的实现

使用悬浮窗口之前,要向用户申请权限

//在清单文件中定义权限
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>

目标版本安卓6.0以上,还要在运行时申请权限

//在运行时申请权限
if (!Settings.canDrawOverlays(this)){
    Intent floatPermission = new Intent(
        Settings.ACTION_MANAGE_OVERLAY_PERMISSION, //不同于一般的权限,悬浮窗口权限是在独立的界面请求的
        Uri.parse("package:" + getPackageName())); //带上自己的包名,打开自己软件的权限设置界面
    startActivityForResult(floatPermission,REQ_FLOAT_PERMISSION); //打开设置界面
}

悬浮窗口的实现原理是利用WindowManager直接向屏幕添加视图

//获取WindowManager
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
//适应安卓API的变化
int DEFAULT_WINDOW_TYPE;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    DEFAULT_WINDOW_TYPE = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; //最新安卓版本将应用的悬浮窗口独立出来了,使用专用的窗口类型
}else{
    DEFAULT_WINDOW_TYPE = WindowManager.LayoutParams.TYPE_PHONE; //早期安卓版本,悬浮窗口使用PHONE窗口类型
}
//定义视图属性
lp = new WindowManager.LayoutParams();
lp.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL //悬浮窗口不会占满屏幕,所以设置为视图外部不拦截点击事件
        |WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN //限制窗口显示在屏幕之类,按需设置
        |WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; //禁止悬浮窗口获取输入焦点,否则返回键之类的按键事件在别的应用程序中会失效,而且别的应用也不能输入文字
lp.format = PixelFormat.RGBA_8888; //设置颜色格式,支持透明
lp.gravity = Gravity.LEFT|Gravity.TOP; //重力左上角,方便调整位置和大小
lp.width = 320; //窗口宽320个像素
lp.height = 320; //窗口高320个像素
lp.x = 0; //距离屏幕左边0像素
lp.y = 0; //距离屏幕上方0像素
//这里的view可以是任意View类对象
//将视图属性赋给视图对象,并添加到屏幕上去
wm.addView(view, lp);
//使用结束,将视图从屏幕上移除
wm.removeView(view);

这里为了适应新版安卓,早期安卓使用TYPE_PHONE作为窗口类型,较新的安卓使用TYPE_APPLICATION_OVERLAY作为窗口类型

2. 用户交互事件监听

像正常的视图对象一样,调用setOnClickListener之类的方法即可实现点击事件的监听

如果要监听窗口拖动的事件,最好在界面上布置一个透明Button,在其上面监听touch事件。实测的时候,直接在LinearLayout或TextView对象上面监听touch事件,只能收到touch_down回调,touch_move和touch_up事件都收不到。

3. 封装及使用

先来看看封装过后绘制出来的悬浮窗口长啥样:3
显示悬浮窗口
屏幕截图.png
调整大小事件
屏幕截图.png
拖动事件
屏幕截图.png
以下是封装的代码


import android.content.Context;
import android.graphics.Color;
import android.graphics.PixelFormat;
import android.os.Build;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.FrameLayout;


/*
 * Manage the float windows easily
 * Make the window looks like the window on Windows  :p
 * Created by Dreagonmon on 2017/1/15.
 * 记得先申请悬浮窗权限!如果只想用APPLICATION层面的窗口,请修改DEFAULT_WINDOW_TYPE
 */

public class FloatWindow
{
    public final int DEFAULT_WINDOW_TYPE;
    public final int scrWidth;
    public final int scrHeight;
    public int minWidth;
    public int maxWidth;
    public int minHeight;
    public int maxHeight;
    private final WindowManager wm;
    private final Context context;
    private boolean isShowing = false;
    private View view;//内容对象
    private View window;//窗口对象
    private WindowManager.LayoutParams lp;
    private TouchListenerMove listenerMove = new TouchListenerMove();
    private TouchListenerResize listenerResize = new TouchListenerResize();
    private Runnable onPressAction;//点击事件
    public FloatWindow(Context context)
    {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            DEFAULT_WINDOW_TYPE = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
        }else{
            DEFAULT_WINDOW_TYPE = WindowManager.LayoutParams.TYPE_PHONE;
        }
        this.wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        this.context = context;
        DisplayMetrics DM = new DisplayMetrics();
        wm.getDefaultDisplay().getMetrics(DM);
        maxWidth = scrWidth = DM.widthPixels;
        maxHeight = scrHeight = DM.heightPixels;
        minWidth = 0;
        minHeight = 0;
        readyLp();
    }

    public FloatWindow(Context context,View view)
    {
        this(context);
        this.setView(view);
    }
    private void readyLp()
    {
        lp = new WindowManager.LayoutParams();
        lp.type = DEFAULT_WINDOW_TYPE;//悬浮窗口类型,记得给予权限
        //背景可点击,保持在屏幕内
        lp.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                |WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
                |WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
        lp.format = PixelFormat.RGBA_8888;//透明背景
        lp.gravity = Gravity.LEFT|Gravity.TOP;//重力左上角,方便调整大小
        lp.width = scrWidth;
        lp.height = scrHeight;
        lp.x = 0;
        lp.y = 0;
    }


    /*内部元素操作类方法*/
    /*
    * setWindow()与setView()的区别:
    * 一个会自动包裹进窗口内部,一个就是单纯的传入视图
    * 当使用了setView的时候,getWindow()与getView()效果一样,
    * 当使用了setWindow的时候,getWindow()返回窗口整体,getView()返回之前包裹的视图,*/
    public View setWindow(int styleLayoutID,int containerID, View v)
    {
        /*主要内容容器
        * 自定义layout布局,为styleLayoutID所指定的布局
        * 最后自由添加内容的FrameLayout容器的ID为window_container*/
        if (window == null||window.getId()!=styleLayoutID)
        {
            window = LayoutInflater.from(context).inflate(styleLayoutID,null);
        }
        FrameLayout container = (FrameLayout) window.findViewById(containerID);
        v.setLayoutParams(new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,FrameLayout.LayoutParams.MATCH_PARENT));
        container.removeAllViews();
        container.addView(v);
        this.view = v;
        return window;
    }
    public View getWindow()
    {
        return window;
    }
    public void setView(View view)
    {
        this.view = window = view;
    }
    public View getView()
    {
        return view;
    }
    public View getElementById(int ID)
    {
        return window.findViewById(ID);
    }
    public WindowManager.LayoutParams getLayoutParams()
    {
        return lp;
    }
    public void updateWindow()//配合取出的LayoutParams使用,记得改过lp之后更新窗口!
    {
        if (isShowing)
        {
            wm.updateViewLayout(window,lp);
        }
    }
    public boolean isShowing()
    {
        return isShowing;
    }


    /*窗口设置类方法*/
    /*
    * 这几个事件最好是由窗口样式中的元素来设置,这样在更改内容View之后不需要再次设置
    * OnPressAction仅作用于设置的MoveView,拖动距离不远的话算作点击,
    * 该方法的调用在主线程,不要放置网络连接等操作,如果想取消点击事件,传入null即可
    * Move和Resize被限制在屏幕之内,这是为了保证窗口的稳定
    * 移动和改变大小事件推荐设置在Button上,TextView对touch事件的触发不完整
    * 只有touch_down事件触发了回调*/
    public void setMoveView(View v)
    {
        v.setOnTouchListener(listenerMove);
    }
    public void setMoveView(int ID)
    {
        View v = window.findViewById(ID);
        if(v != null)
        {
            v.setOnTouchListener(listenerMove);
        }
    }
    public void setResizeView(View v)
    {
        v.setOnTouchListener(listenerResize);
    }
    public void setResizeView(int ID)
    {
        View v = window.findViewById(ID);
        if(v != null)
        {
            v.setOnTouchListener(listenerResize);
        }
    }
    public void setOnPressAction(Runnable run)
    {
        onPressAction = run;
    }


    /*窗口行为类方法*/
    /*记得先resize再move
    * 为了保证所有按钮可用,这里的移动和调整大小的事件被限制为在屏幕之内*/
    public void moveTo(int x,int y)
    {
        lp.x = x<0?0:(x+lp.width<=scrWidth?x:scrWidth-lp.width);
        lp.y = y<0?0:(y+lp.height<=scrHeight?y:scrHeight-lp.height);
        if (isShowing)
        wm.updateViewLayout(window,lp);
    }
    public void moveBy(float xOffset,float yOffset)
    {
        lp.x =(int) (lp.x+xOffset<0?0:(lp.x+lp.width+xOffset<=scrWidth?lp.x+xOffset:scrWidth-lp.width));
        lp.y =(int) (lp.y+yOffset<0?0:(lp.y+lp.height+yOffset<=scrHeight?lp.y+yOffset:scrHeight-lp.height));
        if (isShowing)
        wm.updateViewLayout(window,lp);
    }
    public void resizeTo(int width,int height)
    {
        lp.width = width<minWidth?minWidth:(width<=maxWidth-16?width:maxWidth);
        lp.height = height<minHeight?minHeight:(height<=maxHeight-16?height:maxHeight);
        if(isShowing)
        wm.updateViewLayout(window,lp);
    }
    public void resizeBy(float xOffset,float yOffset)
    {
        lp.width =(int) (lp.width+xOffset<minWidth?minWidth:(lp.width+xOffset<=maxWidth?lp.width+xOffset:maxWidth));
        lp.height =(int) (lp.height+yOffset<minHeight?minHeight:(lp.height+yOffset<=maxHeight?lp.height+yOffset:maxHeight));
        if (isShowing)
        wm.updateViewLayout(window,lp);
    }
    public void show()
    {
        if (!isShowing)
        {
            isShowing = true;
            wm.addView(window, lp);
        }
    }
    public void hide()
    {
        if (isShowing)
        {
            isShowing = false;
            wm.removeView(window);
        }
    }

    /*设计或许会用到的dp转px*/
    public static int dp2px(Context context, float dipValue)
    {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int)(dipValue * scale + 0.5f);
    }

    /*默认执行了移动和改变大小的点击事件后会移到最上层*/
    class TouchListenerMove implements View.OnTouchListener
    {
        float lX,lY;
        int startX,startY;
        @Override
        public boolean onTouch(View view, MotionEvent motionEvent) {
            switch (motionEvent.getAction())
            {
                case MotionEvent.ACTION_DOWN:
                    lX = motionEvent.getRawX();
                    lY = motionEvent.getRawY();
                    startX = lp.x;
                    startY = lp.y;
                    break;
                case MotionEvent.ACTION_MOVE:
                    moveBy(motionEvent.getRawX()-lX,motionEvent.getRawY()-lY);
                    lX = motionEvent.getRawX();
                    lY = motionEvent.getRawY();
                    break;
                case MotionEvent.ACTION_UP:
                    lX = lY = 0;
                    if (Math.abs(lp.x-startX)<5&&Math.abs(lp.y-startY)<5)
                    {
                        moveTo(startX,startY);
                        if (onPressAction!=null)
                        {
                            onPressAction.run();
                        }
                    }
                    //hide();
                    //show();
                    break;
            }
            return false;
        }
    }
    class TouchListenerResize implements View.OnTouchListener
    {
        View tmpView;
        WindowManager.LayoutParams tmpLp = new WindowManager.LayoutParams();
        float lX,lY;
        private void resizeTmpView(float xOffset,float yOffset)
        {
            tmpLp.width =(int) (tmpLp.width+xOffset<minWidth?minWidth:(tmpLp.width+xOffset<=maxWidth?tmpLp.width+xOffset:maxWidth));
            tmpLp.height =(int) (tmpLp.height+yOffset<minHeight?minHeight:(tmpLp.height+yOffset<=maxHeight?tmpLp.height+yOffset:maxHeight));
            if (isShowing)
                wm.updateViewLayout(tmpView,tmpLp);
        }
        @Override
        public boolean onTouch(View view, MotionEvent motionEvent) {
            switch (motionEvent.getAction())
            {
                case MotionEvent.ACTION_DOWN:
                    lX = motionEvent.getRawX();
                    lY = motionEvent.getRawY();
                    if (tmpView==null)
                    {
                        tmpView = new View(context);
                        tmpView.setBackgroundColor(Color.parseColor("#800080FF"));
                    }
                    tmpLp.x = lp.x;
                    tmpLp.y = lp.y;
                    tmpLp.width = lp.width;
                    tmpLp.height = lp.height;
                    tmpLp.type = lp.type;
                    tmpLp.flags = lp.flags;
                    tmpLp.format = lp.format;
                    tmpLp.gravity = lp.gravity;
                    wm.addView(tmpView,tmpLp);
                    break;
                case MotionEvent.ACTION_MOVE:
                    resizeTmpView(motionEvent.getRawX()-lX,motionEvent.getRawY()-lY);
                    lX = motionEvent.getRawX();
                    lY = motionEvent.getRawY();
                    break;
                case MotionEvent.ACTION_UP:
                    lX = lY = 0;
                    resizeTo(tmpLp.width,tmpLp.height);
                    wm.removeView(tmpView);
                    //hide();
                    //show();
                    break;
            }
            return false;
        }
    }
}

以下是使用的代码

FloatWindow fw = new FloatWindow(this);
fw.setWindow(R.layout.float_window_frame,R.id.window_container,new TextView(this));
fw.setMoveView(R.id.window_move);
fw.setResizeView(R.id.window_resize);
fw.minWidth = fw.minHeight = 320;
fw.resizeTo(480,640);
fw.moveTo(240,320);
fw.show();


pushState方法实例与PJAX

平时没有注意过,其实推特的网站用了很多HTML5的新技术。
每次点击链接的时候,页面并不是全部刷新的,而是像AJAX那样,局部刷新的。
如果单独使用AJAX,无论局部刷新多少次,只要用户一后退,页面变化的历史就都没了。
而这里和AJAX不同的是,当点击浏览器上的后退按钮时,页面会回到上一个局部刷新时的状态。
这里用到了HTML5的一个新特性,history对象的pushState、replaceState相关操作。
这种结合了AJAX、pushState的技术,简称为PJAX

使用pushState应用的设计

最简单的一种应用,就是保存当前网页的状态。
比如用户点开了网页上的一个面板,这个过程并不需要向服务器发送请求,只需要用户后退的时候关闭面板就好。
这种时候,只要在网页加载完成时替换一个初始状态,用户打开面板时压入一个状态,在用户后退时回到初始状态即可。
推特之类的网站就是结合了AJAX使用的pushState方法。
用户点击链接的时候,将URL压入历史纪录,这样地址栏里的内容就变成了新页面的URL。
然后使用AJAX,带上特殊的请求头,向服务器请求页面数据,如此就不需要请求整个页面了。
用户后退的时候,根据历史纪录里的URL,使用AJAX重新加载局部数据即可。服务端可根据这个请求头判断,是返回完整的网页还是网页部分数据。
也可以将AJAX请求到的数据放在localStorage中,后退时直接调取而不是重新请求。

pushState实际应用

这里用一个+1程序作为例子
首先写一个数字+1的方法

//喜加一
addone = function(){
    //模拟页面数据改变
    value = parseInt(document.getElementById('display').innerHTML);
    value = value+1;
    //压入历史纪录
    history.pushState({v:value},value+'','?count='+value);
    //pushState并不会触发onpopstate回调
    //手动调用来触发加载数据的动作
    window.onpopstate();
}

pushState方法有三个参数:
第一个参数是一个自定义对象,之后可以使用history.state获取
第二个参数是状态名称
第三个参数是URL地址,用于浏览器地址栏的显示,可以通过windows.location获取
然后完成onpopstate回调,实现加载数据以及更新页面:

window.onpopstate = function(){
    //这里的data对象就是pushState传入的第一个对象,只读
    data = history.state;
    //可以用ajax加载数据了
    url = window.location.toString();
    //......
    //加载完之后更新页面
    document.getElementById('display').innerHTML = data.v + '';
    //AJAX是异步加载的,更新页面之前最好检查一下location
}

页面写到这里会有一个问题,就是在退回初始状态时,history.state对象未定义,所以要给页面一个初始历史:

history.replaceState({v:0},'0','?count=0');
//手动调用来触发加载数据的动作
window.onpopstate();

这样就实现了一个点击+1的页面,后退的话就会"-1",完整的页面代码如下:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>历史纪录测试</title>
</head>
<body>
    <script>
        //当后退或前进时触发事件
        window.onpopstate = function(){
            //这里的data对象就是pushState传入的第一个对象,只读
            data = history.state;
            //可以用ajax加载数据了
            url = window.location.toString();
            //......
            //加载完之后更新页面
            document.getElementById('display').innerHTML = data.v + '';
            //AJAX是异步加载的,更新页面之前最好检查一下location
        }
        //喜加一
        addone = function(){
            //模拟页面数据改变
            value = parseInt(document.getElementById('display').innerHTML);
            value = value+1;
            //压入历史纪录
            history.pushState({v:value},value+'','?count='+value);
            //pushState并不会触发onpopstate回调
            //手动调用来触发加载数据的动作
            window.onpopstate();
        }
        //初始化历史纪录栈
        history.replaceState({v:0},'0','?count=0');
        //手动调用来触发加载数据的动作
        window.onpopstate();
    </script>
    <button onclick="addone();">喜加一</button>
    <pre id="display">0</pre>
</body>
</html>

全剧终

Java9新特性——模块化

Java9引入了模块化的特性,利用这个特性可以创建小型的JRE来发布应用程序,减少JRE体积,提高资源利用率

将应用打包成模块

  1. 在源码根路径新建module-info.java来配置模块信息

    module 模块名(用于其它应用导入,以及运行环境打包) {
        //requires声明依赖模块
        requires java.base;
        //transitive关键字将依赖的模块传递出去,导入当前模块的时,无需再次导入即可以使用这个依赖的模块
        requires transitive java.desktop;
        //exports声明导出的模块,即外部工程导入本模块之后,可见的包名,其他包名将对外隐藏
        exports top.dreagonmon.app.test;
    }
  2. 编译源码,生成字节码.class文件(生成的字节码中有module-info.class文件)
  3. 将字节码打包成jmod模块包

    jmod create --class-path <字节码所在的路径> <输出的jmod文件路径>
  4. 根据jmod生成运行环境

    jlink --output <输出目录名> --add-modules <应用模块名> --module-path <jmod文件所在的路径>

    可以不包含java自带模块的路径,正确配置的JAVA环境会自动发现这些模块

  5. 运行应用的主类

    cd <输出目录>/bin/java 主类类名(一定要将主类导出,才可以从外部执行)

java对象序列化学习

由于开发中的需要,要将数据变成字节流之后分片传输,学习一下Java里面将对象序列化的方法。

1.简单序列化的实现

最简单的序列化实现的方法,就是自定义数据类的时候继承java.io.Serializable接口。
所有继承了这个接口的类都可以被序列化,用ObjectOutputStream和ObjectInputStream可以分别将对象输出和输入。

public class myObject implements java.io.Serializable{
    //这是一个空接口,所以不需要任何实现,只是告诉别人这个类可以序列化
    //所有基本类型默认实现了Serializable接口,所以都是可以序列化的
    int arg1;
    String msg;
    
    //可以序列化的类型的数组也可以序列化
    byte[] data;
    float[] d3;
    
    //如果下面的类实现了Serializable接口,则可以序列化
    MyAnotherClass obj;

    //对于不知道是否可以序列化的类要慎用,不然会出错
    UnknowClass uobj;//可能会在序列化时报错!!!
}

如果一个类声明实现了Serializable接口,那么它里面的所有属性都必须可以序列化,否则会在序列化的时候报错!

2.自己控制序列化的实现

很多时候,在对象序列化的时候,我们不希望某些敏感属性被序列化,或者某些属性不能被自动序列化,这时候就需要我们手动实现序列化了。
手动实现序列化需要自定义类实现java.io.Externalizable接口,然后重写相关的方法,来完成对象的序列化与反序列化。

int arg1;
long time;
String msg;
@Override
public void readExternal(ObjectInput in)
    throws IOException, ClassNotFoundException {
    //在这里对对象进行反序列化
    //注意调用readXXX()与writeXXX()的先后顺序!
    time = in.readLong();
    msg = (String) in.readObject();//String用Object来读
    arg1 = in.readInt();
}

@Override
public void writeExternal(ObjectOutput out) throws IOException {
    //在这里对对象进行序列化
    //注意调用readXXX()与writeXXX()的先后顺序!
    out.writeLong(time);
    out.writeObject(msg);//String用Object来写
    out.writeInt(arg1);
}

手动序列化的时候,按照一定的顺序调用writeXXX()和readXXX()的方法,将需要的属性序列化。
序列化与反序列化时,先序列化的就先反序列化,上面的例子中,序列化的顺序是long、String、int,所以反序列化的顺序就是long、String、int。
序列化生成的对象,并不会调用类的构造方法,所以要在这两个方法中对其他属性进行必要的初始化。

3.serialVersionUID的特殊用处

在自定义类的时候,只要更改了一点点的源文件,编译出来的类便与之前序列化出来的数据流不兼容了,无法完成反序列化。
serialVersionUID的作用就是标识当前类的版本号。
版本号相同的类可以序列化与反序列化,反之则不行。
所以如果有可能,自定义的所有需要序列化的类都应该定义一个serialVersionUID属性

private static final long serialVersionUID = -5314860580001L;

通过控制serialVersionUID来控制序列化的流与类的兼容性。
只要serialVersionUID相同,无论类里的方法如何变,对应的属性总能正确地完成序列化与反序列化。

4.可以序列化的属性

并不是所有的属性都可以用Serializable接口自动序列化,自动序列化的属性需要满足以下条件:

  • 该属性的类本身可以序列化
  • 该属性不是静态(static)属性
  • 该属性不是瞬时(transient)属性