• 随手翻译的文档,要看懂linux内核,内核附带的文档自然是逃不过😂,全英文配合Google翻译也得上。。。。

  • 最后更新时间 10/05/2007

  • linux看门狗设备API

  • Copyright 2002 Christer Weingel wingel@nano-system.com

  • 本文档的一些内容来自 sbc60xxwdt ,版权属于 Copyright 2000 Jakob Oestergaard jakob@ostenfeld.dk

  • 本文档基于linux内核 2.4.18


介绍

  • 你应该已经知道了,看门狗定时器(WDT)是一个硬件复位电路,在系统软件出现故障时复位系统。
  • 通常,用户空间的定期喂狗程序通过/dev/watchdog的设备文件通知内核看门狗驱动程序。驱动收到通知,会告知硬件看门狗一切正常,硬件看门狗需要等一会再复位系统。如果用户空间通知失败(RAM错误、内核漏洞等等),通知将停止。超时后,硬件看门狗将复位系统(重启)。
  • linux看门狗的API是相当特殊的结构,不同的驱动程序对API的执行各不相同,有时会碰到不兼容的情况。本文档参数记录现有的情况,以供未来的驱动开发参考。

最简单的API

  • 所有驱动都支持的基本操作模式。/dev/watchdog被打开后,看门狗被激活并复位系统,除非在一小段时间内被ping通?(喂狗),这段时间被称为超时时间或间断时间。简单的喂狗的方式是向驱动程序写入一些数据,一个非常简单的实例在see samples/watchdog/watchdog-simple.c下.
  • 更高级的驱动程序可以做到例如检查一个http服务器,在执行喂狗操作之前,做出回应。
  • /dev/下设备节点被关闭后,看门狗也同时被禁用,除非支持“Magic Close”(见下文),这并不是个好办法。因为如果看看门狗demo有bug导致系统崩溃,但系统不会重启。因为这样一些驱动支持”Disable watchdog shutdown on close”, CONFIG_WATCHDOG_NOWAYOUT”这样的配置选项。一旦编译内核时候启用这些选项,一旦看门狗程序激活,就不可能禁用看门狗。如果看门狗demo崩溃,系统会在超时后自动重启。看门狗驱动通常也支持nowayout module parameter,以便在运行时控制nowayout module。

Magic Close feature

  • 如果驱动程序支持Magic Close,那么设备将不支持禁用看门狗,除非在关闭设备文件前,向/dev/watchdog下设备文件写入特定的magic 字符 ‘V’。如果用户空间的 daemon关闭了看门的设备文件,但是没有发送那个特定的字符,驱动会认为daemon进程已经被杀,并且停止了定期喂狗行为。这样会导致超时后系统重启。

ioctl API

  • 所有的驱动程序都支持ioctl API

  • 喂狗操作使用ioctl完成

  • 所有的驱动都要支持一个KEEPALIVE的ioctl命令,这个命令可以起到向驱动写入数据一样的作用。所以watchdog daemon的主函数循环可以这样实现

    1
    2
    3
    4
    while (1) {
    ioctl(fd, WDIOC_KEEPALIVE, 0);
    sleep(10);
    }

Setting and getting the timeout

  • 某些驱动支持运行时通过SETTIMEOUT ioctl命令修改超时时间,这些驱动都有WDIOF_SETTIMEOUT的标志位_。超时时间是一个单位为秒的整数,驱动程序会返回这个变量实际设置的数值,但是由于硬件限制,返回的数值可能与设置数值不同。

    1
    2
    3
    int timeout = 45;
    ioctl(fd, WDIOC_SETTIMEOUT, &timeout);
    printf("The timeout was set to %d seconds\n", timeout);
  • 这个实例或许会输出”The timeout was set to 60 seconds”,如果设备超时时间为分钟。

  • 自从2.4.18内核开始,可以使用GETTIMEOUT ioctl命令,获取超时时间。

    1
    2
    ioctl(fd, WDIOC_GETTIMEOUT, &timeout);
    printf("The timeout was is %d seconds\n", timeout);

Pretimeouts

  • 一些看门狗定时器可以在系统重启前一段时间设置一个触发器。这样运行linux在系统从前记录一些重要的信息(像panic信息和kernel coredumps),具体可以通过NMT、中断等机制实现。

    1
    2
    pretimeout = 10;
    ioctl(fd, WDIOC_SETPRETIMEOUT, &pretimeout);
  • 请注意,pretimeout 时间是在 超时关闭系统 之前的秒数。不是直到发生pretimeout 事件的秒数。例如设定的超时时间是60s, pretimeout是10s。pretimeout事件会在50s时发生。pretimeout设置为0代表禁用pretimeout。

  • 同样存在一个获取pretimeout的ioctl命令。

    1
    2
    ioctl(fd, WDIOC_GETPRETIMEOUT, &timeout);
    printf("The pretimeout was is %d seconds\n", timeout);
  • 不是所有看门狗驱动都支持 pretimeout

获取重启前秒数

  • 一些看门狗驱动支持获取系统重启前秒数。通过WDIOC_GETTIMELEFT ioctl_命令可以返回系统重启前秒数。

    1
    2
    ioctl(fd, WDIOC_GETTIMELEFT, &timeleft);
    printf("The timeout was is %d seconds\n", timeleft);

环境监测

  • 所有的看门狗驱动都被要求返回更多关于系统最后一次重启的信息,像是温度、风扇转速、电源等。GETSUPPORT ioctl 的命令可以返回 设备可以支持的信息。

    1
    2
    struct watchdog_info ident;
    ioctl(fd, WDIOC_GETSUPPORT, &ident);
  • 返回的结构ident中的字段如下:

    1
    2
    3
    4
    5
    identity  描述watchdog driver的字符串

    firmware_version the firmware version of the card if available
    options 设备支持情况的标志位

  • options描述了GET_STATUS 和GET_BOOT_STATUS ioctl命令_可以返回那些信息。并且可以设置。()**??无法理解什么意思??**

  • WDIOF_OVERHEAT cpu过热复位_
    系统重启因为,超过了温度限制上限。

  • WDIOF_FANFAULT 风扇失效复位_

  • WDIOF_EXTERN1 External relay 1_
    外部监控电源1被触发,???Controllers intended for real world applications include external monitoring pins that will trigger a reset.??(不明白什么意思)

  • WDIOF_EXTERN2 External relay 2
    外部监控电源2被触发

  • WDIOF_POWERUNDER 电源坏了,电源带不动了
    机器显示欠压。


  • 后面一点不翻了,有时间再添坑

  • WDIOF_CARDRESET Card previously reset the CPU
    The last reboot was caused by the watchdog card

  • WDIOF_POWEROVER Power over voltage
    The machine is showing an overvoltage status. Note that if one level is under and one over both bits will be set - this may seem odd but makes sense.

  • WDIOF_KEEPALIVEPING Keep alive ping reply
    The watchdog saw a keepalive ping since it was last queried.

  • WDIOF_SETTIMEOUT Can set/get the timeout
    The watchdog can do pretimeouts.

  • WDIOF_PRETIMEOUT Pretimeout (in seconds), get/set
    For those drivers that return any bits set in the option field, the GETSTATUS and GETBOOTSTATUS ioctls can be used to ask for the current status, and the status at the last reboot, respectively.

    1
    2
    3

    int flags;
    ioctl(fd, WDIOC_GETSTATUS, &flags);

    or

    1
    ioctl(fd, WDIOC_GETBOOTSTATUS, &flags);
  • Note that not all devices support these two calls, and some only support the GETBOOTSTATUS call.

  • Some drivers can measure the temperature using the GETTEMP ioctl. The returned value is the temperature in degrees fahrenheit.

    1
    2
    int temperature;
    ioctl(fd, WDIOC_GETTEMP, &temperature);
  • Finally the SETOPTIONS ioctl can be used to control some aspects of the cards operation.

    1
    2
    int options = 0;
    ioctl(fd, WDIOC_SETOPTIONS, &options);
  • The following options are available:

    • WDIOS_DISABLECARD Turn off the watchdog timer
    • WDIOS_ENABLECARD Turn on the watchdog timer
    • WDIOS_TEMPPANIC Kernel panic on temperature trip

  • 正式开始让人崩溃的linux系列,希望自己能写完。
    先拿rtc开刀。

  • 这里我尽可能记录下思维的细节,而不是仅局限于代码,希望自己能领会Linux内核开发者的想法。

  • 内核版本:linux4.1

  • 需要了解:简略了解字符驱动 和 Linux设备驱动模型


RTC

  • rtc即real time clock,实时时钟。
  • rtc一般负责系统关机后计时,面对繁多的Linux RTC设备,内核干脆提供了一个rtc子系统,来支持所有的rtc设备。
  • 参考资料

rtc子系统

  • rtc设备本质上是一个字符设备,rtc子系统在字符设备的基础上抽象与硬件无关的部分,并在这个基础上拓展sysfs和proc文件系统下的访问。
  • 分析时候始终记住两点:
    • rtc子系统是为了让rtc设备驱动编写更为简单,与硬件无关部分已被抽离。
    • rtc子系统是基于字符设备而来的。

文件框架

  • rtc子系统的源码在 /drivers/rtc
    1.jpg
    删减了很多rtc-xxx.c的驱动,只留下了ds1307作为示例,这里看到实际上代码并不多。

  • 具体文件分析

    • rtc.h:定义与RTC有关的数据结构。

    • class.c:向内核注册RTC类,为底层驱动提供rtc_device_register与rtc_device_unregister接口用于RTC设备的注册/注销。初始化RTC设备结构、sysfs、proc。

    • Interface.c:提供用户程序与RTC的接口函数,其中包括ioctl命令。

    • rtc-dev.c:将RTC设备抽象为通用的字符设备,提供文件操作函数集。

    • rtc-sysfs.c:管理RTC设备的sysfs属性,获取RTC设备名、日期、时间等。

    • rtc-proc.c:管理RTC设备的procfs属性,提供中断状态和标志查询。

    • rtc-lib.c:提供RTC、Data和Time之间的转换函数。

    • rtc-xxx,c:RTC设备的实际驱动,此处以rtc-ds1307为例。

    • hctosys.c:开机时获取RTC时间。

  • 整个文件系统框架

  • RTC子系统具体可分为3层:

    • 用户层:RTC子系统向上层提供了接口,用户通过虚拟文件系统,间接调用RTC设备,具体有3种方式。
      • /dev/rtc RTC设备抽象而来的字符设备,常规文件操作集合。
      • /sys/class/rtc/rtcx 通过sysfs文件系统进行RTC操作,也是最常用的方式。
      • /proc/driver/rtc 通过proc文件系统获取RTC相关信息。
    • RTC核心层:与硬件无关,用于管理RTC设备注册/注销、提供上层文件操作的接口等。
    • RTC驱动:特定RTC设备的驱动程序,实现RTC核心层的回掉函数。编写RTC驱动需要按照RTC子系统的接口填写对应函数并建立映射即可。RTC核心层函数实现的过程和数量与特定硬件紧密相关。
  • 初看linux系统的人来说,这个图够头晕的了,但是呢,实际上没那么麻烦。由浅入深,一点一点来分析。

rtc子系统分析

rtc-dev

  • rtc子系统基于字符设备,字符设备对应的肯定是rtc-dev.c了,我们的分析由rtc-dev起步。

  • rtc-dev.c
    rtc-dev.c

  • 典型的字符设备,模块的初始化/卸载自然是 rtc_dev_init(void) 和 rtc_dev_exit(void)。
    设备接入,添加/删除设备 rtc_dev_add_device(struct rtc_device rtc) 和 e) void rtc_dev_del_device(struct rtc_devicertc)
    还有ioctl和open等函数,熟悉字符设备驱动的不用多说。

  • 追踪一下这两组函数在哪里调用的。

    • rtc_dev_init(void)对应在 class.c的rtc_init(void)函数
    • rtc_dev_add_device对应在class.c的 rtc_device_register()函数。
  • rtc_dev_init(void)对应实在系统初始化时使用,对应rtc_init(void)也是在系统初始化之后调用。

  • rtc_dev_add_device()是在驱动匹配后调用,rtc_device_register()也是在驱动匹配后调用。打开rtc-ds1307.c>-prob函数,能找到rtc_device_register()也就证实了这个思路。

  • 接下来转入class.c

class.c

  • linux驱动模型中class.c对应类的意思,是rtc类。
    每个class对应都有自己核心数据结果,对应rtc类就是rtc-device

  • rtc_device
    rtc_device代表 RTC设备基础的数据结构

    • 数据结构

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
       struct rtc_device  {
      struct device dev;
      struct module *owner;
      int id; //RTC设备的次设备号
      char name[RTC_DEVICE_NAME_SIZE];
      const struct rtc_class_ops *ops;
      struct mutex ops_lock;
      struct cdev char_dev;
      unsigned long flags;
      unsigned long irq_data;
      spinlock_t irq_lock;
      wait_queue_head_t irq_queue;
      struct fasync_struct *async_queue;
      struct rtc_task *irq_task;
      spinlock_t irq_task_lock;
      int irq_freq;
      int max_user_freq;
      #ifdef CONFIG_RTC_INTF_DEV_UIE_EMUL
      struct work_struct uie_task;
      struct timer_list uie_timer;
      /* Those fields are protected by rtc->irq_lock */
      unsigned int oldsecs;
      unsigned int uie_irq_active:1;
      unsigned int stop_uie_polling:1;
      unsigned int uie_task_active:1;
      unsigned int uie_timer_active:1;
      #endif
      };

    • 很长?很😵对不对?只要关注一点就行_

      1
      2
      3
      4
      5
      6
      7
      8
      9

      int id; //代表是那个rtc设备
      char name[RTC_DEVICE_NAME_SIZE]; //代表rtc设备的名称
      const struct rtc_class_ops *ops; //rtc操作函数集,需要驱动实现
      struct mutex ops_lock; //操作函数集的互斥锁

      struct cdev char_dev; //代表rtc字符设备,因为rtc就是个字符设备
      unsigned long flags; //rtc的状态标志,例如RTC_DEV_BUSY

    • 上文书说到,驱动程序的prob函数里面调用了rtc_device_register()这货的类型就是rtc_device。参加驱动程序怎样调用rtc_device_register(),与其他核心的基本结构不同的是,驱动程序以不是以rtc-device为参数注册设备到子系统,而是注册函数会返回一个rtc_deivce的结构给驱动。

  • rtc_class_ops
    这个是rtc_device的一部分。_

    • 数据结构

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      struct rtc_class_ops {
      int (*open)(struct device *); //打开设备时的回调函数,这个函数应该初始化硬件并申请资源
      void (*release)(struct device *); //这个函数是设备关闭时被调用的,应该注销申请的资源
      int (*ioctl)(struct device *, unsigned int, unsigned long); //ioctl函数,对想让RTC自己实现的命令应返回ENOIOCTLCMD
      int (*read_time)(struct device *, struct rtc_time *); //读取时间
      int (*set_time)(struct device *, struct rtc_time *); //设置时间
      int (*read_alarm)(struct device *, struct rtc_wkalrm *); //读取下一次定时中断的时间
      int (*set_alarm)(struct device *, struct rtc_wkalrm *); //设置下一次定时中断的时间
      int (*proc)(struct device *, struct seq_file *); //procfs接口
      int (*set_mmss)(struct device *, unsigned long secs); //将传入的参数secs转换为struct rtc_time然后调用set_time函数。程序员可以不实现这个函数,但 前提是定义好了read_time/set_time,因为RTC框架需要用这两个函数来实现这个功能。
      int (*irq_set_state)(struct device *, int enabled); //周期采样中断的开关,根据enabled的值来设置
      int (*irq_set_freq)(struct device *, int freq); //设置周期中断的频率
      int (*read_callback)(struct device *, int data); ///用户空间获得数据后会传入读取的数据,并用这个函数返回的数据更新数据。
      int (*alarm_irq_enable)(struct device *, unsigned int enabled); //alarm中断使能开关,根据enabled的值来设置
      int (*update_irq_enable)(struct device*, unsigned int enabled); //更新中断使能开关,根据enabled的值来设置
      };
    • 该结构体中函数大多数都是和rtc芯片的操作有关,需要驱动程序实现。
      所有RTC驱动都必须实现read_time、set_time函数,其他函数可选。

  • 参考其他的资料,class的分析如下
    class

  • static int __init rtc_init(void)

    • 调用class_create创建RTC类,创建/sys/class/rtc目录,初始化RTC类相关成员,向用户空间提供设备信息。
    • 调用rtc-dev.c实现的rtc_dev_init();动态分配RTC字符设备的设备号。
    • 调用rtc_sysfs_init(rtc_class);创建/sys/class/rtc下属性文件_
  • static void _exit rtc_exit(void)

    • 调用rtc-dev.c实现的rtc_dev_exit();注销设备号。
    • 调用class_destroy(rtc_class);注销/sys/class下的rtc目录
  • struct rtc_device *rtc_device_register(const char *name, struct device *dev,const struct rtc_class_ops *ops,struct module *owner)_

    • 申请一个idr整数ID管理机制结构体,并且初始化相关成员
    • 将设备dev关联sysfs下的rtc类
    • 初始化rtc结构体的信号量
    • 初始化rtc结构体中的中断
    • 设置rtc的名字
    • 初始化rtc字符设备的设备号,然后注册rtc设备,自动创建/dev/rtc(n)设备节点文件
    • 注册字符设备
    • 在/sys/rtc/目录下创建一个闹钟属性文件
    • 创建/proc/driver/rtc目录
  • void rtc_device_unregister(struct rtc_device *rtc)

    • 删除sysfs中的rtc设备,即删除/sys/class/rtc目录
    • 删除dev下的/dev/rtc(n)设备节点文件
    • 删除虚拟文件系统接口,即删除/proc/driver/rtc目录
    • 卸载字符设备
    • 清空rtc的操作函数指针rtc->ops
    • 释放rtc的device结构体_
  • static void rtc_device_release(struct device dev)

    • 卸载idr数字管理机制结构体
    • 释放rtc结构体的内存

Rtc子系统初始化

  • 上图
    初始化

  • 使用rtc子系统首先需要在内核编译选项中启用RTC子系统支持。

    • 必须启用Real Time Clock
    • 使用/dev下的设备文件对应开启CONFIG_RTC_INTF_DEV
    • 使用/proc下的接口对应开启CONFIG_RTC_INTF_PROC
    • 使用/sysfs下的接口对应开启CONFIG_RTC_INTF_SYSFS
  • _系统启动后,如配置启用rtc子系统,则会首先执行rtc_init_函数,创建rtc类、初始化相关成员、分配设备号等

  • 创建rtc类后,之后调用rtc_dev_init()动态分配rtc字符设备的设备号。之后调用rtc_sysfs_init()初始化/sys/class/rtc目录中的属性文件

Rtc设备注册

  • Rtc设备本质上属于字符设备,依附于系统内总线。一般来说cpu内部rtc依附于platform总线,外置rtc芯片则依附于通信方式对应总线。其过程与通用字符设备相似,rtc子系统在设备注册过程中附加了prob和sysfs相关的注册和初始化操作。

  • 上图
    Rtc设备注册

  • Rtc设备挂载后,相应总线会搜索匹配的驱动程序,驱动程序成功match后,进入驱动实现的probe函数,执行设备注册等操作。

  • 完成总线设备注册后,probe会跳转到rtc_device_register()函数,将设备注册到rtc子系统。

  • Rtc设备本质属于字符设备,会调用rtc_dev_prepare()函数,初始化字符设备,设置rtc相关的file operation函数集合。

  • 之后依次调用rtc_dev_add_device(rtc)、rtc_sysfs_add_device(rtc)、rtc_proc_add_device(rtc) ,进行注册字符设备、在/sys/rtc/目录下创建一个闹钟属性文件、创建/proc/driver/rtc目录等操作。

  • rtc_device_register()会将驱动实现的rtc_class_ops结构体与具体设备联系起来。

interface.c

  • 在rtc-proc.c、rtc_sysfs和ioctl命令中,所有的操作调用的都是interface.c提供的接口,这里以ioctl的一个例子说明整个调用的过程

  • 上图
    interface.c

  • 以icotl命令RTC_RD_TIME为例,说明命令调用的流程。

    • RTC_RD_TIME对应的是/dev下ioctl命令,调用被转发至/rtc-dev.c

    • rtc-dev.c->rtc_dev_ioctl(struct file *file,unsigned int cmd, unsigned long arg)函数中。RTC_RD_TIME对应的代码为err = rtc_read_time(rtc, &tm); rtc_read_time是interface.c文件提供的接口之一。

    • interface.c->rtc_read_time(struct rtc_device *rtc, struct rtc_time *tm)函数中对应rtc_class_ops转发代码为err = rtc->ops->read_time(rtc->dev.parent, tm);将操作转发至匹配的rtc设备。

    • 设备驱动这里以rtc-ds1307为例,但设备注册时通过mcp794xx_rtc_ops结构体将rtc_class_ops对应函数与驱动程序实现的函数绑定

      1
      2
      3
      4
      5
      6
      7
      static const struct rtc_class_ops mcp794xx_rtc_ops = {
      .read_time = ds1307_get_time,
      .set_time = ds1307_set_time,
      .read_alarm = mcp794xx_read_alarm,
      .set_alarm = mcp794xx_set_alarm,
      .alarm_irq_enable = mcp794xx_alarm_irq_enable,
      };
    • 最终执行转入ds1307.c-> ds1307_get_time函数,执行与硬件相关的操作。

rtc-sysfs.c

  • 由前半部分可知,/sys/class/rtc/是在rtc-init调用rtc_sysfs_init后生成。

    1
    2
    3
    4
    5

    void __init rtc_sysfs_init(struct class *rtc_class) {
    rtc_class->dev_groups = rtc_groups;
    }

  • 这里的rtc_groups是rtc-sysfs.c中定义了这样一个attribute函数指针数组:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    static struct attribute *rtc_attrs[] = {
    &dev_attr_name.attr,
    &dev_attr_date.attr,
    &dev_attr_time.attr,
    &dev_attr_since_epoch.attr,
    &dev_attr_max_user_freq.attr,
    &dev_attr_hctosys.attr,
    NULL,
    };
    ATTRIBUTE_GROUPS(rtc);
  • _在rtc_sysfs_init函数调用后绑定了sysfs节点操作函数的集合,使得设备匹配驱动程序后而生成对应的rtcn文件夹。

  • dev_attr_name和dev_attr_data由宏DEVICE_ATTR_RO和DEVICE_ATTR_RW生成,他们分别定义了只读的和可读可写的attribute节点。每个属性函数下都有DEVICE_ATTR_XX()宏声明,绑定到对应attribute节点。

rtc-proc.c

  • proc文件系统是软件创建的文件系统,内核通过他向外界导出信息,下面的每一个文件都绑定一个函数,当用户读取这个文件的时候,这个函数会向文件写入信息。

  • rtc-proc.c中初始化了file_operations结构体:

    1
    2
    3
    4
    5
    6
    static const struct file_operations rtc_proc_fops = {
    .open = rtc_proc_open,
    .read = seq_read,
    .llseek = seq_lseek,
    .release = rtc_proc_release,
    };
  • _RTC驱动在向RTC核心注册自己的时候,由注册函数rtc_device_resgister调用rtc_proc_add_device来实现proc接口的初始化,这个函数如下定义:

    1
    2
    3
    4
    5
    6

    void rtc_proc_add_device(struct rtc_device *rtc)
    {
    if (rtc->id == 0)
    proc_create_data("driver/rtc", 0, NULL, &rtc_proc_fops, rtc);
    }

    主要是完成创建文件节点,并将文件的操作函数与节点联系起来。调用这个函数后,在/proc/driver目录下就会有一个文件rtc

rtc设备访问

  • rtc子系统最多可以支持16个rtc设备,多个rtc设备会在/dev/和 /sys/class/rtc/下生成rtc0、rtc1…等不同节点(下文以rtcn代称)。而系统启动时会选择一个rtc设备读取计时,在/dev下有rtc文件,rtc文件指向系统选择的rtc设备对应的rtcn(一般为rtc0)。
  • 用户层访问rtc设备有3种途径:
    • /dev/rtcn open等字符设备操作和ioctl命令。
    • /sys/class/rtc/rtcn sysfs 属性,一些属性是只读。
    • /proc/driver/rtc 第一个rtc会占用procfs接口,在procfs下暴露更多信息。

/dev

  • 在/dev下用户可以通过两种方式访问rtc设备,第一个是通过字符设备定义的open、read等函数(需要驱动程序实现)、另一个是通过定义的ioctl命令。第一种方式是直接打开rtc-dev.c定义的open等函数,在open等中直接调用驱动程序实现的函数。通过ioctl命令访问则是将操作转发到了interface.c定义的接口,间接调用驱动程序实现的函数。

  • ioctl()函数访问/dev下的设备。以下是典型函数:_

    • ioctl(fd,RTC_ALM_SET, &rtc_tm);
      设置alarm中断的触发时刻,不超过24小时。第三个参数为structrtc_time结构体,读取时会忽略年月日信息。alarm中断与wakeupalarm中断只能同时使用1个,以最后一次设定为准。

    • ioctl(fd,RTC_ALM_READ, &rtc_tm)
      读取alarm中断的触发时刻。

    • ioctl(fd,RTC_WKALM_SET, &alarm);
      设置wakeupalarm中断的触发时刻, wakeupalarm中断的触发时刻可以在未来的任意时刻。alarm中断与wakeupalarm中断只能同时使用1个,以最后一次设定为准。

    • ioctl(fd,RTC_WKALM_RD, &alarm);
      读取wakeupalarm中断的触发时刻。

    • ioctl(fd,RTC_IRQP_SET, tmp);
      设置周期中断的频率,tmp的值必须是2的幂,非Root用户无法使用64HZ以上的周期中断。

    • ioctl(fd,RTC_IRQP_READ, &tmp);
      读取周期中断的频率。

    • ioctl(fd,RTC_SET_TIME, &rtc_tm)
      更新RTC芯片的当前时间。

    • ioctl(fd,RTC_RD_TIME, &rtc_tm);
      读取RTC硬件中的当前时间。

  • 以open操作为例,在用户层对/dev下设备执行open会被转发至rtc_dev_open(struct inode *inode, struct file *file)函数,通过err= ops->open ? ops->open(rtc->dev.parent) : 0;判断驱动程序是否通过连接的rtc_class_ops结构体实现了open函数,驱动程序实现了open函数,则将open操作转发至驱动程序。

/sys

  • /sys/class/rtc/rtcn下面的sysfs接口提供了操作rtc属性的方法,所有的日期时间都是墙上时间,而不是系统时间。
    • date: RTC提供的日期
    • hctosys: 如果在内核配置选项中配置了CONFIG_RTC_HCTOSYS,RTC会在系统启动的时候提供系统时间,这种情况下这个位就是1,否则为0
    • max_user_freq: 非特权用户可以从RTC得到的最大中断频
    • name: RTC的名字,与sysfs目录相关
    • since_epoch: 从纪元开始所经历的秒数
    • time: RTC提供的时间
    • wakealarm: 唤醒时间的时间事件。 这是一种单次的唤醒事件,所以如果还需要唤醒,在唤醒发生后必须复位。这个域的数据结构或者是从纪元开始经历的妙数,或者是相对的秒数

/proc

  • /proc/driver/rtc下只对应第一个rtc设备,与sysfs下相比,该设备暴露更多信息
  • 对应截图
    /proc

RTC子系统测试

Hwclock命令或使用测试文件。

  • Hwclock命令可以执行最简单的RTC测试。常用命令示例如下
    • hwclock #查看RTC时间
    • hwclock -set -date=”07/07/17 10:10” #设置硬件RTC时间(月/日/年 时:分:秒)
    • hwclock -w #系统时间同步至RTC
    • hwclock -s #同步RTC到系统时间
  • Linux内核提供了RTC子系统的测试示例文件,位于tools/testing/selftests/timers/rtctest.c,包含了基于ioctl命令的完整测试。

  • 转载自”Blog of UnicornX” (http://unicornx.github.io/)

  • 面向对象的思想在c语言中的应用,第一次看到才知道c语言还能这么用。。。


前言

  • 语言只是工具,重要的是思想。但是!!C语言用面向对象??看了内核代码,真给那些写Linux、内核的大神跪了。。。
  • 当然因为C语言的局限性,并非完全面向对象的实现,私有成员等就没有了。

封装

  • 既类在C语言中的实现,实际上是 struct + 函数体 = 函数表
    结构体里不允许存在成员函数,但是换成指向函数的指针是可以滴。

  • 示例

    1
    2
    3
    4
    5
    struct foo_operations {
    void (*op_a) (struct foo *, loff_t, int);
    void (*op_b) (struct foo *, char __user *, size_t, loff_t *);
    void (*op_c) (struct foo *, const char __user *, size_t, loff_t *);
    };

继承

  • 这个简单点, struct 里面套 struct ,警告熟悉c语言的应该都清除。。。这里没有父类子类的严格区分,实际上是完全自由的。

  • 示例

    1
    2
    3
    4
    5
    6
    struct foo {
    int a;
    int b;
    int c;
    struct foo_operations ops;
    };

多态

  • 这个是最有意思的,c语言中严格规定同一个.c文件/工程中不能存在重名函数,但是c语言的精华-指针给了思路,借助结构体,我函数名不变,每指向不同的结构体变量就行了。。。虽然还是很麻烦。。

  • 示例

    1
    2
    3
    4
    5
    6
    struct foo a;
    a->ops = ops1;
    a->ops->op_a(a);
    //切换
    a->ops = ops2;
    a->ops->op_a(a);

链表

  • 内核内大量运用链表,开始不清楚真是头晕。。

  • 示例

    1
    2
    3
    struct list_head {
    struct list_head *next, *prev;
    };

    一个指向自身的结构体,包含下一个与自身的指针。
    使用

    1
    2
    3
    4
    struct A_LIST {
    data_t data;
    struct list_head *list;
    };
  • offsetof宏

    1
    #define offsetof(type, member) (size_t)&(((type*)0)->member)

    offsetof是用来判断结构体中成员的偏移位置

  • container_of_宏

    1
    2
    3
    #define container_of(ptr, type, member) ({ \
    const typeof( ((type *)0)->member ) *__mptr = (ptr); \
    (type *)( (char *)__mptr - offsetof(type,member) );})

    根据一个结构体变量中的一个域成员变量的指针来获取指向整个结构体变量的指针..链表元素常用


资料来源如下

  • 乐联网

编程环境

  • Android Studio 2.2.3

最终效果

  • 使用okhttp上传数据。
  • Tcp长连接实现方向控制
  • 以代码为主

相关教程

  • okhttp入门

    http://blog.csdn.net/biezhihua/article/details/50603624

  • 乐联网

    https://www.lewei50.com/dev/doc/176
    https://www.lewei50.com/dev/doc/155

  • Tcp长连接

    http://ls15114843569.blog.51cto.com/11015399/1767195

  • 简易上传

    http://ls15114843569.blog.51cto.com/11015399/1767195

上传数据

  • API介绍

    https://www.lewei50.com/dev/apiinfo/3

  • API测试

    https://www.lewei50.com/dev/apitest/3

  • 地址:http://www.lewei50.com/api/v1/gateway/updatesensors/你的网关号

    POST方式

    需要配置header头 Userkey

  • 数据发送/返回方式JSON

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    [
    {
    "Name":"T1",
    "Value":"1"
    },
    {
    "Name":"01H1",
    "Value":"96.2"
    }
    ]

    返回格式

    1
    2
    3
    4
    {
    "Successful": true,
    "Message": null
    }
  • okhttp POST 传感器数据 这里使用了一个静态内部类。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    	//返回数据处理
    public okhttp3.Callback callback = new okhttp3.Callback() {
    @Override
    public void onResponse(Call call, Response response) throws IOException {
    //返回服务器内容
    String responsedata = response.body().string();
    LogUtil.d("okHttp", responsedata);
    }

    @Override
    public void onFailure(Call call, IOException e) {
    //异常处理
    LogUtil.d("okHttp", "POST错误");
    }
    };

    //内部类
    public static class Http {
    public static final MediaType MEDIA_TYPE_MARKDOWN
    = MediaType.parse("text/x-markdown; charset=utf-8");
    //POST数据,指定接收回掉
    public static void postData(String sensor_name, String sensor_data, okhttp3.Callback callback) {
    OkHttpClient client = new OkHttpClient();
    final String value =
    "[" +
    " {" +
    " \"Name\":\"" + sensor_name + "\"," +
    " \"Value\":\"" + sensor_data + "\"" +
    " }" +
    "]";
    RequestBody requestBody = new RequestBody() {
    @Override
    public MediaType contentType() {
    return MEDIA_TYPE_MARKDOWN;
    }

    @Override
    public void writeTo(BufferedSink sink) throws IOException {
    sink.write(value.getBytes());
    }
    };
    Request request = new Request.Builder()
    .url("http://www.lewei50.com/api/V1/gateway/UpdateSensors/01")
    .header("userkey", "你的userkey")
    .post(requestBody)
    .build();
    client.newCall(request).enqueue(callback);
    }
    }
  • 实际使用 放在一个后台服务内,调用相当简单

    1
    Http.postData("PM2.5", "你的数据转为String", callback);

    Tcp长连接,远程控制

  • 首先参考乐联网反向控制教程,新建一个控制器,这里以开关为例。

  • 原理是与服务器保持一个TCP长连接,不到40s刷新一次,以此保持通道,与被控制段通信,发送控制信息。

  • Tcp长连接参考了@墨迹流韶的Android基于Tcp协议的Socket长链接封装

  • 地址 tcp.lewei50.com
    端口号 9960
    心跳包间隔 1min以内

  • 发送/接收数据格式 Json
    本地发送数据格式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    {

    "method": "update",

    "gatewayNo": "你的网关号",

    "userkey": "你的userkey"

    }&^!

    服务器发送控制命令格式,数据处理时需要去掉字符串最后的&^!

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    {

    "method":"send",

    "gatewayNo":"01",

    "userkey":"6d16ddb3c58c4e448a7e15e7acxxxxxx",

    "f":" updateSensor",

    "p1":"D1",

    "p2":"1"

    }&^!

    本地响应控制命令后返回数据格式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    {

    "method":"response",

    "result":{

    "successful":true,

    "message":"ok!",

    "data":[{

    "id":"D1",

    "value":"1"

    },

    }&^!
  • TCP连接类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    public class TcpSocketHelper {
    private String mTag = "TcpSocketHelper";
    private Socket socket;
    private boolean _connect;
    private ReceiveThread mReceiveThread;
    private boolean receiveStop;
    private Date lastKeepAliveOkTime;
    private OnRecivedListener mRecivedListener;
    //地址
    private String mIpAddr = "http://tcp.lewei50.com";
    //端口
    private int mPort = 9960;

    /**
    * 开启链接socket
    * @param ipAddr
    * @param port
    */
    public void startConnect(String ipAddr, int port){
    LogUtil.d(mTag, "准备链接...");
    this.mIpAddr = ipAddr;
    this.mPort = port;
    InetAddress serverAddr;
    try {
    socket = new Socket(ipAddr, port);
    LogUtil.d(mTag, "准备链接...");
    _connect = true;
    mReceiveThread = new ReceiveThread();
    receiveStop = false;
    mReceiveThread.start();
    LogUtil.d(mTag, "链接成功...");

    } catch (Exception e) {
    LogUtil.d(mTag, "链接出错..." + e.getMessage());
    e.printStackTrace();
    }
    }

    /**
    * 关闭链接
    */
    public void closeConnect(){
    if (socket != null){
    try {
    socket.close();
    socket = null;
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    }

    /**
    * 保持心跳
    */
    public void KeepAlive() {
    // 判断socket是否已断开,断开就重连
    if (lastKeepAliveOkTime != null) {
    LogUtil.d(mTag, "上次心跳成功时间:"+ DateFormat.format("yyyy-MM-dd HH:mm:ss", lastKeepAliveOkTime));
    Date now = Calendar.getInstance().getTime();
    long between = (now.getTime() - lastKeepAliveOkTime.getTime());// 得到两者的毫秒数
    if (between > 60 * 1000) {
    LogUtil.d(mTag, "心跳异常超过40,重新连接:");
    lastKeepAliveOkTime = null;
    socket = null;
    }

    } else {
    lastKeepAliveOkTime = Calendar.getInstance().getTime();
    }

    if (!checkIsAlive()) {
    LogUtil.d(mTag, "链接已断开,重新连接.");
    startConnect(mIpAddr, mPort);
    }
    }

    /**
    * 此方法是检测是否连接
    * @return
    */
    public boolean checkIsAlive() {
    if (socket == null||!socket.isConnected())
    return false;
    return true;

    }

    /**
    * 发送数据的方法
    * @param msg
    */
    public void sendmessage(String msg) {
    boolean isAlive = checkIsAlive();
    if (!isAlive)
    return;
    LogUtil.d(mTag, "准备发送消息:" + msg);
    try {
    if (socket != null && socket.isConnected()) {
    if (!socket.isOutputShutdown()) {

    //2.得到socket读写流
    OutputStream os=socket.getOutputStream();
    //true:是否自动flush
    PrintWriter outStream=new PrintWriter(os, true);
    outStream.print(msg);
    outStream.flush();
    }
    }
    LogUtil.d(mTag, "发送成功!");
    } catch (Exception e) {
    e.printStackTrace();
    }
    }

    /**
    * 设置接收数据监听器
    * @param mRecivedListener
    */
    public void setmRecivedListener(OnRecivedListener mRecivedListener) {
    this.mRecivedListener = mRecivedListener;
    }

    /**
    * 数据接收线程
    */
    class ReceiveThread extends Thread{

    @Override
    public void run() {
    while (true) {
    try {
    sleep(2000);
    // 判断 Socket 是否处于连接状态
    if (socket != null && socket.isConnected()) {
    // 客户端接收服务器端的响应,读取服务器端向客户端的输入流
    InputStream isRead = socket.getInputStream();
    // 缓冲区
    byte[] buffer = new byte[isRead.available()];
    // 读取缓冲区
    isRead.read(buffer);
    // 转换为字符串
    String responseInfo = new String(buffer);
    // 日志中输出
    if(responseInfo != null&&!responseInfo.equals("")){
    LogUtil.d("TcpManager", "返回:"+responseInfo);
    mRecivedListener.onRecived(responseInfo);
    }

    lastKeepAliveOkTime = Calendar.getInstance().getTime();
    KeepAlive();

    continue;
    } else {
    if (socket != null)
    LogUtil.d(mTag, "链接状态:" + socket.isConnected());
    }

    } catch (Exception e) {
    LogUtil.d(mTag, "监听出错:" + e.toString());
    e.printStackTrace();
    }
    }
    }
    }
    }
  • 使用,包装在一个后台service中,在service中实现TcpSocketHelper的onRecived方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
      //tcp返回信息
    @Override
    public void onRecived(String data) {
    LogUtil.d("okHttpService", data);
    //处理服务器发回的数据
    }

    TcpSocketHelper tcpSocketHelper = new TcpSocketHelper();
    tcpSocketHelper.startConnect("tcp.lewei50.com", 9960);
    //设置监听
    tcpSocketHelper.setmRecivedListener(this);
  • 发送心跳包

    1
    2
    3
    4
    5
    6
    7
    8
    String value =
    " {" +
    " \"method\":\"update\"," +
    " \"gatewayNo\":\"01\"," +
    " \"userkey\":\"你的userkey\"" +
    " }&^!";
    //发送数据
    tcpSocketHelper.sendmessage(value);
  • 处理数据 在service的onRecived中

  • 本地处理完毕后,向服务器返回被控制器状态

    1
    2
    3
    String value5 = "{\"method\":\"response\",\"result\":{\"successful\":true,\"message\":\"ok!\",\"data\":[{\"id\":\"D1\",\"value \":\"1\"}]}}&^! ";

    tcpSocketHelper.sendmessage(value6);


  • 资料来源如下
  • 第一行代码(第二版)

Activity基础

Activity定义

  • Activity 是Android四大组件之一,用户可与其提供的屏幕进行交互,以执行操作。 每个 Activity 都会获得一个用于绘制其用户界面的窗口。窗口可以充满屏幕,也可浮动。应用通常由多个彼此联系的 Activity 组成。应用中的某个 Activity 为“主”Activity,即首次启动应用时的Activity。

创建Activity

  • 新建FirstAtivity继承自AppCompatActivity

    1
    2
    3
    4
    5
    public class FirstActivity extends AppCompatActivity{
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    } }
  • 创建加载布局
    1.jpg

  • 切换到first-layout

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
      <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Button
    android:id="@+id/button_1"
    android:text="button_1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />
    </LinearLayout>
  • 在FirestActivity中加载layout
    在onCreate中加入

    1
    setContentView(R.layout.first_layout);
  • 在AndroidMainfest文件中注册。

    1
    2
    3
    4
    5
    6
    7
    8
    <activity
    android:name=".FirstActivity"
    android:label="This is FirstActivity">
    <intent-filter>
    <action android:name="android.intent.action.MAIN"/>
    <category android:name="android.intent.category.LAUNCHER"/>
    </intent-filter>
    </activity>
  • 在模拟器中启动apk
    5ea429.jpg

使用Toast

Toast google官方说明

  • 推送一个短小信息推送给用户
    如图(摘自android developer)
    toast.png

  • 使用方法

    1
    2
    Toast.makeText(context, text, duration).show();

    举例

    1
    2
    Toast.makeText(FirstActivity.this, "you clicked button 1", Toast.LENGTH_SHORT).show();
    /* activity.this 消息内容 显示时间设置 */

使用Menu

  • 创建菜单

    • 在res下新疆menu文件夹,右击menu文件夹—new—Menu resource file,创建main的菜单文件。
    • main.xml中添加如下代码
      1
      2
      3
      4
      5
      6
      7
      8
      9
       <?xml version="1.0" encoding="utf-8"?>
      <menu xmlns:android="http://schemas.android.com/apk/res/android">
      <item
      android:id="@+id/add_item"
      android:title="Add"/>
      <item
      android:id="@+id/remove_item"
      android:title="Remove"/>
      </menu>
      这里创建了两个菜单项 Add和Remove
    • 在FirestActivity中重写 onCreateOptionsMenu()方法(快捷键 Ctrl+O)
      1
      2
      3
      4
      5
        @Override
      public boolean onCreateOptionsMenu(Menu menu) {
      getMenuInflater().inflate(R.menu.main, menu);
      return true;
      }
      getMenuInflater()可以得到MenuInflater对象,再调用.inflate就可以创建菜单。.inflat接受俩个参数,一是资源文件名,二是菜单项添加至那个对象中。onCreateOptionsMenu方法中返回true表示创建菜单并显示。
    • 效果如下。
      Screenshot_1481113497.png
  • 创建菜单点击响应事件

    • 重写onOptionsItemSelected方法
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      @Override
      public boolean onOptionsItemSelected(MenuItem item) {
      switch (item.getItemId()) {
      case R.id.add_item:
      Toast.makeText(this, "Add", Toast.LENGTH_SHORT).show();
      break;
      case R.id.remove_item:
      Toast.makeText(this, "Remove", Toast.LENGTH_SHORT).show();
      break;
      default:
      }
      return true;
      }
      通过item.getItemId()判断点击选项,弹出不同的Toast

向一个activity 并传递字符串

  • 构建一个Intent
    Intent intent = new Intent(this, DisplayMessageActivity.class);
    构造方法有两个参数:
    Context 是第一个参数,这里使用了this是因为Activity是Context的子类。
    Class 类是系统将要分发的APP组件,在这里,这个Activity将会被启动。
  • ```java
    public void sendMessage(View view) {
      // 创建一个新的intent对象,绑定DisplayMessageActivity
      Intent intent = new Intent(this, DisplayMessageActivity.class);
      //创建一个editText对象,绑定xml中editText
      EditText editText = (EditText) findViewById(R.id.edit_message);
      //获取editText中输入文字,转成字符串
      String message = editText.getText().toString();
      //一个Intent对象可以携带被称为extras的键值对。
      // putExtra()方法将键放在第一个参数中,将值放在第二个参数中。
      intent.putExtra(EXTRA_MESSAGE, message);
      //启动intent对应Activity
      startActivity(intent);
      }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22

    * ```java
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_display_message);

    //创建本地intent
    Intent intent = getIntent();
    //取出String类型数据,如果是Int类型就是getIntExtra,Boolean类型就是getBooleanExtra
    String message = intent.getStringExtra(MainActivity.EXTRA_MESSAGE);
    //显示String
    //创建新的TextView
    TextView textView = new TextView(this);
    //设置文本大小
    textView.setTextSize(40);
    //设置显示内容
    textView.setText(message);
    //绑定xml
    ViewGroup layout = (ViewGroup) findViewById(R.id.activity_display_message);
    //ViewGroup中添加TextView
    layout.addView(textView);
    }

返回数据给上一个Activity

  • 构建Intent使用startActivityForResult()方法传入请求码,启动下一个Activity
    在下一个Activity中构建Intent,intent.putExtra存入键值-key,调用setResult()方法,传入finish()结束掉Activity
    重写第一个Activity中的onActivityResult()方法,调用.getStringExtra取出key对应键值。

  • startActivityForResult(Intent, int Bundle) Intent与单纯启动Activity的Intent相同,第二个是请求码,下一级 回调提供相同的请求码,以便您应用可以正确识别结果。

    1
    2
    Intent intent = new Intent(FirestActivity.this,ScendActivity.class);
    startActivityForResult(intent,1);
  • setResult()方法,第一个参数向上级方法处理结果,一般使用RESULT_OK或RESULT_CANCELED,第二个参数 对应Intent

    1
    2
    3
    4
    5
    6
    7
    //新建显示Intent
    Intent intent = new Intent();
    //存入key-键值
    intent.putExtra("data_return","Hello Firest");
    setResult(RESULT_OK,intent);
    //结束Activity
    finish();
  • onActivityResult()三个参数.第一个startActivityForResult() 传递的请求代码。第二个 Activity 指定的结果代码。成功是 RESULT_OK;失败,则是 RESULT_CANCELED。第三个是传送结果数据的 Intent。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
     @Override
    protected void onActivityResult(int requestCode,int resultCode,Intent data){
    //选择不同请求码对应处理逻辑
    switch(requestCode){
    case 1:
    //处理结果时候ok
    if(resultCode == RESULT_OK){
    //取出数据
    String returnedData = data.getStringExtra("data_return");
    Log.d("FirstActivity", returnedData);
    }
    }
    }

Activity生命周期

basic-lifecycle.png

  • Activity 3种状态
    • Resumed/继续
      Activity 处于前台,且用户可以与其交互
    • Paused/暂停
      Activity 被在前台中处于另一个 Activity—部分阻挡。 暂停的 Activity 不会接收用户输入并且无法执行任何代码。
    • Stopped/停止
      Activity完全隐藏,对用户完全不可见.当停止时,activity的所有状态信息比如成员变量都会被保留,但是不能再执行任何代码

启动一个Activity

  • onCreate方法
  • 主Activity:
    • 用户点击app图标,启动主Activity
    • 在AndroidManifest.xml中声明
      1
      2
      3
      4
      <intent-filter>
      <action android:name="android.intent.action.MAIN" />
      <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
  • 通过调用onCreate方法创建一个Activity实例,onCreate方法在Activity的整个生命周期中只执行一次。
  • 之后系统会很快的调用onStart和onResume方法,Activity进入Resumed/继续模式。直到被其他activity覆盖/屏幕关闭。

    销毁Activity

  • onDestory方法
    大多数的APP不需要实现这个方法,因为本地类引用会随着Activity一起总结,不过Activity的清理工作应该放在onPause下或者onStop。
  • Note:
    在所有的情况下系统调用onDestory方法之后已经调用过onPause方法与onStop方法,不过有一个例外情况:你在onCreate方法中调用了finish方法。在一些例子中,当你的Activity临时决定要启动另一个Activity,你可能要在onCreate方法内调用finish方法来销毁这个Activity,在这种情况下,系统会立即调用onDestory方法,而不会调用其它任何生命周期方法。

暂停Activity

  • onPause()方法
  • Activity被其他Activity覆盖/失去用户焦点,系统调用onPause()方法,Activity 进入暂停状态。
    • note:android7.0及以上版本加入了多窗口模式,当Activity失去用户焦点时,可能处于多窗口模式。
  • onPause() 常用回调:
    • 检查 Activity 是否可见。不可见则停止可能消耗 CPU 的操作
    • 提交未保存的更改,仅保存用户离开时希望永久性保存此类更改(比如电子邮件草稿)。
    • 释放系统资源,GPS/Camer等
    • 示例 (释放Camer)
      1
      2
      3
      4
      5
      6
      7
      8
      9
      public void onPause() {
      super.onPause(); // Always call the superclass method first

      // Release the Camera because we don't need it when paused
      // and other activities might need to use it.
      if (mCamera != null) {
      mCamera.release();
      mCamera = null;
      }
    • 注意事项:
      • 在onPause()一般不执行永久性存储用户更改,不执行 CPU 密集型工作,这些工作一般放在onStop() 。

继续 Activity

  • onResume() 方法
  • 暂停状态回到继续状态,Activity第一次启动时也会调用这个方法。
  • onResume() 以初始化在 onPause() 期间释放的组件。
  • 示例(重新获取Camera)
    1
    2
    3
    4
    5
    6
    7
    public void onResume() {
    super.onResume(); // Always call the superclass method first

    // Get the Camera instance as the activity achieves full user focus
    if (mCamera == null) {
    initializeCamera(); // Local method to handle camera init
    }

停止 Activity

  • note:
    大多数相对简单的 Activity 而言,系统在 Activity 停止时会将Activity 实例保留在系统内存中,无需实现 onStop() 和 onRestart() 或甚至onStart() 方法。可能只需使用 onPause() 暂停正在进行的操作,并从系统资源断开连接。
  • onStop() 方法

  • 场景:

    • 用户在最近应用切换到另一个应用
    • 应用中执行开始新 Activity 的操作
    • Activity使用时,接打电话
  • 调用时,Activity不再可见,释放几乎所有用户不使用时不需要的资源。如果系统内存紧张,则可能销毁内存中的Acitivity实例。

  • onStop() 方法调用后,Activity不再可见,极端情况下,系统可能会仅终止应用进程,而不调用 onDestroy() ,因此需要使用 onStop() 释放几乎所有用户不使用时不需要的资源。

  • 尽管 onPause() 方法在 onStop()之前调用,在onStop() 执行更大、占用更多 CPU 的关闭操作,比如向数据库写入信息

  • 示例(草稿笔记内容保存在永久存储)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
      protected void onStop() {
    super.onStop(); // Always call the superclass method first

    // Save the note's current draft, because the activity is stopping
    // and we want to be sure the current note progress isn't lost.
    ContentValues values = new ContentValues();
    values.put(NotePad.Notes.COLUMN_NAME_NOTE, getCurrentNoteText());
    values.put(NotePad.Notes.COLUMN_NAME_TITLE, getCurrentNoteTitle());

    getContentResolver().update(
    mUri, // The URI for the note to update.
    values, // The map of column names and new values to apply to them.
    null, // No SELECT criteria are used.
    null // No WHERE columns are used.
    );
    }

启动/重启 Activity

  • onStart() 方法
  • Activity 停止转换为继续状态时,系统回调onRestart() 方法+ onStart() 方法.onStop() 方法清理了所有 Activity 的资源,重启 Activity 需要重新实例化它们。同时 Activity 初次创建时重新实例化它们。 出于此,经常使用 onStart() 方法作为 onStop() 方法的对应
  • 示例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
     @Override
    protected void onStart() {
    super.onStart(); // Always call the superclass method first

    // The activity is either being restarted or started for the first time
    // so this is where we should make sure that GPS is enabled
    LocationManager locationManager =
    (LocationManager) getSystemService(Context.LOCATION_SERVICE);
    boolean gpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);

    if (!gpsEnabled) {
    // Create a dialog here that requests the user to enable GPS, and use an intent
    // with the android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS action
    // to take the user to the Settings screen to enable GPS when they click "OK"
    }
    }

    @Override
    protected void onRestart() {
    super.onRestart(); // Always call the superclass method first

    // Activity being restarted from stopped state
    }

    保存 Activity 状态

  • onSaveInstanceState()方法
  • 默认情况下,Activity 实例被销毁时系统会使用 Bundle 实例状态保存 Activity 布局中有关每个 View 对象的信息。在Activity 重建时,布局状态便自动恢复先前的状态。
  • 默认实现保存有关 Activity 视图层次的状态信息,例如 EditText 小部件中的文本或ListView 的滚动位置
  • 要恢复的更多信息,需要重写 onSaveInstanceState()方法,将键值对添加至 Bundle 对象
  • basic-lifecycle-savestate.png
  • note:
    旋转屏幕时,Activity 将被销毁并重新创建。原因:方向更改时可能需要时加载备用资源(比如布局)
  • 示例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    static final String STATE_SCORE = "playerScore";
    static final String STATE_LEVEL = "playerLevel";
    ...

    @Override
    public void onSaveInstanceState(Bundle savedInstanceState) {
    // Save the user's current game state
    savedInstanceState.putInt(STATE_SCORE, mCurrentScore);
    savedInstanceState.putInt(STATE_LEVEL, mCurrentLevel);

    // Always call the superclass so it can save the view hierarchy state
    super.onSaveInstanceState(savedInstanceState);
    }

恢复 Activity

  • onCreate() 和 onRestoreInstanceState() 回调方法均接收包含实例状态信息的相同 Bundle
  • onCreate() 方法
  • 调用onCreate() 方法需要区分是创建 Activity 的新实例还是恢复先前的实例,判断 Bundle 是否为 null
  • 示例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState); // Always call the superclass first

    // Check whether we're recreating a previously destroyed instance
    if (savedInstanceState != null) {
    // Restore value of members from saved state
    mCurrentScore = savedInstanceState.getInt(STATE_SCORE);
    mCurrentLevel = savedInstanceState.getInt(STATE_LEVEL);
    } else {
    // Probably initialize members with default values for a new instance
    }
    ...
    }
  • onRestoreInstanceState()方法
  • 只需要恢复的已保存的状态
  • 示例
    1
    2
    3
    4
    5
    6
    7
    8
     public void onRestoreInstanceState(Bundle savedInstanceState) {
    // Always call the superclass so it can restore the view hierarchy
    super.onRestoreInstanceState(savedInstanceState);

    // Restore state members from saved instance
    mCurrentScore = savedInstanceState.getInt(STATE_SCORE);
    mCurrentLevel = savedInstanceState.getInt(STATE_LEVEL);
    }

重启Activity其他选择

  • 官方说明
  • 重启应用并恢复大量数据不仅成本高昂,而且会留下糟糕的使用体验,有两个其他选择

    在配置变更期间保留对象

  • Activity 因配置变更而重启,则可通过保留 Fragment 来减轻重新初始化 Activity 的负担
  • 当 Android 系统因配置变更而关闭 Activity 时,不会销毁已标记为要保留的 Activity 的片段。 您可以将此类片段添加到 Activity 以保留有状态的对象。

Activity最佳实践

知晓当前运行的活动

  • 自定义BaseActivity继承自AppCompatActivity
    重写onCreate方法,打印当前运行的Activity名
    app类所有Activity改为继承BaseActivity

  • 代码如下

    1
    2
    3
    4
    5
    6
    7
    public class BaseActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    //打印当前类名
    Log.d("BaseActivity", getClass().getSimpleName());
    }
  • 效果如下
    ScreenShot_20161208172740.png

随时退出程序

  • 新建ActivityCollector类作为活动管理器,添加/删除Activity登记,在BaseActivity中添加对应代码。
  • 代码如下
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
     public class ActivityCollector {
    //新建Activity的列表对象
    public static List<Activity> activities = new ArrayList<>();
    //添加Activity进入列表
    public static void addActivity(Activity activity) {
    activities.add(activity);
    }
    //在列表中移除Activity
    public static void removeActivity(Activity activity) {
    activities.remove(activity);
    }
    //for循环列表 结束所有Activity
    public static void finishAll() {
    for (Activity activity : activities) {
    if (!activity.isFinishing()) {
    activity.finish();
    }
    }
    }
    }
  • BaseActivity
    在onCreate()方法中添加
    1
    2
    //Activity管理器中添加新的活动
    ActivityCollector.addActivity(this);
    重写onDestroy()方法
    1
    2
    3
    4
    5
    6
    @Override
    protected void onDestroy(){
    super.onDestroy();
    //Activity管理器中移除活动
    ActivityCollector.removeActivity(this);
    }
  • 在任何地方调用finishAll()方法即可。

启动活动的最近写法

  • 启动下一个Activity并传递数据模块化
  • 在SecondActivity内增加actionStart()方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
     //调用的Activity,数据1,数据2
    public static void actionStart(Context context, String data1, String data2){
    //新建Intent,绑定SecondActivity
    Intent intent = new Intent(context,ScendActivity.class);
    //存入data1,data2
    intent.putExtra("param1",data1);
    intent.putExtra("param2",data2);
    //启动Activity
    context.startActivity(intent);
    }
  • 启动SecondActivity方式
    ScendActivity.actionStart(FirestActivity.this,"data1","data2");


资料来源如下

  • 网络

编程环境

  • Android Studio 2.2.3

导语

  • 转眼刚用上手的MVP又不符合需求了,还是总结一下,继续MVVM吧。

  • 主要内容是转载,搭建小工程应该足够了。
    Android mvp 架构的自述
    如何更高效的使用MVP以及官方MVP架构解析

  • 框架开源在GitHub 地址点我直达

  • 好,我们开始吧!


MVC MVP MVVM

  • 省略200行,详情看Android App的设计架构:MVC,MVP,MVVM与架构经验谈
  • 再累赘属于掉书袋了,作者写的是很用心,恩,MVVM也有。

框架详解

  • 整个框架截图
    ScreenShot_20170509215208.png

  • 整体层次比较明确了,BaseClass 里存放基类,Model层存放数据存储有关类、Presenter层存放逻辑代码、View层存放Activity、frament等。

  • 这个框架是由MVP在Android中应用存在的问题而搭建的,基类中代码也由此而来。

问题

  • presenter一直持有Activity对象导致的内存泄漏问题

    • 使用mvp的时候,presenter会持有view,如果presenter有后台异步的长时间的动作,比如网络请求,这时如果返回退出了Activity,后台异步的动作不会立即停止,这里就会有内存泄漏的隐患。
    • 需要一个销毁view的方法,这里是在基类中完成的。
  • presenter绑定和解绑繁琐

    • 一般一个presenter对应一个Activity,一般应用内存在多个Actiivity,绑定与解绑相当繁琐。
    • 统一在 Basepresenter 与 BaseActivity中进行,同时解决内存泄漏问题,还有其他常用内容一并添加。
  • presenter 与 Model 的通信问题。

    • presenter已经与View层 强耦合了,框架中需要解耦presenter 与 Model ,使用异步通信。Handle等都太复杂了。。。
    • 开始没有什么好的解决办法,直到遇到了EventBus,当然EventBus也不是万能解决方案,跨进程,还是要另辟蹊径,不过EventBus足够自己写着完了。还有Rxjava,恩,还在玩着,没搞太懂。

代码

  • BasePresenter

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
     public abstract class BasePresenter<Viewinterface> {
    //传入泛型
    public Viewinterface mView;
    //绑定
    public void attach(Viewinterface mView) {
    this.mView = mView;
    }
    //解绑,防止view为空是内存泄漏
    public void dettach() {
    mView = null;
    }
    }
  • BaseActivity

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
     public abstract class BaseActivity <Viewinterface,mPresenter extends BasePresenter<Viewinterface>>extends AppCompatActivity {
    //获取Presenter对象
    public mPresenter mpresenter;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    //Presenter实例化
    mpresenter = initPresenter();
    //打印当前activity
    LogUtil.d("Activity", getClass().getSimpleName()+"onCreate");
    }

    @Override
    protected void onResume() {
    //重新刷新时重新绑定view
    mpresenter.attach((Viewinterface) this);
    super.onResume();
    LogUtil.d("Activity", getClass().getSimpleName()+"onResume");
    }

    @Override
    protected void onDestroy() {
    //解绑presenter持有的view
    mpresenter.dettach();
    super.onDestroy();
    LogUtil.d("Activity", getClass().getSimpleName()+"onDestroy");
    }


    public abstract mPresenter initPresenter();
    }
  • Presenter

    1
    2
     public interface Presenter {
    }
  • mPresenter

    1
    2
    3
    4
    5
    6
    7
    8
     public class mPresenter extends BasePresenter<Viewif> implements Presenter {
    private Model mModel;

    public mPresenter(Viewif view) {
    this.attach(view);
    this.mModel = new mModel();
    }
    }
  • BaseView

    1
    2
    3
     public interface BaseView<T>  {

    }
  • Viewif

    1
    2
    3
     public interface Viewif extends BaseView {

    }
  • MainActivity

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
     public class MainActivity extends BaseActivity<Viewif, mPresenter> implements Viewif{

    Presenter presenter;
    //Presenter初始化
    public mPresenter initPresenter() {
    return new mPresenter(this);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    //presenter初始化
    presenter = mpresenter;
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    }
    }
  • LogUtil
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
     public class LogUtil {
    public static final int VERBOSE = 1;//啰嗦,等级最低的
    public static final int DEBUG = 2;//调试
    public static final int INFO = 3;//信息
    public static final int WARN = 4;//警告
    public static final int ERROR = 5;//错误
    public static final int NOTHING = 6;//什么也不打印出来
    public static final int level = VERBOSE;//LEVEL:标准

    public static void v(String tag, String msg) {
    if (level <= VERBOSE) {//如果大于或者等于定义的标准就打印出来
    Log.v(tag, msg);
    }
    }

    public static void d(String tag, String msg) {
    if (level <= DEBUG) {
    Log.d(tag, msg);
    }
    }

    public static void i(String tag, String msg) {
    if (level <= INFO) {
    Log.i(tag, msg);
    }
    }

    public static void w(String tag, String msg) {
    if (level <= WARN) {
    Log.w(tag, msg);
    }
    }

    public static void e(String tag, String msg) {
    if (level <= ERROR) {
    Log.e(tag, msg);
    }
    }
    }


资料来源如下

  • 第一行代码(第二版)

编程环境

  • Android Studio 2.2.3

导语

  • 毕设中的蓝牙部分,记录以供复习

概述

最终效果

  • Android应用 与 Hc-05 蓝牙模块连接,单片机与 Android 端 可以通过串口正常收发数据.

  • 预留足够灵活的接口, Android 应用中可以在任意位置获取到蓝牙数据

  • Activity切换时,蓝牙连接不断开.

用到的开源库/知识点资料 简介/来源

  • 蓝牙:
    Android API 指南

    https://developer.android.com/guide/topics/connectivity/bluetooth.html

  • EventBus:
    EventBus-GitHub 是一个Android端优化的publish/subscribe消息总线,用来替代 Intent 、 Handler 、 Broadcast 等在 Actvity 、 Fragment 、 Service 等组件之间传递信息 . EventBus 可以传递一个完整的对象,简单高效, 注意 EventBus 只能在多线程之间传递消息,无法在不同进程之间传递消息 ,EventBus 3.0 以后进一步简化了传递方式,真的是很值得学习的一个开源库!

    参考资料如下:

    EventBus 3.0初探: 入门使用及其使用 完全解析

    EventBus3(3.0.0)源码解析


基础部分

  • 蓝牙 与 EventBus 基础部分

BlueTooth


  • 备注 : 这里使用的是 传统蓝牙 即 蓝牙4.0以前版本, 而不是 蓝牙4.0 ( ble低功耗蓝牙)及以后版本

  • Android 对 Bluetooth 做了很好的封装,我们可以比较轻松的在 Android Bluetooth API 上进行开发.
  • Android中所有蓝牙API均来自 android.bluetooth 包

BlueTooth基础

  • BluetoothAdapter 本地蓝牙适配器
    BluetoothAdapter 是所有蓝牙交互的入口点, 在初始化蓝牙及蓝牙配对阶段使用
    1.发现其他蓝牙设备
    2.查询绑定(配对)设备的列表
    3.使用已知的 MAC 地址实例化 BluetoothDevice
    4.创建 BluetoothServerSocket 侦听来自其他设备的通信。

  • BluetoothDevice 远程蓝牙设备
    含有该设备的信息,例如设备的名称、地址、类和绑定状态等。

  • BluetoothSocket 蓝牙套接字接口(与 TCP Socket 相似)
    与 BluetoothDevice 配合建立远程连接,允许应用通过 InputStream 和 OutputStream 与其他蓝牙设备交换数据.

初始化蓝牙

  • 声明蓝牙权限
    Android 应用使用蓝牙前都需要声明蓝牙权限
    一般只需要 BLUETOOTH 权限即可,但是考虑到之后有更多的需求,需要更改系统蓝牙设置,由此需要 BLUETOOTH_ADMIN 权限_也一起声明

    1
    2
    3
    4
    5
    <manifest ... >
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    ...
    </manifest>
  • 蓝牙设备可用性 获取 BluetoothAdapter
    获取 BluetoothAdapter,需要静态 getDefaultAdapter() 方法。getDefaultAdapter() 会返回一个表示设备自身的蓝牙适配器的 BluetoothAdapter 设备不支持蓝牙 则返回 null

    1
    2
    3
    4
    BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
    if (mBluetoothAdapter == null) {
    // 蓝牙不可用操作
    }
  • 开启蓝牙
    BluetoothAdapter 的 .isEnabled() 可以判断系统蓝牙是否开启.
    没有开启时 startActivityForResult() 发送包含 ACTION_REQUEST_ENABLE 的 Intent 请求系统开启蓝牙,并在 onActivityResult() 返回的数据中 RESULT_CANCELED 表示开启失败 RESULT_OK 表示开启成功,这里我们只检测开启失败情况,并提示用户

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    if (!mBluetoothAdapter.isEnabled()) {
    Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
    startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
    }

    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == REQUEST_ENABLE_BT && resultCode == RESULT_CANCELED) {
    Toast.makeText(MyApplication.getContext(),R.string.Bluetooth_openfail, Toast.LENGTH_SHORT).show();
    }
    BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
    if (mBluetoothAdapter == null) {
    //设备不支持蓝牙时处理
    }
    }
  • 查询已配对的设备
    调用 getBondedDevices(),返回已配对设备的一组 BluetoothDevice.之后使用for循环遍历

    1
    2
    3
    4
    5
    6
    7
    8
    9
    Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices();
    if (pairedDevices.size() > 0) {
    //重新加载bluetoothDeviceList
    bluetoothDeviceList.clear();
    //BluetoothDevice列表循环
    for (BluetoothDevice device : pairedDevices) {
    bluetoothDeviceList.add(device);
    }
    }
  • 扫描设备
    查找设备是非常耗费系统资源的事项,需要安排在子线程中执行,这里没有用到,由需要请参考Google官方蓝牙教程

  • 连接设备
    蓝牙分主从机,链接为服务器/客户端。这里使用的是连接为客户端。

    • 首先获取远程设备的 BluetoothDevice 对象,即在选择设备阶段的BluetoothDevice
    • 调用 createRfcommSocketToServiceRecord(UUID) 获取 BluetoothSocket,UUID通用唯一识别码 在蓝牙中具体是什么没有很好的解释。这里的值取的是
      "00001101-0000-1000-8000-00805F9B34FB"
    • 调用 connect() 发起连接,阻塞调用,需要在子线程中执行。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      //蓝牙连接子线程
      private class ConnectThread extends Thread {
      private final BluetoothSocket mmSocket;

      //解析函数
      ConnectThread(BluetoothDevice bluetoothDevice) {
      // 使用一个中间变量 tmp
      // mmSocket 类型是 final

      BluetoothSocket tmp = null;
      // 获取 BluetoothSocket
      try {
      // MY_UUID is the app's UUID string, also used by the server code
      tmp = bluetoothDevice.createRfcommSocketToServiceRecord(UUID.fromString(MyApplication.MY_UUID));
      } catch (IOException ignored) {
      }
      //赋值给 mmSocket
      mmSocket = tmp;
      }

      public void run() {
      // 关闭蓝牙扫描
      MyApplication.getBluetoothAdapter().cancelDiscovery();
      try {
      // 通过 socket 连接到设备. connect() 会一直执行直到成功连接或者抛出异常
      mmSocket.connect();
      } catch (IOException connectException) {
      //无法连接到蓝牙,关闭连接并退出
      try {
      mmSocket.close();
      }
      //没有正常关闭
      catch (IOException ignored) {
      }
      return;
      }
      // Do work to manage the connection (in a separate thread)
      mConnectedThread = new ConnectedThread(mmSocket);
      mConnectedThread.start();
      }

      /**
      * 关闭蓝牙连接
      */
      void cancel() {
      try {
      mmSocket.close();
      } catch (IOException ignored) {
      }
      }
      }
  • 管理连接
    获取BluetoothSocket
    获取 InputStream 和 OutputStream, getInputStream() 和 getOutputStream() 来处理数据传输。read(byte[]) 和 write(byte[]) 读取数据并写入到流式传输。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    private class ConnectedThread extends Thread {
    //BluetoothSocket
    private final BluetoothSocket mmSocket;
    //输入流
    private final InputStream mmInStream;
    //输出流
    private final OutputStream mmOutStream;

    ConnectedThread(BluetoothSocket socket) {
    //传入BluetoothSocket,实例化mmSocket
    mmSocket = socket;
    //输入/输出流 中间变量
    InputStream tmpIn = null;
    OutputStream tmpOut = null;

    // 输入输出流实例化
    try {
    tmpIn = socket.getInputStream();
    tmpOut = socket.getOutputStream();
    } catch (IOException e) {
    e.printStackTrace();
    }
    mmInStream = tmpIn;
    mmOutStream = tmpOut;
    }

    public void run() {
    byte[] buffer = new byte[1024];
    int bytes;
    // 连接成功时
    while (true) {
    try {
    // 在InputStream读数据
    bytes = mmInStream.read();
    //发送数据
    Events.bluetooth_Recycle bluetooth_recycle = new Events.bluetooth_Recycle();
    bluetooth_recycle.s = bytes;
    bluetooth_recycle.bytes = String.valueOf((char) bytes);
    EventBus.getDefault().post(bluetooth_recycle);
    } catch (IOException e) {
    break;
    }
    }
    }

    //写方法
    void write(byte[] bytes) {
    try {
    mmOutStream.write(bytes);
    } catch (IOException e) {
    e.printStackTrace();
    }
    }

    //关闭流连接
    void cancel() {
    try {
    mmSocket.close();
    } catch (IOException e) {
    }
    }
    }

    EventBus

  • 直接放链接了

    http://www.jianshu.com/p/a040955194fc
    http://www.jianshu.com/p/acfe78296bb5
    http://www.ff50.net/view/40565212977623506063.html

正文

  • 蓝牙连接子线程
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
     private class ConnectThread extends Thread {
    private final BluetoothSocket mmSocket;

    //解析函数
    ConnectThread(BluetoothDevice bluetoothDevice) {
    // 使用一个中间变量 tmp
    // mmSocket 类型是 final

    BluetoothSocket tmp = null;
    // 获取 BluetoothSocket
    try {
    // MY_UUID is the app's UUID string, also used by the server code
    tmp = bluetoothDevice.createRfcommSocketToServiceRecord(UUID.fromString(MyApplication.MY_UUID));
    } catch (IOException ignored) {
    }
    //赋值给 mmSocket
    mmSocket = tmp;
    }

    public void run() {
    // 关闭蓝牙扫描
    MyApplication.getBluetoothAdapter().cancelDiscovery();
    try {
    // 通过 socket 连接到设备. connect() 会一直执行直到成功连接或者抛出异常
    mmSocket.connect();
    } catch (IOException connectException) {
    //无法连接到蓝牙,关闭连接并退出
    try {
    mmSocket.close();
    }
    //没有正常关闭
    catch (IOException ignored) {
    }
    return;
    }
    // Do work to manage the connection (in a separate thread)
    mConnectedThread = new ConnectedThread(mmSocket);
    mConnectedThread.start();
    }

    /**
    * 关闭蓝牙连接
    */
    void cancel() {
    try {
    mmSocket.close();
    } catch (IOException ignored) {
    }
    }
    }
  • 管理连接子线程

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
      //管理连接子线程
    private class ConnectedThread extends Thread {
    //BluetoothSocket
    private final BluetoothSocket mmSocket;
    //输入流
    private final InputStream mmInStream;
    //输出流
    private final OutputStream mmOutStream;

    ConnectedThread(BluetoothSocket socket) {
    //传入BluetoothSocket,实例化mmSocket
    mmSocket = socket;
    //输入/输出流 中间变量
    InputStream tmpIn = null;
    OutputStream tmpOut = null;

    // 输入输出流实例化
    try {
    tmpIn = socket.getInputStream();
    tmpOut = socket.getOutputStream();
    } catch (IOException e) {
    e.printStackTrace();
    }
    mmInStream = tmpIn;
    mmOutStream = tmpOut;
    }

    public void run() {
    byte[] buffer = new byte[1024];
    int bytes;
    // 连接成功时
    while (true) {
    try {
    // 在InputStream读数据
    bytes = mmInStream.read();
    //发送数据
    Events.bluetooth_Recycle bluetooth_recycle = new Events.bluetooth_Recycle();
    bluetooth_recycle.s = bytes;
    bluetooth_recycle.bytes = String.valueOf((char) bytes);
    EventBus.getDefault().post(bluetooth_recycle);
    } catch (IOException e) {
    break;
    }
    }
    }

    //写方法
    void write(byte[] bytes) {
    try {
    mmOutStream.write(bytes);
    } catch (IOException e) {
    e.printStackTrace();
    }
    }

    //关闭流连接
    void cancel() {
    try {
    mmSocket.close();
    } catch (IOException e) {
    }
    }
    }
  • 连接蓝牙

    1
    2
    3
    4
    5
    6
    7
    8
    9
    //连接线程
    private ConnectThread mConnectThread;
    //管理连接进程
    private ConnectedThread mConnectedThread;

    //创建连接子线程
    mConnectThread = new ConnectThread(bluetoothDevice);
    //启动连接子线程
    mConnectThread.start();
  • 向蓝牙写入数据

    1
    2
    3
    4
    5
    6
    7
     //发送数据String,        主线程            优先级4       非粘性事件
    @Subscribe(threadMode = ThreadMode.MAIN, priority = 4, sticky = false)
    public void onEvent(Events.bluetooth_Send bluetoothSend) {
    String bytes = bluetoothSend.bytes;
    mConnectedThread.write(bytes.getBytes());
    LogUtil.d(Tag, "onEvent:Send" + bluetoothSend.bytes);
    }
  • 接收数据处理

    1
    2
    3
    4
    5
    6
      //数据转换                后台进程            优先级3       非粘性事件
    @Subscribe(threadMode = ThreadMode.BACKGROUND, priority = 3, sticky = false)
    public void onEvent(Events.bluetooth_Transformers bluetoothTransformers) {
    int bytes = bluetoothTransformers.bytes;
    //处理数据
    }


资料来源如下

  • 第一行代码(第二版)
  • Android Training Notifying the User
  • Android API 通知
  • 【Android】状态栏通知Notification、NotificationManager详解

编程环境

  • Android Studio 2.2.3

导语

  • OneTapDoze遇到的第一个难题,顺带记录Android 通知相关内容

最终效果

  • 绿色守护进入doze模式后,会在通知栏创建一个计时通知 如下图
    Screenshot_20170307-231118.png

  • 退出doze模式后,会指示进入doze和退出doze的时间段。
    Screenshot_20170307-231132.png

  • 我们要仿照的样式就是这样,进入退出doze,对应创建通知的代码都在doze模式改变的Broadcast中。

基础部分

创建通知

  • 行文前:
    Android每次版本几乎都会有通知API的改动,不同版本之间通知兼容性很是问题,为此我们使用support_v7 库中的NotificationCompat.Builder代替Notification.Builder二者使用方式相同

  • 涉及到的两个类

    • NotificationManager
    • Notification (support库中对应NotificationCompat)
  • NotificationManager

    • 状态栏通知的管理类,负责发通知、清除通知等
    • NotificationManager 是一个系统Service,必须通过 getSystemService()方法来获取
  • Notification

    • 具体的状态栏通知对象,可以设置icon、文字、提示声音、振动等参数
    • 一个Builder对象至少包含三个方面
      • 一个小图标,通过setSmallIcon()方法设置。
      • 通知标题,通过setContentTitle()方法设置。
      • 详细文本,通过setContentText()方法设置。
  • 简单示例

    • 创建通知构建器
      代码

      1
      2
      3
      4
      5
      6
      Notification notification = new NotificationCompat.Builder(this)
      .setContentTitle("这是通知标题")
      .setContentText("这是通知内容")
      //这里使用的是应用图标,一般没人这么干,就是为了方便
      .setSmallIcon(R.mipmap.ic_launcher)
      .build();
    • 发布通知

      • 获得NotificationManager的实例
      • 使用notify()方法发布通知。在调用notify()方法 指定通知的ID,(ID用于通知更新) 加载Notification实例
      1
      2
      3
      4
      int mNotificationId = 001;

      NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
      manager.notify(mNotificationId, notification);
  • 效果如下

    • android 7.0 / 6.0 /4.0 通过
    • ScreenShot_20170202173430.png

更新通知

  • 可以创建一个全新的NotificationCompat对象,也可以在原NotificationCompat对象基础上修改,最后只要 .notify 方法中对应同一个 mNotificationId ,系统就会自动更新已有通知,代码不在累赘。

  • 基础部分到此,足矣

正题

  • 实现时间段的显示,肯定会保存系统进入doze 退出doze对应的时间点。再计算出中间经历的时间。

提取系统时间

  • java.util.Date
    这里用到了java中的 时间类型 java.util.Date
    java.util.Date 是java中常用时间类型,可以被SimpleDateFormat格式化format() 指定输出的时间格式
    比如我们需要 小时:分:秒
    SimpleDateFormat scanf = new SimpleDateFormat("HH:mm:ss");
    scanf.format(java.util.Date) 即可。

  • 提前当前系统时间

    1
    2
    java.util.Date time_after = null;
    time_after = new java.util.Date(System.currentTimeMillis());
  • 计算时间差
    理论上是两个时间相减再格式化输出即可,可惜没成,原因还没找到。于是土办法:

    • 调用 .getTime() 方法,将 java.util.Date 转化为毫秒计时(long)
    • 取两者差值
    • 转化为 时间长短 字符串返回,
  • 代码如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
     size = (time_after.getTime() - time_befor.getTime());
    string = scanf.format(time_befor) + "-" + scanf.format(time_after) + "="+ trform(size);

    String trform(long size) {
    long day, hour, min, secone;

    day = size / (1000 * 60 * 60 * 24); //以天数为单位取整
    hour = (size / (60 * 60 * 1000) - day * 24); //以小时为单位取整
    min = ((size / (60 * 1000)) - day * 24 * 60 - hour * 60); //以分钟为单位取整
    secone = (size / 1000 - day * 24 * 60 * 60 - hour * 60 * 60 - min * 60);

    if (day != 0) {
    return day + "d" + hour + "h" + min + "m" + secone + "s";
    } else if (hour != 0) {
    return hour + "h" + min + "m" + secone + "s";
    } else if (min != 0) {
    return min + "m" + secone + "s";
    } else {
    return secone + "s";
    }
    }

通知扩展布局

  • 在如下图片中,可以发现,通知在发出后,只默认显示最近一次的时间记录,其他时间记录可以在此条通知上下滑查看。明显与基础部分不同。
    Screenshot_20170307-231132.png

  • 此处应用了 Builder.setStyle() 拓展布局,顾名思义,这是用于拓展通知显示范围


使用扩展布局

  • 使用拓展布局
    • 构建一个 inboxStyle 对象
    • .addLine方法 添加 String inboxStyle 行
    • 使用 Builder.setStyle( inboxStyle )加载到通知
    • 等待通知发布即可。
  • 简单例程如下
    1
    2
    3
    4
    5
    6
    //创建 inboxStyle
    NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle();
    //添加 String 行 ,前提是 我已经创建了一个 String
    inboxStyle.addLine(string);
    // 在notification 设置 inboxStyle 在一堆 build 里面
    .setStyle(inboxStyle)
  • 清除原有内容,发布新的 inboxStyle 只需要 inboxStyle = new NotificationCompat.InboxStyle();

Doze模式改变广播

  • 对应 ACTION_DEVICE_IDLE_MODE_CHANGED

  • 坑:只能动态注册,静态注册无效,具体代码中powermanger.ACTION_DEVICE_IDLE_MODE_CHANGED 需要实例化 PowerManger ,而 PowerManger 又是与 Activity绑定,所以只有应用保持后台存活时才会进入广播。
  • 例程如下
    1
    2
    3
    4
    5
    6
    7
    8
    //实例化 PowerManager
    PowerManager powermanger = (PowerManager) getApplicationContext().getSystemService(Context.POWER_SERVICE);
    //动态注册广播
    intentFilter = new IntentFilter();
    intentFilter.addAction(powermanger.ACTION_DEVICE_IDLE_MODE_CHANGED);
    //idlemodechage 是 BroadcastReceiver
    idlemodechage = new IdleModeChange();
    registerReceiver(idlemodechage, intentFilter);


资料来源如下

  • 第一行代码(第二版)
  • android6.0运行时权限详解

编程环境

  • Android Studio 2.2.3

导语

  • 内容提供器是安全的应用间共享数据途径,在正式进入之前需要了解一下 Android 自6.0以来权限的变化

Android 权限详解


资料来源:

  • android6.0运行时权限详解
  • Develop-API Guides-系统权限
  • 第一行代码(第二版)

Android权限介绍

  • Android 是一个权限分隔的操作系统
    每个应用系统标识(Linux 用户 ID 和组 ID)不同。系统各部分标识亦不相同。Linux 据此将不同的应用以及应用与系统分隔开来。
    更详细的安全功能通过“权限”提供,权限 会限制特定进程可以执行的具体操作,并且根据 URI 权限授权临时访问特定的数据段

  • 权限声明
    最常见的权限声明在 AndroidManifest.xml 文件中一个或多个uses-permission 标记,

    • 例如 声明短信权限
      1
      2
      3
      4
      5
      <manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.android.app.myapp" >
      <uses-permission android:name="android.permission.RECEIVE_SMS" />
      ...
      </manifest>
  • android 6.0 权限机制
    android 6.0之前 权限及在安装的时候,根据权限声明产生一个权限列表,用户只有在同意之后才能完成app的安装。
    android 6.0 以后 引入了 运行时权限 功能,应用所需权限不再一次性在安装时申请,而是在运行时需要用到那种权限向用户申请,同时用户也可以随时取消该授权

  • 权限分类

    • Normal Permissions(普通权限)
      不涉及用户隐私,不需要用户进行授权的,例如 手机震动、访问网络等
    • Dangerous Permission(危险权限)
      涉及到用户隐私的,需要用户进行授权,例如 读取sdcard、访问通讯录等
      危险权限组如表1

应用运行时申请权限(适用于 Android6.0 及以上版本)

  • 通过一个简单的实例来说明过程

    实例:

  • 拨打电话 CALL_PHONE _权限为例

    • 权限声明
      1
      <uses-permission android:name="android.permission.CALL_PHONE"/>
  • 拨打10010

    • 代码如下 构建一个隐式intent 启动拨打电话
      1
      2
      3
      Intent intent = new Intent(Intent.ACTION_CALL);
      intent.setData(Uri.parse("tel:10010"));
      startActivity(intent);
  • 在主界面上添加一个Button 在其onClick方法中拨打电话,运行程序!


  • 在An’d’roid 6.0 以下版本中 电话都可以正常拨出。Android 6.0 以上版本中 则可能会应用崩溃/无反应,查看logcat的打印日志可以看到 “Permission Denial”权限被禁止的信息,接下来尝试使用 运行时申请权限

  • 检查是否已获得授权
    int checkSelfPermission(Context context, String permission)方法

    • Context context context
      String permission 权限具体名称
      打电话对应 Manifest.permission.CALL_PHONE _
    • 方法返回 PERMISSION_GRANTED / PERMISSION_DENIED
    • 例程
      1
      2
      3
      4
      5
      6
      7
      8
      if(ContextCompat.checkSelfPermission
      (MainActivity.this,Manifest.permission.CALL_PHONE)
      !=PackageManager.PERMISSION_GRANTED)
      {
      ;
      }else {
      ;
      }
  • 申请权限
    void ActivityCompat.requestPermissions (Activity activity, String[] permissions, int requestCode)方法

    • Activity activity Activity实例
      String[] permissions 请求权限名
      int requestCode 请求码,大于0即可,对应在onRequestPermissionsResult 回掉方法中的requestCode

    • 例程

      1
      2
      ActivityCompat.requestPermissions(MainActivity.this,
      new String[]{Manifest.permission.CALL_PHONE},1 );
  • 授权后回掉
    无论用户是否授权都会进入回掉方法,需要重写该方法。
    void onRequestPermissionsResult (int requestCode, String[] permissions, int[] grantResults)方法

    • int requestCode 在 requestPermissions 中传入的请求码
      String[] permissions 请求的权限,不为空
      int[] grantResults 授权结果 只有 PERMISSION_GRANTED / PERMISSION_DENIED 两个值

    • 例程

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      @Override
      public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
      switch (requestCode) {
      //请求码 1
      case 1:
      //授权结果(int) 的 长度是否大于0 与上 授权结果是否等于 PackageManager.PERMISSION_GRANTED
      if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
      //授权通过执行
      } else {
      //授权没有通过
      }
      break;
      default:
      }
      }
  • 当用户拒绝后再次询问
    boolean ActivityCompat.shouldShowRequestPermissionRationale(Activity activity, String permission) (真心太长了)

    • Activity activity Activity实例
      String permission 权限名称
      应用第一次需要授权时,用户授权,该方法返回false。当用户拒绝授权,下次需要该权限时,该方法会返回true
    • 例程
      1
      2
      3
      4
      5
      6
      7
       if (ActivityCompat.shouldShowRequestPermissionRationale
      (PermissionActivity.this,Manifest.permission.READ_CONTACTS))
      {
      //第二次询问用户是否授权
      }else{
      //用户依旧拒绝后操作
      }
  • 例程源码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    }

    public void Button(View view){
    if(ContextCompat.checkSelfPermission(MainActivity.this,Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED){
    ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.CALL_PHONE},1 );
    }else {
    call();
    }

    }

    private void call() {
    try {
    Intent intent = new Intent(Intent.ACTION_CALL);
    intent.setData(Uri.parse("tel:10010"));
    startActivity(intent);
    } catch (SecurityException e) {
    e.printStackTrace();
    }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
    switch (requestCode) {
    case 1:
    if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
    call();
    } else {
    Toast.makeText(this, "You denied the permission", Toast.LENGTH_SHORT).show();
    }
    break;
    default:
    }
    }

    }
    1
    2
    3
    4
    5
    <Button
    android:onClick="Button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Hello World!" />


资料来源如下

  • 第一行代码(第二版)

编程环境

  • Android Studio 2.2.3

Android 中的主要数据存储

  • 有三种

  • 保存键值集
    要保存的相对较小键值集合,使用 SharedPreferences

  • 保存文件
    使用 Android 文件系统通过 File API 读取和写入文件,适合按开始到结束的顺序不跳过地读取或写入大量数据。 例如,图片文件或通过网络交换的任何内容

  • 在 SQL 数据库中保存数据
    Android内建的SQLite 数据库存储重复或结构化数据


  • PS:这里如果没有学过 SQL 数据库使用,推荐LitePal
  • Android开源数据库LitePal :郭霖(第一行代码作者)
  • 详细介绍在这里(懒人必备)Android开源数据库LitePal

保存键值集

  • 想要保存的相对较小键值集合

  • 调用SharedPreferences

  • 首先:获取SharedPreferences的句柄
    用户可以创建一个共享参数文件或访问一个已经存在的共享参数文件。
    具体途径有三条

    • getSharedPreferences (String name, int mode)
      • 第一个参数:SharedPreferences的名称,第二个:参数指定操作模式
      • 需要创建/读取 多个共享参数文件时使用,每个文件都拥有一个标识符,可以通过这个标识符通过该方法的第一个参数获得共享参数对象。可以通过APP中的任意Context对象调用它。
      • 示例
        1
        2
        //指定SharedPreferences名称为data  模式为MODE_PRIVATE:只有当前程序才有权限读取/修改
        SharedPreferences sharedPref = getSharedPreferences("data", MODE_PRIVATE);
    • getPreferences (int mode)
      • 参数:指定操作模式
      • 只创建 Activity 的一个SharedPreferences。在Activity 中使用。 方法会检索属于该 Activity 的默认共享首选项文件,无需提供名称
      • 示例
        1
        2
        //模式:MODE_PRIVATE:只有当前程序才有权限读取/修改
        SharedPreferences sharedPref = getActivity().getPreferences(Context.MODE_PRIVATE);
    • getDefaultSharedPreferences(context)
      • 参数:context
      • 自动以档期应用程序包名作为前缀命名SharedPreferences文件
  • 写入数据到共享参数中
    • SharedPreferences 调用 edit() 创建一个 SharedPreferences.Editor
      使用诸如 putInt() 和 putString() 方法写入的键和值
      最后调用commit() 以保存更改
      也可调用apply() 保存更改

  • commit() 与 apply() 相同/区别
  • 相同
    • 都是提交preference修改数据
  • 区别
    • apply没有返回值 commit返回boolean表明修改是否提交成功
    • apply是将操作提交到内存,而后异步真正提交到文件
      commit是同步的提交到文件
      多个commit同时提交,互相等待,效率较低
      apply直接覆盖,效率较高
    • apply没有任何错误提示,只是提交的动作

  • 示例
    1
    2
    3
    4
    5
    6
    //创建 .edit()
    SharedPreferences.Editor editor = sharedPref.edit();
    //写入字符串。key-值 TextView
    editor.putString("TextView","hello");
    //保存更改
    editor.commit();
  • 从共享参数中读取数据
    • 获取SharedPreferences的句柄
      方式同上
    • 调用比如getInt()或getString()方法,然后传入键值,如果键不存在,则会返回一个默认值
    • 示例
      1
      2
      3
      4
      5
      6
      //获取SharedPreferences的句柄
      SharedPreferences sharedPreferences = getSharedPreferences("data",MODE_PRIVATE);
      //读取键值对应数据
      String text = sharedPreferences.getString("TextView","");
      //打印到log中 验证
      Log.d("data",text);

保存到文件

  • Android 设备有两个文件存储区域: 内部存储 和 外部存储
    Android 设备早期内置的非易失性内存(内部存储),以及移动存储介质 微型 SD 卡等(外部存储)
    任何Android设备始终有两个存储空间,并且无论外部存储设备是否可移动,API 的行为均一致

  • Android的文件存储是以java流为基础,并优化了一些操作。java流不属于本文范围。

  • 涉及java流部分参考Android文件IO详解

内部存储

保存到内部存储

  • 某一应用对其内部存储 始终具有 进行读写的权限,而无需声明任何权限,在 Android N之前,内部文件可以通过放宽权限让其他应用访问。但是极不推荐以这种方式共享文件
  • 简单实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    public void save(String inputText){
    //新建一个FileOutputStream对象(字节流)
    FileOutputStream out = null;
    //新建一个BufferedWriter (字符流)
    BufferedWriter writer = null;
    try{
    //out 实例化
    out = openFileOutput("data",Context.MODE_PRIVATE);
    //OutputStreamWrit 字节流转换为字符流
    //字符流绑定BufferedWriter(缓冲区)
    writer = new BufferedWriter(new OutputStreamWriter(out));
    //写入到文件
    writer.write(inputText);
    }catch (IOException e ){
    e.printStackTrace();
    } finally {
    try{
    //如果写入成功
    if (writer != null) {
    //关闭流
    writer.close();
    }
    }catch (IOException e){
    e.printStackTrace();
    }
    }
    }
  • FileOutputStream:

    • 继承自 OutputStream 用于文件处理的节点流
    • 用于写入原始字节流到文件
  • FileOutputStream openFileOutput (String name, int mode)

    • 返回FileOutputStream对象
    • 第一个参数是文件名
    • 第二个参数是操作模式,有4种取值
      • MODE_PRIVATE(默认)_当同一文件名存在时覆盖原文件
      • MODE_APPEND _当同名文件存在时,在原文件末尾追加
      • 其余两种均在android4.2及以上版本中废弃
  • OutputStreamWrit

    • 继承自 Writer
    • 将字节流转换为字符流
  • BufferedWriter

    • 继承自Writer
    • 绑定字符输出流,提高具体的流对象的效率
    • .flush()方法 对缓冲区进行刷新,让数据到目的地
    • .close();方法 关闭缓冲区,即关闭绑定的流
从内部存储读取文件
  • 简单实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    public String load(){
    //新建FileInputStream对象
    FileInputStream in = null;
    //新建 BufferedReader对象
    BufferedReader reader = null;
    //新建一个StringBuilder 空
    content = new StringBuilder();
    try{
    //FileInputStream对象实例化
    in = openFileInput("data");
    //字节流转换为字符流
    //缓冲流绑定字符流
    reader = new BufferedReader(new InputStreamReader(in));
    //空 String
    String line = "";
    //读取文件内容,并判断是否读取完成
    while((line = reader.readLine())!=null){
    //读取内容添加到 StringBuilder 对象中
    content.append(line);
    }
    }catch (IOException e){
    e.printStackTrace();
    }
    finally {
    if(reader != null){
    try{
    //关闭流
    reader.close();
    }catch (IOException e){
    e.printStackTrace();
    }
    }
    }
    //将读取结果 由StringBuilder转换为String 并返回
    return content.toString();
    }
  • FileInputStream

    • 继承自 InputStream,用于文件处理的节点流
    • 从文件中读取
  • FileInputStream openFileInput(String filename)

    • 返回FileInputStream对象
    • 参数为 读取的文件名
  • InputStreamRead

    • 将字节流转换为字符流
  • BufferedReader

    • 缓冲流

外部存储

  • 占坑