Java泛型的实战应用

Java泛型的实战应用
Java有很多的高级特性,泛型是其中之一,泛型即参数化类型。关于泛型的概念,有很多文章都有介绍,这里就不再过多的介绍了。本文将从实战的角度,来看看泛型在实际项目中的使用
1 泛型在框架中的使用
泛型在框架中及为常见,我们在使用各种框架的时候,都会使用到泛型,具体看下面的例子。
1.1 集合框架中使用泛型
这是*常见的泛型的使用场景,比如下面的代码
List<String> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
list1和list2虽然都是列表类型,但是列表里面存的数据可以是String,可以是Integer,也可以是自定义类型。集合中存放的数据,在定义的时候不能确定是什么类型,只有当使用集合时,才能确定放什么类型的数据。所以在集合的定义中,就用泛型来代表集合里面的数据。
1.2 fastjson框架中使用泛型
alibaba的fastjson很多人应该都用过,fastjson很多地方也用到了泛型,比如将json字符串转成对象,如下的例子
// 将userStr字符串映射成UserDto类
String userStr = “{id: ‘123’, name: ‘张三’}”;
UserDto userDto = JSON.parseObject(userStr, UserDto.class);
JSON类中,对parseObject方法的定义,如下
public static <T> T parseObject(String text, Class<T> clazz) {
    return parseObject(text, clazz);
}
parseObject方法中的参数传递用到了泛型,要把json字符串转成什么类,在定义的时候并不知道,只有在用到的时候,才知道具体的类型。
1.3 泛型使用场景总结
综合集合框架和JSON框架的例子,我们可以大概总结出泛型的使用场景,便于理解:不管是数据存储还是参数传递,定义的时候,类型并不确定,只有到使用的时候,才知道具体的类型。所以我们的项目中,如果有用到这种不确定类型的时候,就可以考虑泛型。
当然,泛型还有更多的使用场景,比如泛型接口,这里就不一一举例了。
2 泛型的实战应用
2.1 数据的存储使用泛型类
在实际项目开发中,有一种场景:前后端分离时,为了方便前后端的对接,一般会统一定义参数的传递格式和结果的返回格式。以结果返回为例,一般都会定义【返回码】,【返回描述】,【返回数据】等,所以可以定义一个ResponseDto类,如下:
public class ResponseDto {
    /**
     * 返回码
     */
    private String code;
    /**
     * 返回信息
     */
    private String message;
    /**
     * 返回数据,???,应该定义成什么类型呢?
     */
    private ??? content;
// 省略set get
}
【返回码】一般是前后端约好的字符串类型,【返回描述】一般就是字符串类型,【返回数据】就不一定了,如果是查询类的请求,返回的数据就是列表信息,可能还包含分页信息;如果是保存、删除之类的请求,返回的数据可能是一条数据,也可能只是ID,也可能不需要返回数据。所以【返回数据】这个字段就是个不确定的类型,可以定义成泛型。所以我们就可以把ResponseDto类改成下面这样:
public class ResponseDto<T> {
    /**
     * 返回码
     */
    private String code;
    /**
     * 返回信息
     */
    private String message;
    /**
     * 返回数据
     */
    private T content;
}
使用的方法如下:
// 返回单个UserDto对象
ResponseDto<UserDto> responseDto = new ResponseDto<>();
responseDto.setContent(userDto); // userDto是已有的变量
// 返回UserDto列表
ResponseDto<List<UserDto>> responseDto = new ResponseDto<>();
responseDto.setContent(userDtoList); // userDtoList是已有的变量
// 返回ID
ResponseDto<String> responseDto = new ResponseDto<>();
responseDto.setContent(id); // id是已有的变量
这个类就叫做泛型类。
2.2 参数的传递使用泛型方法
以BeanUtils.copyProperties为例,大家应该对这个方法不陌生,就是将一个实体类中的属性值,拷贝到另一个实体类中。一般我们的使用方法是这样的:
// 功能:将UserDto数据拷贝到User
User user = new User();
BeanUtils.copyProperties(userDto, user); // userDto是已有的变量
但是每次都要写两行代码有点麻烦,要先new一个新的实体类,再往里面拷数据。于是我封装了个通用的工具类
public class CopyUtil {
    /**
     * CopyUtil.copy的定义很类似JSON.parseObject的定义
     */
    public static <T> T copy(Object source, Class<T> clazz) {
        if (source == null) {
            return null;
        }
        T obj = null;
        try {
            obj = clazz.newInstance(); // 生成一个实例
        } catch (Exception e) {
            e.printStackTrace();
        }
        BeanUtils.copyProperties(source, obj);
        return obj;
    }
}
同样是上面的例子,用工具类的写如下
User user = CopyUtil.copy(userDto, User.class); // userDto是已有的变量
CopyUtil.copy的定义很类似JSON.parseObject的定义。代码变成了一行,当然,减少一行也不足以将其封装成一个工具类。再来看一个场景,列表的拷贝,用BeanUtils.copyProperties的写法如下
// 功能:将List<UserDto>数据拷贝到List<User>
List<User> userList = new ArrayList<>();
// userDtoList是已有的变量
for (int i = 0, l = userDtoList.size(); i < l; i++) {
    UserDto userDto = userDtoList.get(i);
    User user = new User();
    BeanUtils.copyProperties(userDto, user);
    userList.add(user);
}
这样的代码量就比较多了,并且代码写法比较固定。如果项目中用到列表复制的功能比较多,那就有必要对其进行封装了,如下的copyList方法:
import org.springframework.beans.BeanUtils;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.List;
/**
 * @author 甲蛙
 */
public class CopyUtil {
    /**
     * 列表复制
     */
    public static <T> List<T> copyList(List source, Class<T> clazz) {
        List<T> target = new ArrayList<>();
        if (!CollectionUtils.isEmpty(source)){
            if (!CollectionUtils.isEmpty(source)){
                for (Object c: source) {
                    T obj = copy(c, clazz);
                    target.add(obj);
                }
            }
        }
        return target;
    }
    /**
     * 单体复制
     */
    public static <T> T copy(Object source, Class<T> clazz) {
        if (source == null) {
            return null;
        }
        T obj = null;
        try {
            obj = clazz.newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
        BeanUtils.copyProperties(source, obj);
        return obj;
    }
}
用法很简单,还是以上面的列表复制为例,代码就变成这样:
// 功能:将List<UserDto>数据拷贝到List<User>
List<User> userList = CopyUtil.copyList(userDtoList, User.class);
封装成CopyUtil工具类后,不管是单体复制还是列表复制,都只需要一行代码,省去了大量的重复代码,从而提高开发效率,这里的copy方法和copyList方法,就是泛型方法,在方法的参数中使用了泛型。
当然这个工具类有个缺点,就是用到了反射,会牺牲一点性能,不过这点性能对于大部分的项目来说可以忽略不计。
2.3 总结
本文讨论了两种泛型的实战应用,一种是用来存储类型不确定的数据,用到泛型类;一种是用来传递类型不确定的参数,用到了泛型方法。当然,泛型还有更多的用法,比如泛型接口,比较典型的是比较器接口Comparable,这里就不再展开了。

TF模型部署的特点

TF模型部署的特点
TF模型部署的特点
可扩展性、稳定性、可靠性(能经历大规模多应用部署的考验)好
可扩展性、稳定性、可靠性(能经历大规模多应用部署的考验)好
TF serving高性能,开源的机器学习服务系统,专为部署设计的TF serving可以结合docker一起使用,部署方便提供了REST和GRPC的接口支持regression,classify,predict 的apiserving的轻量化可以很好的解决服务问题
TF采用分布式的架构,对云的处理适应能力好,适合大规模项目的部署
TF有Js的版本,支持的浏览器端训练推理
Tensorboard的可视化工具便于查看数据
各个平台上都能跑,win、Linux、MacOS、Android、iOS都能方便的使用
可以用python接口,也有高性能的C++接口
支持离线多机多cpu、gpu的分布式训练,还有在线提供serving的一整套解决方案,快速落地项目,*大减少工程成本

格式化金额的简单解法

格式化金额的简单解法
很久没有写技术相关的博客了,刚好看到一个题目就想着写一个解法吧,因为简单,所以可能会比较实用。
问:请写出一个格式化金额的方法。
这个其实挺常见的,不过程序员都是比较懒的能上网搜的到的肯定不会自己写,有Git仓库的肯定直接拉来就用,我当然也不例外。
所以我就找了一个网上的方法。
//* 金额千分位加逗号,保留2位小数,不足补零,否则四舍五入
// * 参数说明:
// * num:要格式化的数字 string或者number
// * decimals:保留几位小数
// * dec_point:小数点符号
// * thousandsSep:千分位符号
// return 金额格式的字符串,如’1,234,567.45′
// * */
function number (num, decimals, thousandsSep) {
    if (isNaN(num)) {
        num = ‘0.00’
    }
    let prec = !isFinite(+decimals) ? 0 : Math.abs(decimals) // 保留的位数一定是有限位数的正整数
    let sep = (typeof thousandsSep === ‘undefined’) ? ‘,’ : thousandsSep
    let s = num.toString().replace(/,/g, ”) // 字符串,将,变成”;
    let p = parseFloat(s) // 解析一个字符串,并返回一个浮点数
    let n = isNaN(p) ? 1 : p
    let formatNum = n.toFixed(prec).toString().replace(/(\d)(?=(\d{3})+\.)/g, function ($0, $1) {
        return $1 + sep
    })
    return num ? formatNum : ”
}
// let num = number(333322.8199, 5,”)
// console.log(num,’nm’)
恩。。。其实也蛮简单的,但是看到正则了,正则对萌新其实是不友好的,所以我就自己又写了一个,希望对你有帮助吧。
 /**
@number:需要格式化的数字
@decimals:小数点位数
@separator:千分位分割符
**/
function formatNumber(number,decimals,separator){
  if(isNaN(Number(number))){
    return “0”
  }
  let _separator = separator||”,”;
  let _decimals = decimals||2;
  let numberStr =   number.toFixed(_decimals);
  let splitArray = numberStr.split(“.”);
  let left = splitArray[0];
  let right = splitArray[1];
  let array = left.split(”).reverse();
  let array2 = []
  for(let i = 0; i < array.length; i++){
    if(i>0&&i%3===0){
      array2.push(_separator);
    }
    array2.push(array[i]);
  }
  return array2.reverse().join(“”)+”.”+right;
}
let number = 20200421.56787;
formatNumber(number);
console .log(formatNumber(number,3,null))
//20,200,421.57
我的思路其实比较简单,线分割小数点,整数部分处理千分位,处理方法也比较常见,就是字符串和数组的一些方法,先把数组倒转,然后循环插入分隔符。
好了,就这样吧。如果你记不住正则,那么这个解法适合。
*后反思一下, 阿里给的*轮笔试就挂了,啊哈哈,三个题目就做了两个半,题目都很简单,是我自己没准备,虽然我比较讨厌刷题,但是如果你是为了一份工作的话,尤其是大厂,还是考虑下刷题吧,虽然拿到了别的offer。

JS的赋值与深浅拷贝实例

JS的赋值与深浅拷贝实例
Python入门教程100天
专栏收录该内容
129 篇文章6 订阅
订阅专栏
JS的赋值与深浅拷贝实例
赋值
基本类型: 传值,在栈内存中的数据发生数据变化的时候,系统会自动为新的变量分配一个新的之值在栈内存中,两个变量相互独立,互不影响的。
引用类型: 传址,只改变指针的指向,指向同一个对象,两个变量相互干扰
//基本数据类型赋值
var a = 10;
var b = a;
a++ ;
console.log(a); // 11
console.log(b); // 10
//引用数据类型赋值
let a = { name: ’11’ }
let b = a
b.name = ’22’
console.log(a.name) // 22
console.log(b.name) // 22
浅拷贝
对于基本类型,浅拷贝是对值的复制,拷贝前后对象的基本数据类型互不影响
对于引用类型来说,浅拷贝是对对象地址的复制, 也就是拷贝的结果是两个对象指向同一个地址,所以如果其中一个对象改变了这个地址,就会影响到另一个对象
注意:浅拷贝只复制一层对象的属性
实现浅拷贝方法
方法一:Object.assign
**语法:Object.assign(target, …sources) **ES6中拷贝对象的方法,接受的*个参数target是拷贝后的对象,剩下的参数是要拷贝的对象sources(可以是多个)
例1:
let target = {};
let source = {a:’11’,b:{name:’duoduo’}};
Object.assign(target ,source);
console.log(target); // { a: ’11’, b: { name: ‘duoduo’ } }
例2:
let target = {};
let source = {a:’11’,b:{name:’duoduo’}};
Object.assign(target ,source);
source.a = ’22’;
source.b.name = ‘nana’
console.log(source); // { a: ’22’, b: { name: ‘nana’ } }
console.log(target); // { a: ’11’, b: { name: ‘nana’ } }
Object.assign注意事项
只拷贝源对象的自身属性(不拷贝继承属性)
它不会拷贝对象不可枚举的属性
undefined和null无法转成对象,它们不能作为Object.assign参数,但是可以作为源对象
属性名为Symbol 值的属性,可以被Object.assign拷贝。
方法二:Array.prototype.slice
实现原数组的浅拷贝
var a = [ 1, 3, 5, { x: 1 } ];
var b = Array.prototype.slice.call(a);
b[0] = 2;
console.log(a); // [ 1, 3, 5, { x: 1 } ];
console.log(b); // [ 2, 3, 5, { x: 1 } ];
// 从输出结果可以看出,浅拷贝后,数组a[0]并不会随着b[0]改变而改变
// 说明a和b在栈内存中引用地址并不相同。
var a = [ 1, 3, 5, { x: 1 } ];
var b = Array.prototype.slice.call(a);
b[3].x = 2;
console.log(a); // [ 1, 3, 5, { x: 2 } ];
console.log(b); // [ 1, 3, 5, { x: 2 } ];
// 从输出结果可以看出,浅拷贝后,数组中对象的属性会根据修改而改变
// 说明浅拷贝的时候拷贝的已存在对象的对象的属性引用。
方法三:Array.prototype.concat
let array = [{a: 1}, {b: 2}];
let array1 = [{c: 3},{d: 4}];
let array2=array.concat(array1);
array1[0].c=123;
console.log(array2);// [ { a: 1 }, { b: 2 }, { c: 123 }, { d: 4 } ]
console.log(array1);// [ { c: 123 }, { d: 4 } ]
方法四:…扩展运算符
语法:var cloneObj = { …obj };
var a = [ 1, 3, 5, { x: 1 } ];
var b = {…a};
b[0] = 2;
console.log(a); // [ 1, 3, 5, { x: 1 } ];
console.log(b); // [ 2, 3, 5, { x: 1 } ];
// 从输出结果可以看出,浅拷贝后,数组a[0]并不会随着b[0]改变而改变
// 说明a和b在栈内存中引用地址并不相同。
var a = [ 1, 3, 5, { x: 1 } ];
var b = {…a};
b[3].x = 2;
console.log(a); // [ 1, 3, 5, { x: 2 } ];
console.log(b); // [ 1, 3, 5, { x: 2 } ];
// 从输出结果可以看出,浅拷贝后,数组中对象的属性会根据修改而改变
// 说明浅拷贝的时候拷贝的已存在对象的对象的属性引用。
其他方法:
//浅拷贝实现
var obj = { a:1, arr: [2,3] };
var shallowObj = shallowCopy(obj);
function shallowCopy(src) {
  var dst = {};
  for (var prop in src) {
    if (src.hasOwnProperty(prop)) {
      dst[prop] = src[prop];
    }
  }
  return dst;
}
shallowObj.arr[1] = 5;
obj.arr[1]   // = 5
深拷贝
深拷贝开辟一个新的栈,两个对象对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性注意:深拷贝对对象中的子对象进行递归拷贝,拷贝前后两个对象互不影响
实现方法:
方法一:JSON.parse(JSON.stringify())
JSON.stringify()是前端开发过程中比较常用的深拷贝方式。
原理是把一个对象序列化成为一个JSON字符串,将对象的内容转换成字符串的形式再保存在磁盘上,再用JSON.parse()反序列化将JSON字符串变成一个新的对象
let arr = [1, 2, {name: ‘ duoduo’}];
let newarr = JSON.parse(JSON.stringify(arr));
newarr[2].name = ‘nana’;
console.log(newarr); // [ 1, 2, { username: ‘nana’ } ]
console.log(arr);    // [ 1, 2, { username: ‘duoduo’ } ]
JSON.stringify()实现深拷贝注意点
拷贝的对象的值中如果有函数,undefined,symbol则经过JSON.stringify()序列化后的JSON字符串中这个键值对会消失
无法拷贝不可枚举的属性,无法拷贝对象的原型链
拷贝Date引用类型会变成字符串
拷贝RegExp引用类型会变成空对象
对象中含有NaN、Infinity和-Infinity,则序列化的结果会变成null
无法拷贝对象的循环应用(即obj[key] = obj)
方法二:jquery 的 $.extend
var $ = require(‘jquery’);
var obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
var obj2 = $.extend(true, {}, obj1);
赋值、深拷贝、浅拷贝
浅拷贝和深拷贝****区别浅拷贝只复制一层对象的属性,而深拷贝则递归复制了所有层级

Elasticsearch系列—Term Vector工具探查数据

Elasticsearch系列—Term Vector工具探查数据
Python入门教程100天
专栏收录该内容
129 篇文章6 订阅
订阅专栏
概要
本篇主要介绍一个Term Vector的概念和基本使用方法。
term vector是什么?
每次有document数据插入时,elasticsearch除了对document进行正排、倒排索引的存储之外,如果此索引的field设置了term_vector参数,elasticsearch还会对这个的分词信息进行计算、统计,比如这个document有多少个field,每个field的值分词处理后得到的term的df值,ttf值是多少,每个term存储的位置偏移量等信息,这些统计信息统称为term vector。term vector的值有5个
no:不存储term vector信息,默认值
yes:只存储field terms信息,不包含position和offset信息
with_positions:存储term信息和position信息
with_offsets:存储term信息和offset信息
with_positions_offsets:存储完整的term vector信息,包括field terms、position、offset信息。
term vector的信息生成有两种方式:index-time和query-time。index-time即建立索引时生成term vector信息,query-time是在查询过程中实时生成term vector信息,前者以空间换时间,后者以时间换空间。
term vector有什么作用?
term vector本质上是一个数据探查的工具(可以看成是一个debugger工具),上面记录着一个document内的field分词后的term的详细情况,如拆分成几个term,每个term在正排索引的哪个位置,各自的df值、ttf值分别是多少等等。一般用于数据疑似问题的排查,比如说排序和搜索与预期的结果不一致,需要了解根本原因,可以拿这个工具手动进行数据分析,帮助判断问题的根源。
读懂term vector信息
我们来看看一个完整的term vector报文,都有哪些信息,带#号的一行代码是添加的注释,如下示例:
{
  “_index”: “music”,
  “_type”: “children”,
  “_id”: “1”,
  “_version”: 1,
  “found”: true,
  “took”: 0,
  “term_vectors”: {
    “text”: {
      “field_statistics”: {
        “sum_doc_freq”: 3,
        “doc_count”: 1,
        “sum_ttf”: 3
      },
      “terms”: {
        “elasticsearch”: {
          “doc_freq”: 1,
          “ttf”: 1,
          “term_freq”: 1,
          “tokens”: [
            {
              “position”: 2,
              “start_offset”: 11,
              “end_offset”: 24
            }
          ]
        },
        “hello”: {
          “doc_freq”: 1,
          “ttf”: 1,
          “term_freq”: 1,
          “tokens”: [
            {
              “position”: 0,
              “start_offset”: 0,
              “end_offset”: 5
            }
          ]
        },
        “java”: {
          “doc_freq”: 1,
          “ttf”: 1,
          “term_freq”: 1,
          “tokens”: [
            {
              “position”: 1,
              “start_offset”: 6,
              “end_offset”: 10
            }
          ]
        }
      }
    }
  }
}
一段完整的term vector信息,term vector是按field为维度来统计的,主要包含三个部分:
field statistics
term statistics
term information
field statistics
指该索引和type下所有的document,对这个field所有term的统计信息,注意document的范围,不是某一条,是指定index/type下的所有document。
sum_doc_freq(sum of document frequency):这个field中所有的term的df之和。
doc_count(document count):有多少document包含这个field,有些document可能没有这个field。
sum_ttf(sum of total term frequency):这个field中所有的term的tf之和。
term statistics
hello为当前document中,text field字段分词后的term,查询时设置term_statistics=true时生效。
doc_freq(document frequency):有多少document包含这个term。
ttf(total term frequency):这个term在所有document中出现的频率。
term_freq(term frequency in the field):这个term在当前document中出现的频率。
term information
示例中tokens里面的内容,tokens里面是个数组
position:这个term在field里的正排索引位置,如果有多个相同的term,tokens下面会有多条记录。
start_offset:这个term在field里的偏移,表示起始位置偏移量。
end_offset:这个term在field里的偏移量,表示结束位置偏移量。
term vector使用案例
建立索引music,type命名为children,指定text字段为index-time,fullname字段为query-time
PUT /music
{
  “mappings”: {
    “children”: {
      “properties”: {
        “content”: {
            “type”: “text”,
            “term_vector”: “with_positions_offsets”,
            “store” : true,
            “analyzer” : “standard”
         },
         “fullname”: {
            “type”: “text”,
            “analyzer” : “standard”
        }
      }
    }
  }
}
添加3条示例数据
PUT /music/children/1
{
  “fullname” : “Jean Ritchie”,
  “content” : “Love Somebody”
}
PUT /music/children/2
{
  “fullname” : “John Smith”,
  “content” : “wake me, shark me …”
}
PUT /music/children/3
{
  “fullname” : “Peter Raffi”,
  “content” : “brush your teeth”
}
对document id为1这条数据进行term vector探查
GET /music/children/1/_termvectors
{
  “fields” : [“content”],
  “offsets” : true,
  “positions” : true,
  “term_statistics” : true,
  “field_statistics” : true
}
得到的结果即为上文的term vector示例。另外可以提一下,用这3个document的id进行查询,field_statistics部分是一样的。
term vector常见用法
除了上一节的标准查询用法,还有一些参数可以丰富term vector的查询。
doc参数
GET /music/children/_termvectors
{
  “doc” : {
    “fullname” : “Peter Raffi”,
    “content” : “brush your teeth”
  },
  “fields” : [“content”],
  “offsets” : true,
  “positions” : true,
  “term_statistics” : true,
  “field_statistics” : true
}
这个语法的含义是针对指定的doc进行term vector分析,doc里的内容可以随意指定,特别实用。
per_field_analyzer参数
可以指定字段的分词器进行探查
GET /music/children/_termvectors
{
  “doc” : {
    “fullname” : “Jimmie Davis”,
    “content” : “you are my sunshine”
  },
  “fields” : [“content”],
  “offsets” : true,
  “positions” : true,
  “term_statistics” : true,
  “field_statistics” : true,
  “per_field_analyzer” : {
    “text”: “standard”
  }
}
filter参数
对term vector统计结果进行过滤
GET /music/children/_termvectors
{
  “doc” : {
    “fullname” : “Jimmie Davis”,
    “content” : “you are my sunshine”
  },
  “fields” : [“content”],
  “offsets” : true,
  “positions” : true,
  “term_statistics” : true,
  “field_statistics” : true,
  “filter” : {
      “max_num_terms” : 3,
      “min_term_freq” : 1,
      “min_doc_freq” : 1
    }
}
根据term统计信息,过滤出你想要看到的term vector统计结果。也挺有用的,比如你探查数据可以过滤掉一些出现频率过低的term。
docs参数
允许你同时对多个doc进行探查,这个使用频率看个人习惯。
GET _mtermvectors
{
   “docs”: [
      {
         “_index”: “music”,
         “_type”: “children”,
         “_id”: “2”,
         “term_statistics”: true
      },
      {
         “_index”: “music”,
         “_type”: “children”,
         “_id”: “1”,
         “fields”: [
            “content”
         ]
      }
   ]
}
term vector使用建议
有两种方式可以得到term vector信息,一种是像上面案例,建立时指定,另一种是直接查询时生成
index-time,在mapping里配置,建立索引的时候,就直接给你生成这些term和field的统计信息,如果term_vector设置为with_positions_offsets,索引所占的空间是不设置term vector时的2倍。
query-time,你之前没有生成过任何的Term vector信息,然后在查看term vector的时候,直接就可以看到了,会on the fly,现场计算出各种统计信息,然后返回给你。
这两种方式采用哪种取决于对term vector的使用期望,query-time更常用一些,毕竟这个工具的用处是协助定位问题,实时计算就行。
小结
term vector是一个比较实用的工具,尤其是针对线上数据进行分析、协助问题定位的时候,可以派上很大的用场。

Android任务和返回栈完全解析,那些你不知道的细节

任务和返回栈

一个应用程序当中通常都会包含很多个Activity,每个Activity都应该设计成为一个具有特定的功能,并且可以让用户进行操作的组件。另外,Activity之间还应该是可以相互启动的。比如,一个邮件应用中可能会包含一个用于展示邮件列表的Activity,而当用户点击了其中某一封邮件的时候,就会打开另外一个Activity来显示该封邮件的具体内容。

 

除此之外,一个Activity甚至还可以去启动其它应用程序当中的Activity。打个比方,如果你的应用希望去发送一封邮件,你就可以定义一个具有”send”动作的Intent,并且传入一些数据,如对方邮箱地址、邮件内容等。这样,如果另外一个应用程序中的某个Activity声明自己是可以响应这种Intent的,那么这个Activity就会被打开。在当前场景下,这个Intent是为了要发送邮件的,所以说邮件应用程序当中的编写邮件Activity就应该被打开。当邮件发送出去之后,仍然还是会回到你的应用程序当中,这让用户看起来好像刚才那个编写邮件的Activity就是你的应用程序当中的一部分。所以说,即使有很多个Activity分别都是来自于不同应用程序的,Android系统仍然可以将它们无缝地结合到一起,之所以能实现这一点,就是因为这些Activity都是存在于一个相同的任务(Task)当中的。

 

任务是一个Activity的集合,它使用栈的方式来管理其中的Activity,这个栈又被称为返回栈(back stack),栈中Activity的顺序就是按照它们被打开的顺序依次存放的。

 

手机的Home界面是大多数任务开始的地方,当用户在Home界面上点击了一个应用的图标时,这个应用的任务就会被转移到前台。如果这个应用目前并没有任何一个任务的话(说明这个应用*近没有被启动过),系统就会去创建一个新的任务,并且将该应用的主Activity放入到返回栈当中。

 

当一个Activity启动了另外一个Activity的时候,新的Activity就会被放置到返回栈的栈顶并将获得焦点。前一个Activity仍然保留在返回栈当中,但会处于停止状态。当用户按下Back键的时候,栈中*顶端的Activity会被移除掉,然后前一个Activity则会得重新回到*顶端的位置。返回栈中的Activity的顺序永远都不会发生改变,我们只能向栈顶添加Activity,或者将栈顶的Activity移除掉。因此,返回栈是一个典型的后进先出(last in, first out)的数据结构。下图通过时间线的方式非常清晰地向我们展示了多个Activity在返回栈当中的状态变化:

 

%title插图%num

 

如果用户一直地按Back键,这样返回栈中的Activity会一个个地被移除,直到*终返回到主屏幕。当返回栈中所有的Activity都被移除掉的时候,对应的任务也就不存在了。

 

任务除了可以被转移到前台之外,当然也是可以被转移到后台的。当用户开启了一个新的任务,或者点击Home键回到主屏幕的时候,之前任务就会被转移到后台了。当任务处于后台状态的时候,返回栈中所有的Activity都会进入停止状态,但这些Activity在栈中的顺序都会原封不动地保留着,如下图所示:

 

%title插图%num

 

这个时候,用户还可以将任意后台的任务切换到前台,这样用户应该就会看到之前离开这个任务时处于*顶端的那个Activity。举个例子来说,当前任务A的栈中有三个Activity,现在用户按下Home键,然后点击桌面上的图标启动了另外一个应用程序。当系统回到桌面的时候,其实任务A就已经进入后台了,然后当另外一个应用程序启动的时候,系统会为这个程序开启一个新的任务(任务B)。当用户使用完这个程序之后,再次按下Home键回到桌面,这个时候任务B也进入了后台。然后用户又重新打开了*次使用的程序,这个时候任务A又会回到前台,A任务栈中的三个Activity仍然会保留着刚才的顺序,*顶端的Activity将重新变为运行状态。之后用户仍然可以通过Home键或者多任务键来切换回任务B,或者启动更多的任务,这就是Android中多任务切换的例子。

 

由于返回栈中的Activity的顺序永远都不会发生改变,所以如果你的应用程序中允许有多个入口都可以启动同一个Activity,那么每次启动的时候就都会创建该Activity的一个新的实例,而不是将下面的Activity的移动到栈顶。这样的话就容易导致一个问题的产生,即同一个Activity有可能会被实例化很多次,如下图所示:

 

%title插图%num

 

但是呢,如果你不希望同一个Activity可以被多次实例化,那当然也是可以的,马上我们就将开始讨论如果实现这一功能,现在我们先把默认的任务和Activity的行为简单概括一下:

  • 当Activity A启动Activity B时,Activity A进入停止状态,但系统仍然会将它的所有相关信息保留,比如滚动的位置,还有文本框输入的内容等。如果用户在Activity B中按下Back键,那么Activity A将会重新回到运行状态。
  • 当用户通过Home键离开一个任务时,该任务会进入后台,并且返回栈中所有的Activity都会进入停止状态。系统会将这些Activity的状态进行保留,这样当用户下一次重新打开这个应用程序时,就可以将后台任务直接提取到前台,并将之前*顶端的Activity进行恢复。
  • 当用户按下Back键时,当前*顶端的Activity会被从返回栈中移除掉,移除掉的Activity将被销毁,然后前面一个Activity将处于栈顶位置并进入活动状态。当一个Activity被销毁了之后,系统不会再为它保留任何的状态信息。
  • 每个Activity都可以被实例化很多次,即使是在不同的任务当中。

 

管理任务

 

Android系统管理任务和返回栈的方式,正如上面所描述的一样,就是把所有启动的Activity都放入到一个相同的任务当中,通过一个“后进先出”的栈来进行管理的。这种方式在*大多数情况下都是没问题的,开发者也无须去关心任务中的Activity到底是怎么样存放在返回栈当中的。但是呢,如果你想打破这种默认的行为,比如说当启动一个新的Activity时,你希望它可以存在于一个独立的任务当中,而不是现有的任务当中。或者说,当启动一个Activity时,如果这个Activity已经存在于返回栈中了,你希望能把这个Activity直接移动到栈顶,而不是再创建一个它的实例。再或者,你希望可以将返回栈中除了*底层的那个Activity之外的其它所有Activity全部清除掉。这些功能甚至更多功能,都是可以通过在manifest文件中设置<activity>元素的属性,或者是在启动Activity时配置Intent的flag来实现的。

 

在<activity>元素中,有以下几个属性是可以使用的:

  • taskAffinity
  • launchMode
  • allowTaskReparenting
  • clearTaskOnLaunch
  • alwaysRetainTaskState
  • finishOnTaskLaunch

 

而在Intent当中,有以下几个flag是比较常用的:

  • FLAG_ACTIVITY_NEW_TASK
  • FLAG_ACTIVITY_CLEAR_TOP
  • FLAG_ACTIVITY_SINGLE_TOP

 

下面我们就将开始讨论,如何通过manifest参数,以及Intent flag来改变Activity在任务中的默认行为。

 

定义启动模式

启动模式允许你去定义如何将一个Activity的实例和当前的任务进行关联,你可以通过以下两种不同的方式来定义启动模式:

 

1.使用manifest文件

当你在manifest文件中声明一个Activity的时候,你可以指定这个Activity在启动的时候该如何与任务进行关联。

 

2.使用Intent flag

当你调用startActivity()方法时,你可以在Intent中加入一个flag,从而指定新启动的Activity该如何与当前任务进行关联。

 

也就是说,如果Activity A启动了Activity B,Activity B可以定义自己该如何与当前任务进行关联,而Activity A也可以要求Activity B该如何与当前任务进行关联。如果Activity B在manifest中已经定义了该如何与任务进行关联,而Activity A同时也在Intent中要求了Activity B该怎么样与当前任务进行关联,那么此时Intent中的定义将覆盖manifest中的定义。

需要注意的是,有些启动模式在manifest中可以指定,但在Intent中是指定不了的。同样,也有些启动模式在Intent中可以指定,但在manifest中是指定不了的,下面我们就来具体讨论一下。

 

使用manifest文件

当在manifest文件中定义Activity的时候,你可以通过<activity>元素的launchMode属性来指定这个Activity应该如何与任务进行关联。launchMode属性一共有以下四种可选参数:

 

“standard”(默认启动模式)

standard是默认的启动模式,即如果不指定launchMode属性,则自动就会使用这种启动模式。这种启动模式表示每次启动该Activity时系统都会为创建一个新的实例,并且总会把它放入到当前的任务当中。声明成这种启动模式的Activity可以被实例化多次,一个任务当中也可以包含多个这种Activity的实例。

 

“singleTop”

这种启动模式表示,如果要启动的这个Activity在当前任务中已经存在了,并且还处于栈顶的位置,那么系统就不会再去创建一个该Activity的实例,而是调用栈顶Activity的onNewIntent()方法。声明成这种启动模式的Activity也可以被实例化多次,一个任务当中也可以包含多个这种Activity的实例。

 

举个例子来讲,一个任务的返回栈中有A、B、C、D四个Activity,其中A在*底端,D在*顶端。这个时候如果我们要求再启动一次D,并且D的启动模式是”standard”,那么系统就会再创建一个D的实例放入到返回栈中,此时栈内元素为:A-B-C-D-D。而如果D的启动模式是”singleTop”的话,由于D已经是在栈顶了,那么系统就不会再创建一个D的实例,而是直接调用D Activity的onNewIntent()方法,此时栈内元素仍然为:A-B-C-D。

 

“singleTask”

这种启动模式表示,系统会创建一个新的任务,并将启动的Activity放入这个新任务的栈底位置。但是,如果现有任务当中已经存在一个该Activity的实例了,那么系统就不会再创建一次它的实例,而是会直接调用它的onNewIntent()方法。声明成这种启动模式的Activity,在同一个任务当中只会存在一个实例。注意这里我们所说的启动Activity,都指的是启动其它应用程序中的Activity,因为”singleTask”模式在默认情况下只有启动其它程序的Activity才会创建一个新的任务,启动自己程序中的Activity还是会使用相同的任务,具体原因会在下面 处理affinity 部分进行解释。

 

“singleInstance”

这种启动模式和”singleTask”有点相似,只不过系统不会向声明成”singleInstance”的Activity所在的任务当中再添加其它Activity。也就是说,这种Activity所在的任务中始终只会有一个Activity,通过这个Activity再打开的其它Activity也会被放入到别的任务当中。

 

再举一个例子,Android系统内置的浏览器程序声明自己浏览网页的Activity始终应该在一个独立的任务当中打开,也就是通过在<activity>元素中设置”singleTask”启动模式来实现的。这意味着,当你的程序准备去打开Android内置浏览器的时候,新打开的Activity并不会放入到你当前的任务中,而是会启动一个新的任务。而如果浏览器程序在后台已经存在一个任务了,则会把这个任务切换到前台。

 

其实不管是Activity在一个新任务当中启动,还是在当前任务中启动,返回键永远都会把我们带回到之前的一个Activity中的。但是有一种情况是比较特殊的,就是如果Activity指定了启动模式是”singleTask”,并且启动的是另外一个应用程序中的Activity,这个时候当发现该Activity正好处于一个后台任务当中的话,就会直接将这整个后台任务一起切换到前台。此时按下返回键会优先将目前*前台的任务(刚刚从后台切换到*前台)进行回退,下图比较形象地展示了这种情况:

 

%title插图%num

 

 

使用Intent flags

除了使用manifest文件之外,你也可以在调用startActivity()方法的时候,为Intent加入一个flag来改变Activity与任务的关联方式,下面我们来一一讲解一下每种flag的作用:

 

FLAG_ACTIVITY_NEW_TASK

 

设置了这个flag,新启动Activity就会被放置到一个新的任务当中(与”singleTask”有点类似,但不完全一样),当然这里讨论的仍然还是启动其它程序中的Activity。这个flag的作用通常是模拟一种Launcher的行为,即列出一推可以启动的东西,但启动的每一个Activity都是在运行在自己独立的任务当中的。

 

FLAG_ACTIVITY_SINGLE_TOP

 

设置了这个flag,如果要启动的Activity在当前任务中已经存在了,并且还处于栈顶的位置,那么就不会再次创建这个Activity的实例,而是直接调用它的onNewIntent()方法。这种flag和在launchMode中指定”singleTop”模式所实现的效果是一样的。

 

FLAG_ACTIVITY_CLEAR_TOP

 

设置了这个flag,如果要启动的Activity在当前任务中已经存在了,就不会再次创建这个Activity的实例,而是会把这个Activity之上的所有Activity全部关闭掉。比如说,一个任务当中有A、B、C、D四个Activity,然后D调用了startActivity()方法来启动B,并将flag指定成FLAG_ACTIVITY_CLEAR_TOP,那么此时C和D就会被关闭掉,现在返回栈中就只剩下A和B了。

 

那么此时Activity B会接收到这个启动它的Intent,你可以决定是让Activity B调用onNewIntent()方法(不会创建新的实例),还是将Activity B销毁掉并重新创建实例。如果Activity B没有在manifest中指定任何启动模式(也就是”standard”模式),并且Intent中也没有加入一个FLAG_ACTIVITY_SINGLE_TOP flag,那么此时Activity B就会销毁掉,然后重新创建实例。而如果Activity B在manifest中指定了任何一种启动模式,或者是在Intent中加入了一个FLAG_ACTIVITY_SINGLE_TOP flag,那么就会调用Activity B的onNewIntent()方法。

 

FLAG_ACTIVITY_CLEAR_TOP和FLAG_ACTIVITY_NEW_TASK结合在一起使用也会有比较好的效果,比如可以将一个后台运行的任务切换到前台,并把目标Activity之上的其它Activity全部关闭掉。这个功能在某些情况下非常有用,比如说从通知栏启动Activity的时候。

 

处理affinity

affinity可以用于指定一个Activity更加愿意依附于哪一个任务,在默认情况下,同一个应用程序中的所有Activity都具有相同的affinity,所以,这些Activity都更加倾向于运行在相同的任务当中。当然了,你也可以去改变每个Activity的affinity值,通过<activity>元素的taskAffinity属性就可以实现了。

 

taskAffinity属性接收一个字符串参数,你可以指定成任意的值(经我测试字符串中至少要包含一个.),但必须不能和应用程序的包名相同,因为系统会使用包名来作为默认的affinity值。

 

affinity主要有以下两种应用场景:

  • 当调用startActivity()方法来启动一个Activity时,默认是将它放入到当前的任务当中。但是,如果在Intent中加入了一个FLAG_ACTIVITY_NEW_TASK flag的话(或者该Activity在manifest文件中声明的启动模式是”singleTask”),系统就会尝试为这个Activity单独创建一个任务。但是规则并不是只有这么简单,系统会去检测要启动的这个Activity的affinity和当前任务的affinity是否相同,如果相同的话就会把它放入到现有任务当中,如果不同则会去创建一个新的任务。而同一个程序中所有Activity的affinity默认都是相同的,这也是前面为什么说,同一个应用程序中即使声明成”singleTask”,也不会为这个Activity再去创建一个新的任务了。
  • 当把Activity的allowTaskReparenting属性设置成true时,Activity就拥有了一个转移所在任务的能力。具体点来说,就是一个Activity现在是处于某个任务当中的,但是它与另外一个任务具有相同的affinity值,那么当另外这个任务切换到前台的时候,该Activity就可以转移到现在的这个任务当中。
    那还是举一个形象点的例子吧,比如有一个天气预报程序,它有一个Activity是专门用于显示天气信息的,这个Activity和该天气预报程序的所有其它Activity具体相同的affinity值,并且还将allowTaskReparenting属性设置成true了。这个时候,你自己的应用程序通过Intent去启动了这个用于显示天气信息的Activity,那么此时这个Activity应该是和你的应用程序是在同一个任务当中的。但是当把天气预报程序切换到前台的时候,这个Activity又会被转移到天气预报程序的任务当中,并显示出来,因为它们拥有相同的affinity值,并且将allowTaskReparenting属性设置成了true。

清空返回栈

 

如何用户将任务切换到后台之后过了很长一段时间,系统会将这个任务中除了*底层的那个Activity之外的其它所有Activity全部清除掉。当用户重新回到这个任务的时候,*底层的那个Activity将得到恢复。这个是系统默认的行为,因为既然过了这么长的一段时间,用户很有可能早就忘记了当时正在做什么,那么重新回到这个任务的时候,基本上应该是要去做点新的事情了。

 

当然,既然说是默认的行为,那就说明我们肯定是有办法来改变的,在<activity>元素中设置以下几种属性就可以改变系统这一默认行为:

 

alwaysRetainTaskState

如果将*底层的那个Activity的这个属性设置为true,那么上面所描述的默认行为就将不会发生,任务中所有的Activity即使过了很长一段时间之后仍然会被继续保留。

 

clearTaskOnLaunch

如果将*底层的那个Activity的这个属性设置为true,那么只要用户离开了当前任务,再次返回的时候就会将*底层Activity之上的所有其它Activity全部清除掉。简单来讲,就是一种和alwaysRetainTaskState完全相反的工作模式,它保证每次返回任务的时候都会是一种初始化状态,即使用户仅仅离开了很短的一段时间。

 

finishOnTaskLaunch

这个属性和clearTaskOnLaunch是比较类似的,不过它不是作用于整个任务上的,而是作用于单个Activity上。如果某个Activity将这个属性设置成true,那么用户一旦离开了当前任务,再次返回时这个Activity就会被清除掉。

Taskaffinity属性使用小结

TaskAffinity属性小结

*近在项目中用到了TaskAffinity属性,发现这个还是挺有意思,可以用来控制activity所属的任务栈。但同时只设置这一个属性又是不能完成功能的,需要与其它属性相配合。

一.通过配置方式来实现TaskAffinity来实现

上边说到要想使TaskAffinity属性生效,要与其它属性相配合。在配置文件中,需要设置activity的启动模式为singleTask或singleInstance才能生效(其实singleInstance本来就会在新Task中)

<activity android:name=".bActivity"
            android:launchMode="singleTask"
            android:taskAffinity="taskName"/>

 

二.通过动态的方式实现TaskAffinity属性

通过上述的配置,可以实现TaskAffinity属性。但是这样每次启动该Activity都会在TaskAffinity指定的栈中启动。有时候可能会希望该activity在特殊情况下才在TaskAffinity指定的栈中启动,大部分时候还是在原有的任务栈中启动,这个时候就需要动态方式来实现TaskAffinity属性。
在配置文件中,只制定TaskAffinity属性,而不制定launchMode的属性为singleTask。

<activity android:name=".bActivity"
            android:taskAffinity="taskName"/>

 

这样通过正常方式启动该Activity时,该Activity就会在原有任务栈中启动(启动该Activity的任务栈中)。若想在taskAffinity属性生效,需要在启动该Activity时设置Flag为FLAG_ACTIVITY_NEW_TASK。

Intent intent = new Intent(aAvtivity.this, bActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);

 

当TaskAffinity生效时,如已经存在相应名称的任务栈,则不会新建栈,而是在该栈的栈顶建立相应activity;如果没有相应名称的任务栈,就会建立对应名称的新的任务栈。

另外说明一点,setFlags和addFlags的区别是:setFlags会直接将原来的Flag直接替换掉;而addFlags是将参数添加上去。

Activity singleInstance启动模式

Android singleInstance启动模式实际开发中使用频率不高,*近解了一个bug,与此相关,bug虽然很轻松的解决了,但由它引发的对Activity的启动模式的思考却有点意思,本篇记录分享下。

引出问题的bug

  • 问题描述:Actvity-A启动了一个新的Actvity-B,Actvity-B嵌入了一个Fragment,Actvity-A和Actvity-B都属于同一个应用。此时连续按两次recent键重回Actvity-B,点击back键直接退出到桌面,而预期的结果应该是返回Actvity-A。整个过程示意图如下(Ubuntu暂时未找到好的录制GIF软件,主要领会过程^_^):
    这里写图片描述
  • 问题分析:首先怀疑点击back键后应用在后台发生了Crash,因fragmewrok层做了异常拦截,此时不会有crash弹框出来,应用直接异常退出了,根据测试提供的log,未发现异常log,此种情况排除。接着排查Fragment及其所依附的Actvity-B的主要生命周期方法,看是否存在逻辑错误,也未发现明显异常。这个时候就怀疑是使用了错误的启动模式导致问题,果然在Manifest清单文件里发现Activity-B配置了android:launchMode=”singleInstance”,Actvity-B就是一个设置应用的界面,没必要设置成singleInstance,于是删除该行问题得到解决。
  • 问题延伸:bug虽然解决了,但却引起了1个疑问:
    为何设置成singleInstance就会出现这种bug

Task 与 Back Stack

要弄清上面的问题,不得不先提下Tasks和Back Stack的概念。
Android系统下,当用户为了完成某一个功能可能需要进行多个Actvitiy间的跳转才能达到目的,这些Activity的跳转序列就被Android抽象成了一个Task。而这一组Actvitiy实例都被放到了同一个栈中,先启动的Activity位于栈底,*后到达的Activity位于栈顶。一般而言Task的启动点都是从home 界面算起,点击Launcher界面的应用icon,如果之前没有启动过,则系统新建一个Task,刚启动应用的主Activiy被压入栈底,栈内的Activity是不会在内部重新排列的,只能按先入后出的顺序呈现,当用户连续按back键,使得栈底的Activity也pop出栈,栈为空了,这时该Task结束。Task是抽象的概念,指带了一组Activity,它们为实现用户的某个操作目的而聚在了一起,可以来自不同应用,Back Stack是实实在在用来存放管理这一组Activity的。Back Stack可以放多个Task,而每一个Task可以包含一个或多个Activity实例。
当用户操作同一个Task内的Activity时,其默认的情况如下:
– Activity-A启动Activity-B后,Activiyt-B被压入Activity-A所处的栈中并位于其上, Activity-A不在可见,但状态被记录下来,比如Activity-A里面EditText内输入的内容,当用户按一次back键返回时Activity-A重新可见,并且其内部的EditText输入内容依然存在,Activiyt-B被pop出栈,状态不保留。
– Activity-A启动Activity-B后,用户按home键返回桌面,系统会将此时的task打包进行状态保存,也就是说位于栈内的每个Activity状态都被保存下来了。用户重新启动激活该task,位于栈顶的Activity 可见并走onResume方法。

还原Bug对应的Task现场

写一个demo还原当时的Task现场。代码很简略,目的就是实验singleInstance启动模式对back键的影响。
ActivityA代码:

package com.azhengye.demolaunchmode;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;

public class ActivityA extends Activity implements OnClickListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_a);
        findViewById(R.id.start_b).setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        Intent intent = new Intent(ActivityA.this, ActivityB.class);
        startActivity(intent);
    }
}
  • 24

ActivityB代码:

package com.azhengye.demolaunchmode;

import android.app.Activity;
import android.os.Bundle;

public class ActivityB extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_b);
    }
}

 

清单代码:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.azhengye.demolaunchmode"
    android:versionCode="1"
    android:versionName="1.0" >
    <uses-sdk
        android:minSdkVersion="16"
        android:targetSdkVersion="22" />
    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity android:name=".ActivityA" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity
            android:name=".ActivityB"
            android:launchMode="singleInstance" />
    </application>
</manifest>
  • 1
  • 2

布局文件省略掉,注意这里在清单文件中对ActivityB的启动模式做了设置android:launchMode="singleInstance"。启动demo程序,从ActivityA跳转到ActivityB,此时停留在ActivityB,使用命令:
adb shell dumpsys activity a
查看当前的任务栈信息。demo中的信息输出如下图,为了方便呈现,只保留了部分输出。
这里写图片描述
然后我们将清单文件中的android:launchMode="singleInstance"去掉,输出如下:
这里写图片描述

对照上面的有下面的结论:
1. 当设置了singleInstance启动模式,ActivityA和ActivityB都被放到了同一个栈中,即Stack #41,但是它们分属不同的Task,ActivityA位于Task344,ActivityB位于Task435。
2. 默认的启动模式,ActivityA和ActivityB都被放置在了同一个Task中。
3. 任务栈其实包含了两个概念–栈和任务(Task),一个栈里可以有多个任务(Task),一个任务(Task)可以有多个Activity。

结论

开篇提出的问题”为何设置成singleInstance就会出现这种bug?”这里我是这样理解的:前台Activity所属的task清空就会返回到桌面,因为singleInstance模式启动的Activity会新开一个独立的task,当按下back键后,该task中的唯一ActivityB 被pop出栈,Task为空,直接返回了桌面。

MySQL数据库从GBK转换到UTF-8*简单解决方案(也适用于其它编码转换)

1、使用mysqldump导出表结构,如:

mysqldump -d -u root -p 数据库名 >/root/struct.sql

2、使用mysqldump以特定编码导出数据(其中utf8为所需编码,可按需修改),如:

mysqldump –default-character-set=utf8 -t -u root -p 数据库名 >/root/data.sql

3、打开表结构转存(/root/struct.sql),将所有CREATE TABLE中的编码替换为所需编码;

4、进入mysql控制台,执行:

source /root/struct.sql

source /root/data.sql

即可完成。

导出表时,如果出现1044错误,添加 –skip-lock-tables 可以解决:

mysqldump -d -u root -p 数据库名 –skip-lock-tables >/root/struct.sql

mysqldump –default-character-set=utf8 -t -u root -p 数据库名 –skip-lock-tables >/root/data.sql

Windows下永久解决数据库乱码 utf8 转 gbk

产生乱码原因

%title插图%num

因为windows终端的默认字符集是gbk编码,而mysql数据库是utf8的编码,所以会产生乱码问题

解决乱码问题(临时修改)

询当前数据库默认编码:

mysql> show variables like ‘character%’;

%title插图%num

修改为gbk编码:

mysql> set names gbk;

%title插图%num

但是这只是临时修改,对新开启的终端无效。

解决乱码问题(永久)

修改MySQL的配置文件:my-default.ini

配置文件位于MySQL的安装目录

例如我的:C:\Program Files\MySQL\MySQL Server 5.7

[mysqld]character-set-server=gbk
%title插图%num %title插图%num

# For advice on how to change settings please see
# http://dev.mysql.com/doc/refman/5.7/en/server-configuration-defaults.html
# *** DO NOT EDIT THIS FILE. It's a template which will be copied to the
# *** default location during install, and will be replaced if you
# *** upgrade to a newer version of MySQL.

[mysqld]character-set-server=gbk

# Remove leading # and set to the amount of RAM for the most important data
# cache in MySQL. Start at 70% of total RAM for dedicated server, else 10%.
# innodb_buffer_pool_size = 128M

# Remove leading # to turn on a very important data integrity option: logging
# changes to the binary log between backups.
# log_bin

# These are commonly set, remove the # and set as required.
# basedir = .....
# datadir = .....
# port = .....
# server_id = .....


# Remove leading # to set options mainly useful for reporting servers.
# The server defaults are faster for transactions and fast SELECTs.
# Adjust sizes as needed, experiment to find the optimal values.
# join_buffer_size = 128M
# sort_buffer_size = 2M
# read_rnd_buffer_size = 2M 

sql_mode=NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES

my-default.ini