0%


PPTP协议在没有配置智能分流的情况下,上网需要来回切换略显麻烦,有没有什么办法可以和ssr客户端和V2ray客户端那样实现分流呢?解决这个问题以后又会发现一个新的问题PPTP用的是google的dns,解析到的ip地址未必是适合的ip导致某些网站访问速度很慢,如果一个账号能让任意设备共享网络,省去折腾各个终端安装和配置那就太好了,最近利用家里的树莓派实现了这个需求,下面是我的折腾记录,有更好的方法欢迎留言交流~


安装PPTP的linux客户端

  1. apt-get安装pptp-linux
    sudo apt-get install pptp-linux

  2. 修改pptp-linux的配置文件

    sudo vi /etc/ppp/peers/pptpconf

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    pty "你的服务端地址 --nolaunchpppd"
    name 账号
    password 密码
    remotename PPTP
    require-mppe-128
    require-mschap-v2
    refuse-eap
    refuse-pap
    refuse-chap
    refuse-mschap
    noauth
    persist
    maxfail 0
    defaultroute
    replacedefaultroute
    usepeerdns
  3. 启动/关闭PPTP
    sudo pon pptpconf
    开启后,如果连接正常,ifconfig可以看到PPTP的连接ppp0

  4. 设置开机启动服务 sudo vi /lib/systemd/system/pptp.service

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    [Unit]
    Description=PPTP Service
    After=network.target

    [Service]
    Type=forking
    ExecStart=/usr/bin/pon pptpconf

    [Install]
    WantedBy=multi-user.target
  5. 刷新并启动服务

    1
    2
    3
    sudo systemctl daemon-reload
    sudo systemctl enable pptp
    sudo systemctl start pptp

    chnroutes分流策略

    根据请求ip分流,某些网站强制转发到ppp0使用PPTP访问,某些网站不使用,这里记录PPTP下的使用方法,其它协议的使用方法可以查看官网:https://github.com/fivesheep/chnroutes

1
2
3
git clone https://github.com/fivesheep/chnroutes.git
cd chnroutes
sudo python chnroutes.py -p linux; sudo chmod a+x ip-pre-up; sudo cp ip-pre-up /etc/ppp; sudo chmod a+x ip-down; sudo cp ip-down /etc/ppp/ip-down.d/

clash开启http/https/socks5代理

  1. 安装clash
  • 下载安装包

    sudo wget https://github.com/Dreamacro/clash/releases/download/v1.6.5/clash-linux-armv7-v1.6.5.gz

  • 解压

    sudo gunzip clash-linux-armv7-v1.6.5.gz

  • 移动到系统目录

    sudo mv clash-linux-armv7-v1.6.5 /usr/local/bin/clash

  • 设置可执行权限

    sudo chmod +x /usr/local/bin/clash

  1. 设置配置文件
    sudo vi ~/.config/clash/config.yaml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    # port of HTTP
    port: 7890

    ## port of SOCKS5
    socks-port: 7891

    # `allow-lan` must be true in your config.yml
    allow-lan: true

    # set log level to stdout (default is info)
    # info / warning / error / debug / silent
    log-level: info

    # A RESTful API for clash
    #使用0.0.0.0可以使用局域网设备访问
    external-controller: 0.0.0.0:8080

    mode: Rule

    sudo clash启动,这样我们就拥有http/https/socks5的代理服务器了,任意设备只要配置树莓派的ip地址和对应协议的端口号即可代理请求,如果有公网ip或内网穿透,有域名+ddns解析服务器那么在外网也可以代理

  2. 设置开机启动服务

    sudo vi /etc/systemd/system/clash.service

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    [Unit]
    Description=Clash Service
    After=network.target

    [Service]
    Restart=on-abort
    LimitNOFILE=1048576
    ExecStart=/usr/local/bin/clash -d /home/pi/.config/clash

    [Install]
    WantedBy=multi-user.target
  3. 转发所有请求 sudo vi /etc/sysctl.conf

  • ipv4的请求,修改

    net.ipv4.ip_forward=1

  • ipv6的请求,修改

    net.ipv6.conf.all.forwarding = 1
    刷新设置

    sudo sysctl -p
    流量强制转发到ppp0
    sudo iptables -t nat -A POSTROUTING -o ppp0 -j MASQUERADE

    dnsmasq + dnsmasq-china-list 本地DNS分流策略

  1. apt-get安装dnsmasq

    sudo apt install dnsmasq

  2. 设置配置文件

    sudo vi /etc/dnsmasq.conf

    1
    2
    3
    no-resolv
    server=8.8.8.8
    server=8.8.4.4
  3. 用dnsmasq-china-list设置白名单,用运营商dns去解析,其它用google的dns解析,否则解析到的ip并不是访问速度快的最适合的ip,导致网站和App的访问速度太慢了,所以太干净的dns解析也不好

  • 运营商分配的DNS地址假设为223.5.5.5,切换到su用户,拉取不需要走PPTP的地址用运营商分配的DNS地址去解析
    1
    curl -s https://raw.githubusercontent.com/felixonmars/dnsmasq-china-list/master/accelerated-domains.china.conf|sed 's/114.114.114.114/223.5.5.5/g' >/etc/dnsmasq.d/accelerated-domains.china.223.5.5.5.conf
  1. 重启dnsmasq

    service dnsmasq restart

  2. 测试分流效果

  • 未使用本地dns解析
    dig google.com @223.5.5.5
  • 使用本地dns解析
    dig google.com @127.0.0.1
  1. 添加自定义域名到白名单
    echo 'server=/你需要的域名/223.5.5.5' >>/etc/dnsmasq.d/accelerated-domains.china.223.5.5.5.conf

    DDNS动态域名解析

    如果家里是公网IP,可以使用DDNS给域名绑定动态ip,no-ip是ddns解析服务,免费赠送域名无需备案,速度还不错,每个月需要点一次邮件续期,官网的ip更新方式在树莓派下更新失败,折腾一段时间后,发现用ddclient可以正常更新,但是注意如果当前状态是pptp开启的情况下,获取到的外网ip可能不是运营商的公网ip,在配置了智能路由的情况下需使用无需PPTP访问的网站获取外网ip
    1
    sudo apt-get install ddclient
    设置配置文件 /etc/ddclient.conf
    1
    2
    3
    4
    5
    6
    protocol=noip
    use=web, web=获取外网ip的网站
    server=dynupdate.no-ip.com
    login=用户名(邮箱)
    password='密码'
    你的域名


随着工作年限的增长,越来越意识到C语言的重要性,Android的底层是C和linux内核,Android中为提高安全性,防止反编译,防止二次打包,提升程序的执行效率都是用C去实现的,作为Android开发者,掌握C语言才能进行NDK开发,提高自己的核心竞争力,拓宽职业道路


指针与地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main(){
int i = 100;
double d = 200;
printf("i的值是:%d\n", i); // 100
printf("d的值是:%lf\n", d); // 200

printf("i的值是:%d\n", *(&i)); // 100 取该地址的值
printf("d的值是:%lf\n", *(&d)); // 200 取该地址的值

int *intP = &i;
double *doubleP = &d;

*intP = 220; // 修改内存地址对应的值为220
printf("i的值是:%d\n", *intP); // 220 取该地址的值
printf("d的值是:%lf\n", *doubleP); // 200 取该地址的值
}

交换两个变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void change(int *a, int *b){
int temp = *a;
*a = *b;
*b = temp;
}
int main(){
int a = 100;
int b = 200;
printf("a的值为:%d\n", a); // 100
printf("b的值为:%d\n", b); // 200

change(&a, &b);

printf("a的值为:%d\n", a); // 200
printf("b的值为:%d\n", b); // 100
return 0;
}

多级指针

  • 指针变量存放的是内存地址,指针变量自己也有地址
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 多级指针
    void test1(){
    int num = 999;
    int * p1 = #
    int ** p2 = &p1;
    int *** p3 = &p2;
    printf("p1的值是:%p, p2的值是:%p, p3的值是:%p\n", p1, p2, p3);// p1存放num内存地址,p2存放p1内存地址,p3存放p2内存地址
    printf("p1的值是:%p, p2的值是:%p, p3的值是:%p\n", &p1, &p2, &p3);// p1,p2,p3自己的内存地址都不一样
    printf("p2的内存地址对应的值是:%d\n",**p2); // 999
    printf("p3的内存地址对应的值是:%d\n",***p3); // 999
    }

    数组指针

  • 指针变量在32位下占4个字节,64位下占8个字节,指针类型决定了sizeof,获取元素时的偏移
  • 数组变量就是一个指针,存放的是第一个元素的内存地址,也等于它自己的内存地址
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // 数组指针
    void test2(){
    int arr[] = {1,2,3,4};
    int *arr_p = arr;
    int i = 0;
    for (i = 0; i < sizeof arr / sizeof(int); ++i){
    printf("下标为%d的值是%d\n", i,*(arr_p + i));
    printf("下标%d的内存地址是%p\n",i, arr_p + i); // 地址间隔4个字节
    }
    // 同一个地址
    printf("arr = %p\n", arr);
    printf("&arr = %p\n", &arr);
    printf("&arr[0] = %p\n", &arr[0]);

    // 取数组第二个值
    arr_p ++;
    printf("%d\n", *arr_p);

    // 超出范围,野指针
    arr_p += 200;
    printf("%d\n", *arr_p);
    }

    内存静态开辟和动态开辟

  • 动态开辟需要手动释放,手动释放后如果不赋值为NULL,就是悬空指针
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    void dynamicAction(int num){
    // 堆区动态开辟1M内存
    int *arr = malloc(num * sizeof(int));
    printf("dynamicAction函数,arr自己的内存地址:%p,堆区开辟的内存地址:%p\n",&arr,arr);
    // dynamicAction函数,arr自己的内存地址:0x7ffee47e1480,堆区开辟的内存地址:0x7fdb5dd00000
    // 释放堆区开辟的内存
    if(arr){
    free(arr);
    arr = NULL; // 如果不赋值为NULL,就是悬空指针
    printf("dynamicAction函数2,arr自己的内存地址:%p,堆区开辟的内存地址:%p\n",&arr,arr);
    // dynamicAction函数2,arr自己的内存地址:0x7ffee47e1480,堆区开辟的内存地址:0x0
    }
    }
  • 静态开辟,使用栈内存,自动释放
    1
    2
    3
    4
    void staticAction(){
    int arr[6];
    printf("staticAction函数,arr自己的内存地址:%p,堆区开辟的内存地址:%p\n",&arr,arr);
    }

    realloc重新开辟内存

  • 扩容内存时,地址不一定连续,物理内存被其它占用会返回新的地址,所以内存重新开辟时需要传入指针和总大小进行拷贝
    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
    int main(){
    int num;
    printf("请输入新的个数:");
    // 获取用户输入的值
    scanf("%d", &num);

    int *arr = (int *)malloc(arr, sizeof(int) * num);
    for (int i = 0; i < num; ++i){
    arr[i] = i + 10001;
    }
    printf("开辟的内存地址:%p\n", arr);
    // 开辟新的内存空间
    int newNum;
    printf("请输入新增加的个数");
    scanf("%d", &newNum);
    int *newArr = (int *)realloc(arr, sizeof(int) * (num + newNum));
    if (newArr){
    int j = num;
    for (;j < (num + newNum); ++j){
    arr[j] = j + 10001;
    }
    printf("新开辟的内存地址:%p\n", newArr);
    }
    // 释放内存操作
    if (newArr){
    free(newArr);
    newArr = NULL;
    arr = NULL;
    }
    else{
    free(arr);
    arr = NULL;
    }
    return 0;
    }

    函数指针

  • 使用函数指针实现回调,相当于Java的接口
  • 函数指针和它自己的内存地址相同
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    void add(int num1, int num2){
    printf("num1 + num2 = %d\n", (num1 + num2));
    }

    void mins(int num1, int num2){
    printf("num1 - num2 = %d\n", (num1 - num2));
    }

    // 传递函数指针
    void operate(void(*method) (int,int),int num1, int num2){
    method(num1,num2);
    }

    void test4(){
    operate(add, 10, 20);
    void(*method)(int,int) = mins;
    operate(method, 100, 20);
    // 函数指针和它自己的内存地址相同
    printf("%p, %p\n", add, &add);
    }

    生成随机数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #include <stdlib.h>
    #include <time.h>
    void test5(){
    srand((unsigned) time(NULL));
    int i;
    for (i = 0; i < 10; ++i) {
    printf("随机数%d\n", rand() % 100);
    }
    }

    复制字符串

    1
    2
    3
    4
    5
    6
    7
    #include <string.h>
    void test6(){
    char string[10];
    char* str1 = "abcdefghi";
    strcpy(string, str1);
    printf("%s\n", string);
    }

    字符串获取长度

  • 也可以直接使用strLen
    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
    void getLen(int *resultLen, char *str){
    int count = 0;
    while(*str){
    str ++;
    count ++;
    }
    *resultLen = count;
    }
    /**
    * C/C++会把数组优化成指针提高效率,导致长度计算错误
    int getLen(int arr[]){
    int len = sizeof(arr) / sizeof(int);
    return len;
    }
    */
    void test7{
    char str1[] = {'H','e','l','l','o','\0'}; // 遇到\0停下来
    str1[2] = 'z'; // 栈空间,允许修改
    printf("第一种方式:%s\n", str1);
    int count;
    getLen(&count, str1);
    printf("长度为:%d\n", count);
    char *str2 = "Hello"; // 结尾隐式增加\0
    // str2[2] = 'z'; 会报错,不允许修改全局区的字符串
    printf("第二种方式:%s\n", str2);
    }

    字符串转换

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    void convertInt(){
    char *num = "1";
    int res = atoi(num);
    printf("转换结果:%d\n", res);
    }

    void convertDouble(){
    double resD = atof(num);
    printf("转换结果:%lf\n", resD);
    }

    字符串比较

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    void test8(){
    char *str1 = "Hello";
    char *str2 = "hello";
    // int res = strcmp(str1, str2); // 区分大小写
    int res = strcmpi(str1, str2); // 不区分大小写
    if (!res){
    printf("相等");
    }else{
    printf("不相等")
    }
    }

    字符串查找子串

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    void test9(){
    char *text = "hello world";
    char *subtext = "w";
    // 从搜索到子串的下标位置截取到末尾
    char *pop = strstr(text, subtext);
    if (pop){
    printf("查找到了,pop的值是%s\n",pop);
    }
    else{
    printf("没有查找到,subtext的值是%s\n",subtext);
    }
    int index = pop - text;
    printf("%s第一次出现的位置是:%d\n",subtext, index);
    }

    字符串拼接

    1
    2
    3
    4
    5
    6
    7
    8
    void test10(){
    char destination[25];
    char *blank = "--到--", *CPP = "C++", *Java = "Java";
    strcpy(destination, CPP); // 先拷贝到数组
    strcat(destination, blank); // 拼接blank
    strcat(destination, Java); // 拼接Java
    printf("拼接后的结果%s\n", destination);
    }

    字符串截取

    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
    void substring1(char *res, char *str, int start, int end){
    char *temp = str;
    int index = 0; // 当前截取的位置
    while(*temp){
    if (index > start && index < end){
    *res = *temp;
    res ++;
    }
    temp ++;
    index ++;
    }
    }
    void substring2(char **res, char *str, int start, int end){
    char *temp = str;
    char resArr[end - start]; // 方案1. 临时变量在栈中分配内存,方法结束会被释放
    // char *resArr = malloc(end - start); // 方案2. 堆中开辟内存
    int index = 0;
    for (int i = start, i < end; ++i){
    resArr[index] = *(temp + i);
    index ++;
    }
    // 二级指针的一级指针等于test11的res一级指针
    // *res = resArr; // 方案2. 结果指向堆中的数组,方法结束后也不能释放所以不推荐
    strcpy(*res, resArr); // 方案1. 拷贝到数组
    printf("截取后的结果:%s\n", resArr);
    }
    void substring3(char *res, char *str, int start, int end){
    for (int i = start; i < end; ++i){
    *(result++) = *(str + i);
    }
    }
    void substring4(char *res, char *str, int start, int end){
    strncpy(result, str + start, end - start);
    }
    void test11(){
    char *str = "hello";
    char *res;
    substring1(res, str, 1, 4);
    // substring2(&res, str, 1, 4);
    printf("截取后的内容是:%s",res);
    }

    大小写转换

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    void lower(char *dest, char *text){
    int *temp = text;
    while(*temp){
    *dest = tolower(*temp);
    temp ++;
    dest ++;
    }
    *dest = '\0'; // 结尾增加\0避免打出系统值
    printf("name:%s\n", text);
    }
    void test12(){
    char *text = "hello";
    char dest[20];
    lower(dest, text);
    printf("小写转换后的结构是:%s\n", dest);
    }

    结构体

    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
    struct Dog{
    char name[10];
    int age;
    char sex;
    }
    struct Person{
    char *name;
    int age;
    char sex;
    } person1 = {"小明", 21, 'M'}
    struct Study{
    char *studyContent;
    }
    struct Student{
    char name[10];
    int age;
    char sex;
    struct Study study; // 引用外部结构体,声明结构体对象
    // 定义结构体并声明结构体对象
    struct Wan {
    char *wanContent;
    }wan;
    }
    // 匿名结构体定义别名
    typedef struct {
    char *name;
    int age;
    char sex;
    } Cat;
    int main(){
    struct Dog dog;
    strcpy(dog.name, "旺财");
    dog.age = 2;
    dog.sex = 'M';
    printf("name:%s, age:%d, sex:%c \n", dog.name, dog.age, dog.sex);

    struct Student student = {"小红", 18, 'F', {"学习C"}, {"王者荣耀"}};
    printf("name:%s, age:%d, sex:%c, study:%s, wan:%s \n", student.name, student.age, student.sex, student.study.studyContent, student.wan.wanContent);

    struct Dog dog2 = {"旺财2"4, 'M'};
    struct Dog *dogp = &dog2;
    dogp -> age = 3;
    dogp -> sex = 'F';
    strcpy(dogp->name, "旺财3");
    printf("name:%s, age:%d, sex:%c \n", dogp->name, dogp->age, dogp->sex);
    free(dogp);
    dogp = NULL;

    struct Dog dogArr[10] = {
    {"旺财4"4, 'M'},
    {"旺财5"5, 'M'},
    {"旺财6"6, 'M'},
    {},
    {},
    {},
    {},
    {},
    {},
    {}
    };
    struct Dog dog9 = {"旺财9"9, 'M'};
    // dogArr[9] = dog9;
    *(dogArr + 9) = dog9;
    printf("name:%s, age:%d, sex:%c \n", dog9.name, dog9.age, dog9.sex
    // 动态申请内存
    struct Dog *dogArr2 = malloc(sizeof(struct Dog) * 10);
    strcpy(dogArr2->name, "大黄1");
    dogArr2 -> age = 2;
    dogArr2 -> sex = 'M';
    printf("name:%s, age:%d, sex:%c \n", dogArr2->name, dogArr2->age, dogArr2->sex);
    // 指针移到第8个元素
    dogArr2 += 7;
    strcpy(dogArr2 -> name, "大黄8");
    dogArr2 -> age = 3;
    dogArr2 -> sex = 'M';
    printf("name:%s, age:%d, sex:%c \n", dogArr2->name, dogArr2->age, dogArr2->sex);
    free(dogArr2);
    dogArr2 = NULL;

    Cat *cat = malloc(sizeof(Cat)); // 结构体指针
    return 0;
    }

    枚举

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    enum CommentType{
    TEXT = 10,
    TEXT_IMAGE,
    IMAGE
    };
    typedef enum {
    TEXT1 = 10,
    TEXT_IMAGE1,
    IMAGE1
    } CommentType1;
    int main(){
    enum CommentType commentType = TEXT;
    printf("%d\n", commentType);

    CommentType1 commentType1 = TEXT1;
    printf("%d\n", commentType1);
    return 0;
    }

    文件读写

    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
    void readFile(){
    char *fileName = "/Users/JessieKate/CLionProjects/TestProject/test.txt";
    FILE *file = fopen(fileName,"r"); // 此文件必须存在
    if (!file){
    printf("文件打开失败,请查看路径");
    exit(0);
    }

    char buffer[10]; // 创建buffer读取
    while(fgets(buffer, 10, file)){
    printf("%s", buffer);
    }
    // 关闭文件
    fclose(file);
    }

    void writeFile(){
    char *fileName = "/Users/JessieKate/CLionProjects/TestProject/test1.txt";
    FILE *file = fopen(fileName, "w"); // 此文件可以不存在
    if (!file){
    printf("文件打开失败,请查看路径");
    exit(0);
    }
    fputs("这是我写入的测试内容", file);
    fclose(file);
    }

    文件复制

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    void copyFile(){
    char *fileName = "/Users/JessieKate/CLionProjects/TestProject/test.txt";
    char *fileNameCopy = "/Users/JessieKate/CLionProjects/TestProject/testCopy.txt";
    FILE *file = fopen(fileName, "rb");
    FILE *fileCopy = fopen(fileNameCopy, "wb");
    if (!file || !fileCopy){
    printf("文件打开失败,请查看路径");
    exit(0);
    }
    int buffer[514]; // 缓存数组
    int len; // 每次读取的长度

    // fread 读入缓存buffer, 偏移数量, 读取字节数写入缓存
    while((len = fread(buffer, 1, 514, file)) > 0){
    fwrite(buffer, len, 1, fileCopy);
    }

    fclose(file);
    fclose(fileCopy);
    }

    文件大小

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    void getSize(){
    char *fileName = "/Users/JessieKate/CLionProjects/TestProject/test.txt";
    FILE *file = fopen(fileName,"r");
    if (!file){
    printf("文件打开失败,请查看路径");
    exit(0);
    }
    fseek(file, 0, SEEK_END); //从0开始挪动到文件结束
    long fileSize = ftell(file); //读取file的信息
    printf("%s文件的字节大小是:%ld",fileName, fileSize);
    }

    文件加密解密

    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
    void encrypt(){
    char * fileName = "/Users/JessieKate/CLionProjects/TestProject/Image.jpg";
    char * fileNameEncode = "/Users/JessieKate/CLionProjects/TestProject/Image_encode.jpg";
    FILE * file = fopen(fileName, "rb");
    FILE * fileEncode = fopen(fileNameEncode,"wb");

    if (!file || !fileEncode){
    printf("文件打开失败,请查看路径");
    exit(0);
    }
    char *password = "123456";
    // 加密,破坏文件,解密,还原文件
    int c;
    int index = 0;
    int pass_len = strlen(password); // 获取密码的长度

    // fgetc 返回EOF = end of file
    while((c = fgetc(file)) != EOF){
    // 循环获取密码的每个字符,1,2,3,4,5,6,1,2,3...
    char item = password[index % pass_len];
    printf("item:%c%\n",item);
    fputc( c ^ item, fileEncode);
    index++;
    }
    fclose(file);
    fclose(fileEncode);
    }

    void decrypt(){
    char * fileNameEncode = "/Users/JessieKate/CLionProjects/TestProject/Image_encode.jpg";
    char * fileNameDecode = "/Users/JessieKate/CLionProjects/TestProject/Image_decode.jpg";
    FILE * file = fopen(fileNameEncode, "rb");
    FILE * fileDecode = fopen(fileNameDecode,"wb");
    if (!file || !fileDecode){
    printf("文件打开失败,请查看路径");
    exit(0);
    }
    char *password = "123456";
    int c;
    int index = 0;
    int pass_len = strlen(password);

    // fgetc 返回EOF = end of file
    while((c = fgetc(file)) != EOF){
    char item = password[index % pass_len];
    fputc( c ^ item, fileDecode);
    index++;
    }
    fclose(file);
    fclose(fileDecode);
    }


最近入了一台pixel3当测试机和备用机,完美刷入最新的Android系统11,据说12这样刷入也是可以的,magisk刷root权限和电信模块,确实是最佳测试机,完整的google套件,原汁原味的Android系统,喜欢尝鲜,喜欢原生系统且有梯子的小伙伴可以考虑入手一台,它不会让你失望的。下面记录一下我的刷机流程,第一次刷pixel还是踩了不少的坑


Pixel3不支持中国电信,需要手机root后刷入电信模块才可以使用电信卡(包括打电话和流量上网),Root方案选择了magisk,兼容性好

刷入完整的官方Rom包

  1. 根据自己的手机型号和刷入的系统版本,下载对应的镜像文件,google官方Rom包地址:https://developers.google.com/android/images
  2. 打开开发者模式,狂戳Setting -> Build number,打开USB调试,Setting -> System -> Developer Options打开Usb debugging,勾选oem unlocking
  3. 配置adb命令,如果不是Android开发者可下载android-platform-tools,这里就不赘述了
  4. 解压下载的blueline-rq2a.210505.002-factory-687d8468.zip,有个flash-all.sh的脚本
  5. 手机进入fastboot模式,adb reboot bootloader,再执行这个脚本./flash-all.sh

安装Magisk并Root

github下载最新Magisk并安装到手机,https://github.com/topjohnwu/Magisk/releases

提取boot.img

刚下载的blueline-rq2a.210505.002-factory-687d8468.zip文件解压后内部仍有一个压缩文件image-blueline-rq2a.210505.002.zip,继续解压,解压后的文件夹中会得到一个boot.img,把它push到手机

1
adb push ./blueline-rq2a.210505.002/image-blueline-rq2a.210505.002/boot.img sdcard/Download

Magisk修改boot.img

打开magisk选择第一个install,选择select and patch a file选择刚才push的文件boot.img,执行后会在该目录下生成一个magisk_patched.img,把这个文件pull到PC
adb pull /sdcard/Download/magisk_patched-23000_J2eHI.img

magisk_patched.img刷入手机

  1. 手机进入fastboot模式,adb reboot bootloader
  2. fastboot解锁,fastboot flashing unlock
  3. 刷入magisk_patched.img,fastboot flash boot magisk_patched.img
  4. 重启手机,fastboot reboot

    验证是否root

    执行命令adb shell
    执行命令su
    查看pixel是否有确认root授权的提示

刷入电信模块

  1. github下载china_telecom_supporter,https://github.com/apporc/china_telecom_supporter
  2. 把压缩包push到手机上
    adb push ./china_telecom_supporter.zip /sdcard/Download
  3. adb命令解压
    1
    unzip -d /sdcard/Download/china_telecom_supporter /sdcard/Download/china_telecom_supporter.zip
  4. 移动解压后的文件夹到系统目录/data/adb/modules/
    1
    mv /sdcard/Download/china_telecom_supporter-master/ /data/adb/modules/china_telecom_supporter/
  5. 删除 fdr_check 文件
    rm /data/vendor/modem_fdr/fdr_check
  6. 重启,如果刷入成功此时进入系统可以看到电信的信号了,能正常打电话和流量上网~


又是一段时间没有更新博客了,这段时间对以前的学习做了一个总结,还是很值得的,希望接下来的时间里,能继续保持学习的热情,稳定输出学习和工作的总结到博客,毕竟一个好习惯值得坚持下去~也希望和各位大佬一起学习和交流,接下来记录用kotlin手写PhotoView的过程


自定义View

  1. 居中位置绘制bitmap
  2. 计算小图,大图缩放比
  3. fling,双击,滑动时调整画布偏移值绘制
  4. 双击,缩放手势调整画布当前缩放比例绘制
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
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
class PhotoView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
lateinit var bitmap: Bitmap
lateinit var paint: Paint
/**
* 原始位置
*/
var originalOffsetX = 0f
var originalOffsetY = 0f
/**
* 滑动偏移
*/
var offsetX = 0f
var offsetY = 0f
var smallScale = 0f // 小图缩放比
var bigScale = 0f // 大图缩放比
var currentScale = 0f // 当前缩放比
get() {
Log.i(TAG, "get: $field")
return field
}
set(value) {
field = value
invalidate()
Log.i(TAG, "set: $value")
}
var isLarge = false // 是否放大状态
lateinit var gestureDetector: GestureDetector // 手势回调
lateinit var scaleGestureDetector: ScaleGestureDetector // 缩放手势回调
val overScroller by lazy { OverScroller(context) } // 滚动工具类,边界回弹
// 缩放动画
val scaleAnimation: ObjectAnimator by lazy {
val temp = ObjectAnimator.ofFloat(this, "currentScale", 0f)
temp.setFloatValues(smallScale, bigScale)
temp
}

companion object {
const val SCALE_FACTOR = 1.5f
}

fun initBitmap(bitmap: Bitmap) {
if (width == 0 || height == 0) {
throw IllegalStateException("call after View is rendered")
}
this.bitmap = bitmap
paint = Paint(Paint.ANTI_ALIAS_FLAG)
gestureDetector = GestureDetector(context, PhotoGesture())
scaleGestureDetector = ScaleGestureDetector(context, PhotoScaleGestureListener())
// bitmap居中渲染
originalOffsetX = (width - bitmap.width) / 2f
originalOffsetY = (height - bitmap.height) / 2f
// 横图计算缩放比
if (bitmap.width.toFloat() / bitmap.height > width.toFloat() / height) {
smallScale = width.toFloat() / bitmap.width.toFloat()
bigScale = (height.toFloat() / bitmap.height.toFloat()) * SCALE_FACTOR
}
// 竖图计算缩放比
else {
smallScale = height.toFloat() / bitmap.height.toFloat()
bigScale = (width.toFloat() / bitmap.width.toFloat()) * SCALE_FACTOR
}
// 当前缩放为最小缩放
currentScale = smallScale
invalidate()
}

// 边界修正
private fun fixOffset(){
offsetX = min(offsetX,(bitmap.width * bigScale - width)/ 2)
offsetX = max(offsetX, -(bitmap.width * bigScale - width) / 2)
offsetY = min(offsetY, (bitmap.height * bigScale - height) / 2)
offsetY = max(offsetY, -(bitmap.height * bigScale - height) / 2)
}

override fun onDraw(canvas: Canvas?) {
if (this::bitmap.isInitialized) {
// 当前缩放占比
val scaleFaction = (currentScale - smallScale) / (bigScale - smallScale)
// 画布平移
canvas?.translate(offsetX * scaleFaction, offsetY * scaleFaction)
// 画布缩放
canvas?.scale(currentScale, currentScale, (width / 2).toFloat(), (height / 2).toFloat())
canvas?.drawBitmap(bitmap, originalOffsetX, originalOffsetY, paint)
}
}

override fun onTouchEvent(event: MotionEvent?): Boolean {
// 默认缩放回调处理
var res = scaleGestureDetector.onTouchEvent(event)
// 不是缩放操作时,由手势回调处理
if (!scaleGestureDetector.isInProgress) {
res = gestureDetector.onTouchEvent(event)
}
return res
}

// 滑动处理
val runnable = object :Runnable{
override fun run() {
if (overScroller.computeScrollOffset()){
offsetX = overScroller.currX.toFloat()
offsetY = overScroller.currY.toFloat()
invalidate()
postOnAnimation(this)
}
}
}

inner class PhotoGesture : GestureDetector.SimpleOnGestureListener() {

override fun onDown(e: MotionEvent?): Boolean {
return true // 拦截DOWN事件
}

override fun onFling(e1: MotionEvent?, e2: MotionEvent?, velocityX: Float, velocityY: Float): Boolean {
// 当前放大状态,调overScroller的fling
if (isLarge) {
overScroller.fling(offsetX.toInt(), offsetY.toInt(), velocityX.toInt(), velocityY.toInt(),
(-(bitmap.width * bigScale - width) / 2).toInt(),
((bitmap.width * bigScale - width) / 2).toInt(),
(-(bitmap.height * bigScale - height) / 2).toInt(),
((bitmap.height * bigScale - height) / 2).toInt(),
300,300)
// 滑动刷新偏移值处理
postOnAnimation(runnable)
}
return super.onFling(e1, e2, velocityX, velocityY)
}

override fun onDoubleTap(e: MotionEvent?): Boolean {
isLarge = !isLarge
// 当前是放大状态
if (isLarge) {
// 刷新绘制位置
e?.let {
offsetX = (it.x - width / 2f) - (it.x - width / 2f) * bigScale / smallScale
offsetY = (it.y - height / 2f) - (it.y - height / 2f) * bigScale / smallScale
}
// 边界修正
fixOffset()
// 缩放动画开始
scaleAnimation.start()
} else {
scaleAnimation.reverse()
}

return super.onDoubleTap(e)
}

override fun onScroll(e1: MotionEvent?, e2: MotionEvent?, distanceX: Float, distanceY: Float): Boolean {
// 当前是放大状态
if (isLarge) {
// 刷新绘制位置
offsetX -= distanceX
offsetY -= distanceY
// 边界修正
fixOffset()
invalidate()
}
return super.onScroll(e1, e2, distanceX, distanceY)
}

}

inner class PhotoScaleGestureListener:ScaleGestureDetector.OnScaleGestureListener{
var initScale = 0f
override fun onScaleBegin(detector: ScaleGestureDetector?): Boolean {
// 初始缩放比例
initScale = currentScale
return true
}

override fun onScaleEnd(detector: ScaleGestureDetector?) {

}

override fun onScale(detector: ScaleGestureDetector?): Boolean {
// 如果当前缩放比 大于 最小缩放比 并且 非放大状态 或 当前缩放比 等于 最小缩放比 并且非放大状态
if ((currentScale > smallScale && !isLarge) || (currentScale == smallScale && !isLarge)){
// 设置为放大状态
isLarge = !isLarge
}
// 如果初始缩放比 * 缩放系数 小于 最小缩放比 赋值为最小缩放比 否则为初始缩放比 * 缩放系数
detector?.let {
currentScale = if (initScale * it.scaleFactor < smallScale){
smallScale
} else{
initScale * it.scaleFactor
}
}
invalidate()

return false
}
}
}

在布局中使用

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.tws.moments.views.PhotoView
android:id="@+id/photo"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

设置bitmap

1
2
3
4
5
try {
photo.initBitmap(bitmap)
} catch (e: Exception) {
Log.i(TAG, "subscribe: ${e.stackTrace}")
}


动态换肤算是APP开发中常见的技术,最近公司的项目也是正好用到,需求是从后台下载皮肤包到本地,用户可以选择自己喜欢的皮肤包进行页面的动态换肤,增强用户体验和应用的趣味性。大致整理了一下实现思路,在此做个记录,如果有更好的方案,欢迎交流~

  1. 读取apk的内容
    异步加载本地目录的皮肤资源apk,通过反射调用AssetManager添加资源路径的方法将apk的资源加载进去,得到全新的AssetManager并注册,加载资源时使用新AssetManager的资源,新resource赋给全局的resource,切回默认app皮肤时,再赋值回默认的resource。
  2. 收集换肤的View相关数据,干预xml解析拦截需要换肤的view,Factory2中生产view时记录需要换肤的属性
  3. 观察者模式绑定被观察者-资源管理器和观察者-自定义的LayoutInflater.Factory2,BaseActivity实现统一换肤接口ISkinUpdate的逻辑调资源管理器加载皮肤APK。
  4. 执行换肤逻辑
    设置自定义的LayoutInflater.Factory2,拦截xml中原本解析的view返回一个新的view,判断当前view是否需要换肤,需要则直接设置相应属性,不需要则执行原本逻辑。

皮肤管理器

在Application中初始化皮肤管理器,继承Observable类,是一个可被观察的对象,需要通知观察者对象,主要负责加载本地皮肤包apk并更新皮肤resource对象到资源管理器,通知各个需要换肤的View更新UI,并记录当前使用的皮肤包路径

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
public class SkinManager extends Observable {

private volatile static SkinManager instance;
/**
* Activity生命周期回调
*/
private ApplicationActivityLifecycle skinActivityLifecycle;
private Application mContext;

/**
* 初始化 必须在Application中先进行初始化
*
* @param application
*/
public static void init(Application application) {
if (instance == null) {
synchronized (SkinManager.class) {
if (instance == null) {
instance = new SkinManager(application);
}
}
}
}

private SkinManager(Application application) {
mContext = application;
//初始化sp,记录当前使用的皮肤
SkinPreference.init(application);
//资源管理类,从皮肤resource对象中加载资源
SkinResources.init(application);
//注册Activity生命周期,并设置被观察者
skinActivityLifecycle = new ApplicationActivityLifecycle(this);
application.registerActivityLifecycleCallbacks(skinActivityLifecycle);
//加载上次使用保存的皮肤
loadSkin(SkinPreference.getInstance().getSkin());
}

public static SkinManager getInstance() {
return instance;
}


/**
* 加载皮肤并更新resource到皮肤资源管理器
*
* @param skinPath 皮肤路径 如果为空则使用默认皮肤
*/
public void loadSkin(String skinPath) {
if (TextUtils.isEmpty(skinPath)) {
//还原默认皮肤
SkinPreference.getInstance().reset();
SkinResources.getInstance().reset();
} else {
try {
//宿主app的resources;
Resources appResource = mContext.getResources();
//反射创建AssetManager与Resource
AssetManager assetManager = AssetManager.class.newInstance();
//资源路径设置,压缩包的目录
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath",
String.class);
addAssetPath.invoke(assetManager, skinPath);

//根据设备显示器信息与配置(横竖屏、语言等)创建新的皮肤Resources
Resources skinResource = new Resources(assetManager, appResource.getDisplayMetrics
(), appResource.getConfiguration());

//获取本地皮肤Apk(皮肤包) 包名,并应用皮肤
PackageManager mPm = mContext.getPackageManager();
PackageInfo info = mPm.getPackageArchiveInfo(skinPath, PackageManager
.GET_ACTIVITIES);
String packageName = info.packageName;
SkinResources.getInstance().applySkin(skinResource, packageName);

//记录当前使用的皮肤路径
SkinPreference.getInstance().setSkin(skinPath);


} catch (Exception e) {
e.printStackTrace();
}
}
//通知采集的View 更新皮肤
//被观察者改变,通知所有观察者
setChanged();
notifyObservers(null);
}

}
  • SP工具类,记录皮肤包路径
    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
    public class SkinPreference {
    private static final String SKIN_SHARED = "skins";
    private static final String KEY_SKIN_PATH = "skin-path";
    private volatile static SkinPreference instance;
    private final SharedPreferences mPref;

    public static void init(Context context) {
    if (instance == null) {
    synchronized (SkinPreference.class) {
    if (instance == null) {
    instance = new SkinPreference(context.getApplicationContext());
    }
    }
    }
    }

    public static SkinPreference getInstance() {
    return instance;
    }

    private SkinPreference(Context context) {
    mPref = context.getSharedPreferences(SKIN_SHARED, Context.MODE_PRIVATE);
    }

    public void setSkin(String skinPath) {
    mPref.edit().putString(KEY_SKIN_PATH, skinPath).apply();
    }

    public void reset() {
    mPref.edit().remove(KEY_SKIN_PATH).apply();
    }

    public String getSkin() {
    return mPref.getString(KEY_SKIN_PATH, null);
    }

    }

    监听Activity生命周期

    传入被观察者-皮肤管理器对象,缓存activity到对应自定义LayoutInflaterFactory对象的映射
  1. Activity被创建时,反射设置mFactorySet属性为false,设置自定义的LayoutInflaterFactory,并记录与当前activity的映射关系到map,添加自定义的LayoutInflaterFactory对象为皮肤管理器对象的观察者对象,接收皮肤管理器加载完成的通知
  2. Activity被销毁时,获取到对应的自定义LayoutInflaterFactory对象,移除对被观察者-皮肤管理器对象的监听
    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
    public class ApplicationActivityLifecycle implements Application.ActivityLifecycleCallbacks {

    private Observable mObserable; // 被观察者,皮肤管理器对象
    private ArrayMap<Activity, SkinLayoutInflaterFactory> mLayoutInflaterFactories = new
    ArrayMap<>(); // 记录activity到对应自定义LayoutInflaterFactory对象的映射

    public ApplicationActivityLifecycle(Observable observable) {
    mObserable = observable;
    }

    @Override
    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
    /**
    * 更新状态栏
    */
    SkinThemeUtils.updateStatusBarColor(activity);

    /**
    * 更新布局视图
    */
    //获得Activity的布局加载器
    LayoutInflater layoutInflater = activity.getLayoutInflater();

    try {
    //Android 布局加载器 使用 mFactorySet 标记是否设置过Factory
    //反射设置 mFactorySet 标签为false
    Field field = LayoutInflater.class.getDeclaredField("mFactorySet");
    field.setAccessible(true);
    field.setBoolean(layoutInflater, false);
    } catch (Exception e) {
    e.printStackTrace();
    }

    //设置自定义的LayoutInflaterFactory,并记录与当前activity的映射关系到map
    SkinLayoutInflaterFactory skinLayoutInflaterFactory = new SkinLayoutInflaterFactory
    (activity);
    LayoutInflaterCompat.setFactory2(layoutInflater, skinLayoutInflaterFactory);
    mLayoutInflaterFactories.put(activity, skinLayoutInflaterFactory);

    mObserable.addObserver(skinLayoutInflaterFactory);
    }

    @Override
    public void onActivityStarted(Activity activity) {

    }

    @Override
    public void onActivityResumed(Activity activity) {

    }

    @Override
    public void onActivityPaused(Activity activity) {

    }

    @Override
    public void onActivityStopped(Activity activity) {

    }

    @Override
    public void onActivitySaveInstanceState(Activity activity, Bundle outState) {

    }

    @Override
    public void onActivityDestroyed(Activity activity) {
    SkinLayoutInflaterFactory observer = mLayoutInflaterFactories.remove(activity);
    SkinManager.getInstance().deleteObserver(observer);
    }
    }

    皮肤资源管理器

    记录原始宿主App的resource对象和新的皮肤resource对象,从皮肤resource对象中获取对应的属性
    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
    public class SkinResources {

    private String mSkinPkgName;
    private boolean isDefaultSkin = true;

    // app原始的resource
    private Resources mAppResources;
    // 皮肤包的resource
    private Resources mSkinResources;

    private SkinResources(Context context) {
    mAppResources = context.getResources();
    }

    private volatile static SkinResources instance;
    public static void init(Context context) {
    if (instance == null) {
    synchronized (SkinResources.class) {
    if (instance == null) {
    instance = new SkinResources(context);
    }
    }
    }
    }

    public static SkinResources getInstance() {
    return instance;
    }

    public void reset() {
    mSkinResources = null;
    mSkinPkgName = "";
    isDefaultSkin = true;
    }

    public void applySkin(Resources resources, String pkgName) {
    mSkinResources = resources;
    mSkinPkgName = pkgName;
    //是否使用默认皮肤
    isDefaultSkin = TextUtils.isEmpty(pkgName) || resources == null;
    }

    /**
    * 1.通过原始app中的resId(R.color.XX)获取到自己的 名字
    * 2.根据名字和类型获取皮肤包中的ID
    */
    public int getIdentifier(int resId){
    if(isDefaultSkin){
    return resId;
    }
    String resName=mAppResources.getResourceEntryName(resId);
    String resType=mAppResources.getResourceTypeName(resId);
    int skinId=mSkinResources.getIdentifier(resName,resType,mSkinPkgName);
    return skinId;
    }

    /**
    * 输入主APP的ID,到皮肤APK文件中去找到对应ID的颜色值
    * @param resId
    * @return
    */
    public int getColor(int resId){
    if(isDefaultSkin){
    return mAppResources.getColor(resId);
    }
    int skinId=getIdentifier(resId);
    if(skinId==0){
    return mAppResources.getColor(resId);
    }
    return mSkinResources.getColor(skinId);
    }

    public ColorStateList getColorStateList(int resId) {
    if (isDefaultSkin) {
    return mAppResources.getColorStateList(resId);
    }
    int skinId = getIdentifier(resId);
    if (skinId == 0) {
    return mAppResources.getColorStateList(resId);
    }
    return mSkinResources.getColorStateList(skinId);
    }

    public Drawable getDrawable(int resId) {
    if (isDefaultSkin) {
    return mAppResources.getDrawable(resId);
    }
    //通过 app的resource 获取id 对应的 资源名 与 资源类型
    //找到 皮肤包 匹配 的 资源名资源类型 的 皮肤包的 资源 ID
    int skinId = getIdentifier(resId);
    if (skinId == 0) {
    return mAppResources.getDrawable(resId);
    }
    return mSkinResources.getDrawable(skinId);
    }


    /**
    * 可能是Color 也可能是drawable
    *
    * @return
    */
    public Object getBackground(int resId) {
    String resourceTypeName = mAppResources.getResourceTypeName(resId);

    if ("color".equals(resourceTypeName)) {
    return getColor(resId);
    } else {
    // drawable
    return getDrawable(resId);
    }
    }

    }

    自定义LayoutInflatorFactory

  3. 自定义LayoutInflatorFactory继承LayoutInflater.Factory2和Observer,是一个观察者对象,需要对皮肤管理器对象进行监听,接收到通知时应用皮肤,刷新状态栏,所有需要换肤的View刷新UI
  4. 解析xml创建View时,如果不是SDK自带的view就反射创建,如果是SDK的view尝试增加固定前缀,再反射创建,再修改View的属性刷新UI,达到换肤的效果
    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
    public class SkinLayoutInflaterFactory implements LayoutInflater.Factory2, Observer {
    // 待尝试的系统view固定前缀
    private static final String[] mClassPrefixList = {
    "android.widget.",
    "android.webkit.",
    "android.app.",
    "android.view."
    };


    private static final Class<?>[] mConstructorSignature = new Class[] {
    Context.class, AttributeSet.class};
    // 记录view名称与对应构造函数的映射
    private static final HashMap<String, Constructor<? extends View>> mConstructorMap =
    new HashMap<String, Constructor<? extends View>>();

    // 当选择新皮肤后需要替换View与之对应的属性
    // Activity的属性管理器
    private SkinAttribute skinAttribute;
    // 用于获取窗口的状态栏
    private Activity activity;

    // 初始化属性管理器
    public SkinLayoutInflaterFactory(Activity activity) {
    this.activity = activity;
    skinAttribute = new SkinAttribute();
    }

    @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
    //换肤就是替换View的属性(src、background等)
    //创建 View,再修改View属性
    View view = createSDKView(name, context, attrs);
    if (null == view) {
    view = createView(name, context, attrs);
    }

    if (null != view) {
    //加载属性
    skinAttribute.look(view, attrs);
    }
    return view;
    }


    private View createSDKView(String name, Context context, AttributeSet
    attrs) {
    //如果包含 . 则不是SDK中的view,可能是自定义view包括support库中的View,尝试使用后面的构造方法反射创建
    if (-1 != name.indexOf('.')) {
    return null;
    }
    //不包含,是SDK中的view,在解析的name节点前,拼接上一些前缀如:android.widget. 尝试反射创建View
    for (int i = 0; i < mClassPrefixList.length; i++) {
    View view = createView(mClassPrefixList[i] + name, context, attrs);
    if(view!=null){
    return view;
    }
    }
    return null;
    }

    // 反射创建View
    private View createView(String name, Context context, AttributeSet
    attrs) {
    Constructor<? extends View> constructor = findConstructor(context, name);
    try {
    return constructor.newInstance(context, attrs);
    } catch (Exception e) {
    }
    return null;
    }

    private Constructor<? extends View> findConstructor(Context context, String name) {
    Constructor<? extends View> constructor = mConstructorMap.get(name);
    if (constructor == null) {
    try {
    Class<? extends View> clazz = context.getClassLoader().loadClass
    (name).asSubclass(View.class);
    constructor = clazz.getConstructor(mConstructorSignature);
    mConstructorMap.put(name, constructor);
    } catch (Exception e) {
    }
    }
    return constructor;
    }

    @Override
    public View onCreateView(String name, Context context, AttributeSet attrs) {
    return null;
    }

    //如果有人发送通知,这里就会执行
    @Override
    public void update(Observable o, Object arg) {
    SkinThemeUtils.updateStatusBarColor(activity);
    skinAttribute.applySkin();
    }
    }

    属性管理器

    记录所有View需要替换的属性名称,需要换肤的View与View的属性信息列表,遍历当前解析的View的所有属性,过滤该View需要替换的属性并记录,对需要换肤的View进行属性修改
    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
    public class SkinAttribute {
    private static final List<String> mAttributes = new ArrayList<>();
    // 需要替换的属性名称
    static {
    mAttributes.add("background");
    mAttributes.add("src");
    mAttributes.add("textColor");
    mAttributes.add("drawableLeft");
    mAttributes.add("drawableTop");
    mAttributes.add("drawableRight");
    mAttributes.add("drawableBottom");
    }

    //记录需要换肤的View与View的属性信息
    private List<SkinView> mSkinViews = new ArrayList<>();


    // 遍历当前View的属性,记录需要替换的属性并执行换肤,对View进行属性修改
    public void look(View view, AttributeSet attrs) {
    List<SkinPair> mSkinPars = new ArrayList<>();

    for (int i = 0; i < attrs.getAttributeCount(); i++) {
    // 获得属性名如textColor/background
    String attributeName = attrs.getAttributeName(i);
    // 如果这个属性是需要替换的属性
    if (mAttributes.contains(attributeName)) {
    // 比如color的值有多种格式
    // #
    // ?722727272
    // @722727272
    // 以#开头表示写死的颜色 不可用于换肤
    String attributeValue = attrs.getAttributeValue(i);
    if (attributeValue.startsWith("#")) {
    continue;
    }
    int resId;
    // 以?开头的,去主题资源中找属性值
    if (attributeValue.startsWith("?")) {
    int attrId = Integer.parseInt(attributeValue.substring(1));
    resId = SkinThemeUtils.getResId(view.getContext(), new int[]{attrId})[0];
    } else {
    // 正常以 @ 开头
    resId = Integer.parseInt(attributeValue.substring(1));
    }
    // 记录到当前View需要替换的属性表
    SkinPair skinPair = new SkinPair(attributeName, resId);
    mSkinPars.add(skinPair);
    }
    }
    // 如果当前view有属性需要替换,或者该属性是支持换肤的自定义view
    if (!mSkinPars.isEmpty() || view instanceof SkinViewSupport) {
    SkinView skinView = new SkinView(view, mSkinPars);
    // 执行换肤,进行View的属性修改
    skinView.applySkin();
    // 添加到缓存
    mSkinViews.add(skinView);
    }
    }


    /*
    对所有需要换肤的view执行换肤操作
    */
    public void applySkin() {
    for (SkinView mSkinView : mSkinViews) {
    mSkinView.applySkin();
    }
    }

    static class SkinView {
    View view;
    // 当前View需要替换的属性列表
    List<SkinPair> skinPairs;

    public SkinView(View view, List<SkinPair> skinPairs) {
    this.view = view;
    this.skinPairs = skinPairs;

    }
    /**
    * 对一个View中所有需要替换的属性进行修改
    */
    public void applySkin() {
    // 如果是支持换肤的自定义view调换肤接口
    applySkinSupport();
    // 遍历当前View需要替换的属性列表
    for (SkinPair skinPair : skinPairs) {
    Drawable left = null, top = null, right = null, bottom = null;
    switch (skinPair.attributeName) {
    case "background":
    Object background = SkinResources.getInstance().getBackground(skinPair
    .resId);
    //背景可能是 @color 也可能是 @drawable
    if (background instanceof Integer) {
    view.setBackgroundColor((int) background);
    } else {
    ViewCompat.setBackground(view, (Drawable) background);
    }
    break;
    case "src":
    background = SkinResources.getInstance().getBackground(skinPair
    .resId);
    if (background instanceof Integer) {
    ((ImageView) view).setImageDrawable(new ColorDrawable((Integer)
    background));
    } else {
    ((ImageView) view).setImageDrawable((Drawable) background);
    }
    break;
    case "textColor":
    ((TextView) view).setTextColor(SkinResources.getInstance().getColorStateList
    (skinPair.resId));
    break;
    case "drawableLeft":
    left = SkinResources.getInstance().getDrawable(skinPair.resId);
    break;
    case "drawableTop":
    top = SkinResources.getInstance().getDrawable(skinPair.resId);
    break;
    case "drawableRight":
    right = SkinResources.getInstance().getDrawable(skinPair.resId);
    break;
    case "drawableBottom":
    bottom = SkinResources.getInstance().getDrawable(skinPair.resId);
    break;
    default:
    break;
    }
    if (null != left || null != right || null != top || null != bottom) {
    ((TextView) view).setCompoundDrawablesWithIntrinsicBounds(left, top, right,
    bottom);
    }
    }
    }
    private void applySkinSupport() {
    if (view instanceof SkinViewSupport) {
    ((SkinViewSupport) view).applySkin();
    }
    }
    }

    static class SkinPair {
    //属性名
    String attributeName;
    //对应的资源id
    int resId;

    public SkinPair(String attributeName, int resId) {
    this.attributeName = attributeName;
    this.resId = resId;
    }
    }
    }

主题工具类

负责根据属性id获得theme中对应资源id的值,刷新状态栏颜色为皮肤包中定义的颜色

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
public class SkinThemeUtils {

private static int[] APPCOMPAT_COLOR_PRIMARY_DARK_ATTRS = {
android.support.v7.appcompat.R.attr.colorPrimaryDark
};
private static int[] STATUSBAR_COLOR_ATTRS = {android.R.attr.statusBarColor, android.R.attr
.navigationBarColor
};


/**
* 获得theme中的属性中定义的 资源id
* @param context
* @param attrs
* @return
*/
public static int[] getResId(Context context, int[] attrs) {
int[] resIds = new int[attrs.length];
TypedArray a = context.obtainStyledAttributes(attrs);
for (int i = 0; i < attrs.length; i++) {
resIds[i] = a.getResourceId(i, 0);
}
a.recycle();
return resIds;
}



public static void updateStatusBarColor(Activity activity) {
//5.0以上才能修改
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
return;
}
//获得 statusBarColor 与 nanavigationBarColor (状态栏颜色)
//当与 colorPrimaryDark 不同时 以statusBarColor为准
int[] resIds = getResId(activity, STATUSBAR_COLOR_ATTRS);
int statusBarColorResId = resIds[0];
int navigationBarColor = resIds[1];

//如果获取到状态栏颜色资源id,那么设置状态栏颜色
if (statusBarColorResId != 0) {
int color = SkinResources.getInstance().getColor(statusBarColorResId);
activity.getWindow().setStatusBarColor(color);
} else {
//获得主色资源id
int colorPrimaryDarkResId = getResId(activity, APPCOMPAT_COLOR_PRIMARY_DARK_ATTRS)[0];
// 如果获取到主色资源id,那么设置状态栏颜色
if (colorPrimaryDarkResId != 0) {
int color = SkinResources.getInstance().getColor(colorPrimaryDarkResId);
activity.getWindow().setStatusBarColor(color);
}
}
// 如果获取到导航栏资源id,那么设置导航栏颜色
if (navigationBarColor != 0) {
int color = SkinResources.getInstance().getColor
(navigationBarColor);
activity.getWindow().setNavigationBarColor(color);

}
}

}


Android中使用多进程可以分配更多的内存,内存空间隔离,Android系统是用户空间+内核空间,用户程序崩溃了系统内核程序也不受影响,其中Server、Client、ServiceManager 运行于用户空间,Binder驱动运行于内核空间。Android的多进程通信就是采用Binder实现的。

Binder驱动建立了内核缓存区和内核数据接收缓存区的映射关系,以及内核数据接收缓存区和接收进程用户空间地址的映射关系,内核缓存区和接收进程的用户空间地址存在内存映射,发送方进程将数据复制到内核缓存区,相当于把数据发送到了接收进程的用户空间。

为了更了解Android进程间的通信机制,理解AIDL的原理,利用binder实现进程间的通信,记录一次手写AIDL的过程


定义一个bean类Book,实现序列化

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
public class Book implements Parcelable {

private String name;
private int type;

public Book(String name, int type) {
this.name = name;
this.type = type;
}

protected Book(Parcel in) {
this.name = in.readString();
this.type = in.readInt();
}

public static final Creator<Book> CREATOR = new Creator<Book>() {
@Override
public Book createFromParcel(Parcel in) {
return new Book(in);
}

@Override
public Book[] newArray(int size) {
return new Book[size];
}
};

@Override
public int describeContents() {
return 0;
}

@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(name);
dest.writeInt(type);
}

@Override
public String toString() {
return "Book{" +
"name='" + name + '\'' +
", type=" + type +
'}';
}
}

定义操作接口类IBookManager继承IInterface,定义数据操作的方法

1
2
3
4
5
6
public interface IBookManager extends IInterface {

void addBook(Book book) throws RemoteException;

List<Book> getBookList() throws RemoteException;
}

定义抽象类Stub继承Binder类和IBookManager接口

  1. 实现onTransact方法,获取传入的参数,调用IBookManager接口的方法,这里暂不实现IBookManager的方法
  2. 返回IBookManager对象,需传入binder对象,如果与这个传入的binder对象在同一进程调用queryLocalInterface返回,否则把Binder传入Proxy对象返回一个远程的代理对象
  3. 返回Binder对象(自身)
    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
    public abstract class Stub extends Binder implements IBookManager {

    private static final String DESCRIPTOR = "com.enjoy.binder.common.IBookManager";

    public Stub() {
    this.attachInterface(this, DESCRIPTOR);
    }

    public static IBookManager asInterface(IBinder binder) {
    if ((binder == null)) {
    return null;
    }
    // 先查找本地接口对象,如果同一个进程直接返回
    IInterface iin = binder.queryLocalInterface(DESCRIPTOR);
    if ((iin != null) && (iin instanceof IBookManager)) {
    return (IBookManager) iin;
    }
    // 传入binder对象,返回一个远程的代理对象
    return new Proxy(binder);
    }

    // 返回自身对象
    @Override
    public IBinder asBinder() {
    return this;
    }

    @Override
    protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
    switch (code) {
    case INTERFACE_TRANSACTION:
    reply.writeString(DESCRIPTOR);
    return true;
    // 添加书,调用addBook接口
    case TRANSACTION_addBook:
    data.enforceInterface(DESCRIPTOR);
    // 获取传入的book对象
    Book arg0 = null;
    if ((0 != data.readInt())) {
    arg0 = Book.CREATOR.createFromParcel(data);
    }
    this.addBook(arg0);
    reply.writeNoException();
    return true;
    // 获取书的列表,调用getBookList接口
    case TRANSACTION_getBookList:
    data.enforceInterface(DESCRIPTOR);
    List<Book> result = this.getBookList();
    reply.writeNoException();
    reply.writeTypedList(result);
    return true;
    }
    return super.onTransact(code, data, reply, flags);
    }

    static final int TRANSACTION_addBook = IBinder.FIRST_CALL_TRANSACTION;
    static final int TRANSACTION_getBookList = IBinder.FIRST_CALL_TRANSACTION + 1;
    }
    定义一个代理类实现IBookManager接口,调用Stub类传入的Binder对象通过Stub类的onTransact方法进行数据操作
    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
    public class Proxy implements IBookManager {

    private static final String DESCRIPTOR = "com.enjoy.binder.common.IBookManager";

    private IBinder mRemote;
    // 保存Stub类传入的Binder对象
    public Proxy(IBinder remote) {
    mRemote = remote;
    }

    // 实现跨进程数据传输,调Binder对象的onTransact方法
    @Override
    public void addBook(Book book) throws RemoteException {
    Parcel data = Parcel.obtain(); // 跨进程传输数据对象
    Parcel reply = Parcel.obtain(); // 跨进程传输返回结果
    try {
    data.writeInterfaceToken(DESCRIPTOR);
    if ((book != null)) {
    data.writeInt(1);
    book.writeToParcel(data, 0);
    } else {
    data.writeInt(0);
    }
    // 调Stub的onTransact方法进行Stub.TRANSACTION_addBook处理,远端返回
    mRemote.transact(Stub.TRANSACTION_addBook, data, reply, 0);
    reply.readException();
    } finally {
    reply.recycle();
    data.recycle();
    }
    }

    // 实现跨进程数据传输,调Binder对象的onTransact方法
    @Override
    public List<Book> getBookList() throws RemoteException {
    Parcel data = Parcel.obtain(); // 跨进程传输数据对象
    Parcel reply = Parcel.obtain(); // 跨进程传输返回结果
    List<Book> result;
    try {
    data.writeInterfaceToken(DESCRIPTOR);
    // 调Stub的onTransact方法进行Stub.TRANSACTION_getBookList处理,远端返回
    mRemote.transact(Stub.TRANSACTION_getBookList, data, reply, 0);
    reply.readException();
    result = reply.createTypedArrayList(Book.CREATOR);
    } finally {
    reply.recycle();
    data.recycle();
    }
    return result;
    }

    // 返回Stub类传入的Binder对象
    @Override
    public IBinder asBinder() {
    return mRemote;
    }
    }
    定义一个工作在独立进程的远程服务类,定义数据源,创建Binder对象,实现数据操作接口IBookManager内addBook方法和getBookList方法的具体业务逻辑,并返回创建的Binder对象
    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
    public class RemoteService extends Service {

    private ArrayList<Book> books; // 定义数据源

    // 服务绑定后返回Binder对象
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
    books = new ArrayList<>();
    Log.e("RemoteService", "success onBind");
    return iBinder;
    }

    // 创建一个Binder对象,实现IBookManager的数据操作方法,Stub类的对象在远端进程(独立进程)初始化
    private IBinder iBinder = new Stub() {
    @Override
    public void addBook(Book book) throws RemoteException {
    books.add(book);
    }

    @Override
    public List<Book> getBookList() throws RemoteException {
    return books;
    }
    };
    }
    在AndroidManifest中声明该服务在对立进程
    1
    2
    3
    4
    5
    6
    7
    8
    9
    <service
    android:name=".server.RemoteService"
    android:exported="true"
    android:process=":remote">
    <intent-filter>
    <action android:name="com.enjoy.binder" />
    <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
    </service>
    在主进程Activity中绑定远端服务进程,并通过binder对象调用数据操作方法
    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
    public class ClientActivity extends AppCompatActivity {

    private IBookManager iBookManager;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_main);
    // 绑定远端服务进程
    Intent intent = new Intent(this, RemoteService.class);
    intent.setAction("com.enjoy.binder");
    bindService(intent, connection, Context.BIND_AUTO_CREATE);
    // 点击,通过binder对象(远程代理对象)调用数据操作方法
    findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    try {
    iBookManager.addBook(new Book("数据结构与算法", 3));
    List<Book> books = iBookManager.getBookList();
    } catch (RemoteException e) {
    e.printStackTrace();
    }
    }
    });
    }

    private ServiceConnection connection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
    Log.e("ClientActivity", "onServiceConnected: success");
    iBookManager = Stub.asInterface(service);// Binder对象(远程代理对象)
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {
    Log.e("ClientActivity", "onServiceDisconnected: success");
    iBookManager = null;
    }
    };
    }
    最后总结一下:
  4. Stub继承binder实现服务接口,onTransact根据code调服务接口
  5. Activity绑定远程服务,连接回调中获取Stub.asInterface,区分是否同一进程返回代理对象,如果是返回本地接口,否则返回远程服务onBinder返回的IBinder代理对象
  6. 代理类实现服务接口,数据打包,检查token,跨进程调transact序列化发送data和获取返回reply,客户端线程挂起
  7. 服务端实例化Stub并实现服务接口接收客户端请求并响应


对于WebView的重度使用,如游戏音视频比较耗内存的需求,把WebView放到一个新的进程可以申请到更大的内存,修改其Activity的进程,通过AIDL与主线程通信,与主进程隔离,避免WebView的不稳定性导致所在进程异常影响主进程的正常运行造成不必要的crash,在此记录一下封装过程,欢迎一起学习和讨论~


封装WebView

自定义WebView

初始化WebView默认设置,设置javascript调用接口,回调js的dispatchEvent方法做事件分发,web进程初始化主进程调用接口,加载url时重置touch状态,监听用户的触摸操作,通过是否为用户点击(touch状态)判断当前跳转是否为重定向

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
open class BaseWebView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : WebView(context, attrs, defStyleAttr), BaseWebViewClient.WebviewTouch {
private var webViewCallBack: WebViewCallBack? = null
private var mHeaders: HashMap<String, String> ?= null
private var isTouchByUser: Boolean = false
private var redirectIntercept = false // 是否拦截重定向
protected var mContext: Context? = null

init {
init(context)
}
fun registerWebViewCallBack(webViewCallBack: WebViewCallBack) {
this.webViewCallBack = webViewCallBack
webViewClient = BaseWebViewClient(this, webViewCallBack, mHeaders, this, redirectIntercept)
}

fun setHeaders(mHeaders: HashMap<String, String>) {
this.mHeaders = mHeaders
}

fun setRedirectIntercept(intercept: Boolean){
redirectIntercept = intercept
}

protected fun init(context: Context?) {
mContext = context
// 初始化默认设置
WebviewDefaultSetting.getInstance().toSetting(this)
// javascript调用接口
addJavascriptInterface(this, "webview")
// web进程初始化主进程调用接口
CommandDispatcher.getInstance().initAidlConnect(getContext())
}


@JavascriptInterface
fun post(cmd: String, param: String) {
// web进程的主线程执行
Handler().post {
try {
if (webViewCallBack != null) {
CommandDispatcher.getInstance().execBySelf(context, cmd, param, this@BaseWebView)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}

override fun loadUrl(url: String) {
if (mHeaders == null) {
super.loadUrl(url)
} else {
super.loadUrl(url, mHeaders)
}
Log.e(TAG, "load url: $url")
resetAllStateInternal(url)
}

/**
* 处理header数据请求
* @param url String
* @param additionalHttpHeaders Map<String?, String?>
*/
override fun loadUrl(url: String, additionalHttpHeaders: Map<String?, String?>?) {
super.loadUrl(url, additionalHttpHeaders)
Log.e(TAG, "load url: $url")
resetAllStateInternal(url)
}

/**
* 回调js的callback方法
* @param response String?
*/
fun handleCallback(response: String?) {
if (!TextUtils.isEmpty(response)) {
val trigger = "javascript:callback($response)"
evaluateJavascript(trigger, null)
}
}

/**
* 回调js的cmd方法
* @param cmd String
* @param param Any?
*/
fun loadJS(cmd: String, param: Any?) {
val trigger = "javascript:" + cmd + "(" + Gson().toJson(param) + ")"
evaluateJavascript(trigger, null)
}

/**
* 回调js的dispatchEvent方法做事件分发
* @param name String
*/
fun dispatchEvent(name: String) {
val param = HashMap<String,String>(1)
param["name"] = name
loadJS("dispatchEvent", param)
}

private fun resetAllStateInternal(url: String) {
// url为空或者回调js方法直接返回
if (!TextUtils.isEmpty(url) && url.startsWith("javascript:")) {
return
}
resetAllState()
}

// 加载url时重置touch状态
protected fun resetAllState() {
isTouchByUser = false
}

override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> isTouchByUser = true
}
return super.onTouchEvent(event)
}

companion object {
private const val TAG = "BaseWebView"
const val CONTENT_SCHEME = "file:///android_asset/"
}

override fun isTouchByUser(): Boolean {
return isTouchByUser
}
}

封装WebViewClient

回调接口逻辑,url跳转判断,重定向处理,对特殊链接统一处理,刷新处理,拦截指定url处理,判断是否加载完成,封装请求头,ssl错误处理

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
167
168
169
170
171
class BaseWebViewClient(
val webView: WebView, // 封装的回调
val webViewCallBack: WebViewCallBack, // 封装的回调
val mHeaders: HashMap<String, String>?, // 请求头
val mWebviewTouch: WebviewTouch, // 是否有点击操作
val redirectIntercept: Boolean // 是否拦截重定向
) : WebViewClient() {
var isReady = false // 是否加载完成

interface WebviewTouch {
fun isTouchByUser(): Boolean
}

/**
* url重定向会执行此方法以及点击页面某些链接也会执行此方法
*
* @return true:表示当前url已经加载完成,即使url还会重定向都不会再进行加载 false 表示此url默认由系统处理,该重定向还是重定向,直到加载完成
*/
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
Log.e(TAG, "shouldOverrideUrlLoading url: $url")
// 未发生过点击,当前是重定向且不拦截重定向
if (!mWebviewTouch.isTouchByUser() && !redirectIntercept) {
return super.shouldOverrideUrlLoading(view, url)
}
// 如果链接跟当前链接一样,表示刷新
if (webView.url == url) {
return super.shouldOverrideUrlLoading(view, url)
}
// 特殊链接处理,跳转手机自带应用
if (handleLinked(url)) {
return true
}
// 拦截指定url
if (webViewCallBack.overrideUrlLoading(url)) {
return true
}
// 控制页面中点开新的链接在当前webView中打开
view.loadUrl(url, mHeaders)
return true
}

@RequiresApi(api = Build.VERSION_CODES.N)
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
Log.e(TAG, "shouldOverrideUrlLoading url: " + request.url)
// 当前链接的重定向
if (!mWebviewTouch.isTouchByUser() && !redirectIntercept) {
return super.shouldOverrideUrlLoading(view, request)
}
// 如果链接跟当前链接一样,表示刷新
if (webView.url == request.url.toString()) {
return super.shouldOverrideUrlLoading(view, request)
}
// 需要跳转应用的特殊链接
if (handleLinked(request.url.toString())) {
return true
}
// 拦截指定url
if (webViewCallBack.overrideUrlLoading(request.url.toString())) {
return true
}
// 控制页面中点开新的链接在当前webView中打开
view.loadUrl(request.url.toString(), mHeaders)
return true
}

/**
* 支持电话、短信、邮件、地图跳转,跳转的都是手机系统自带的应用
*/
private fun handleLinked(url: String): Boolean {
if (url.startsWith(WebView.SCHEME_TEL)
|| url.startsWith(SCHEME_SMS)
|| url.startsWith(WebView.SCHEME_MAILTO)
|| url.startsWith(WebView.SCHEME_GEO)
) {
try {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(url)
webView.context.startActivity(intent)
} catch (ignored: ActivityNotFoundException) {
ignored.printStackTrace()
}
return true
}
return false
}

override fun onPageFinished(view: WebView?, url: String?) {
Log.e(TAG, "onPageFinished url:$url")
if (url == null) return
if (!TextUtils.isEmpty(url) && url.startsWith(CONTENT_SCHEME)) {
isReady = true
}
webViewCallBack.pageFinished(url)
}

override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
Log.e(TAG, "onPageStarted url: $url")
if (url == null) return
webViewCallBack.pageStarted(url)
}


override fun onScaleChanged(view: WebView?, oldScale: Float, newScale: Float) {
super.onScaleChanged(view, oldScale, newScale)
}

@TargetApi(21)
override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest
): WebResourceResponse? {
return shouldInterceptRequest(view, request.url.toString())
}

/**
* 默认继续加载
* @param view WebView
* @param url String
* @return WebResourceResponse?
*/
override fun shouldInterceptRequest(view: WebView, url: String): WebResourceResponse? {
return null
}

/**
* webview加载错误处理
* @param view WebView
* @param errorCode Int
* @param description String
* @param failingUrl String
*/
override fun onReceivedError(
view: WebView,
errorCode: Int,
description: String,
failingUrl: String
) {
super.onReceivedError(view, errorCode, description, failingUrl)
Log.e(
TAG,
"webview error$errorCode + $description"
)
webViewCallBack.onError(errorCode,description,failingUrl)
}

/**
* SSL错误处理
* @param view
* @param handler
* @param error
*/
override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) {
var message = webView.context.getString(R.string.ssl_error)
when (error.primaryError) {
SslError.SSL_UNTRUSTED -> message =
webView.context.getString(R.string.ssl_error_not_trust)
SslError.SSL_EXPIRED -> message = webView.context.getString(R.string.ssl_error_expired)
SslError.SSL_IDMISMATCH -> message =
webView.context.getString(R.string.ssl_error_mismatch)
SslError.SSL_NOTYETVALID -> message =
webView.context.getString(R.string.ssl_error_not_valid)
}
message += webView.context.getString(R.string.ssl_error_continue_open)
Log.v(TAG,message)
}

companion object {
private const val TAG = "WebviewClient"
const val SCHEME_SMS = "sms:"
}
}

封装WebChromeClient

处理文件选择和相册选择回调,js提示回调,进度刷新回调

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
class BaseWebChromeClient(private val progressHandler: Handler) : WebChromeClient() {
private var mFilePathCallback: ValueCallback<Array<Uri>>? = null // 文件选择回调
private var mCameraPhotoPath: String? = null // 相册选择回调
override fun onReceivedTitle(view: WebView, title: String) {
super.onReceivedTitle(view, title)
if (view is ProgressWebView) {
if (!TextUtils.isEmpty(title)) {
val params = ArrayMap<String, String>()
params[WebConstants.COMMAND_UPDATE_TITLE_PARAMS] = title
// 调起刷新标题命令
(view as BaseWebView).post(WebConstants.COMMAND_UPDATE_TITLE, Gson().toJson(params))
}
}
}

// 进度更新回调
override fun onProgressChanged(view: WebView, newProgress: Int) {
var newProgress = newProgress
val message = Message()
if (newProgress == 100) {
message.obj = newProgress
progressHandler.sendMessageDelayed(message, 200)
} else {
if (newProgress < 10) {
newProgress = 10
}
message.obj = newProgress
progressHandler.sendMessage(message)
}
super.onProgressChanged(view, newProgress)
}

// js 提示回调
override fun onJsAlert(view: WebView, url: String, message: String, result: JsResult): Boolean {
AlertDialog.Builder(view.context)
.setTitle(R.string.dialog_alert_title)
.setMessage(message)
.setPositiveButton(
R.string.ok
) { dialoginterface, i -> //按钮事件
Toast.makeText(
view.context,
view.context.getString(R.string.ok) + " clicked.",
Toast.LENGTH_LONG
).show()
}.show()
//result.confirm();// 不加这行代码,会造成Alert劫持:Alert只会弹出一次,并且WebView会卡死
return true
}

//文件选择回调
override fun onShowFileChooser(
webView: WebView,
filePathCallback: ValueCallback<Array<Uri>>,
fileChooserParams: FileChooserParams
): Boolean {
mFilePathCallback?.onReceiveValue(null)
mFilePathCallback = filePathCallback
var takePictureIntent:Intent? = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
if (takePictureIntent?.resolveActivity(webView.context.packageManager) != null) {
// Create the File where the photo should go
var photoFile: File? = null
try {
photoFile = createImageFile()
takePictureIntent.putExtra("PhotoPath", mCameraPhotoPath)
} catch (ex: IOException) {
ex.printStackTrace()
}

// Continue only if the File was successfully created
if (photoFile != null) {
mCameraPhotoPath = "file:" + photoFile.absolutePath
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(photoFile))
} else {
takePictureIntent = null
}
}
if (takePictureIntent != null && mFilePathCallback != null){
(webView as BaseWebView).webViewCallBack?.onShowFileChooser(takePictureIntent, mFilePathCallback!!)
}
return true
}

/**
* More info this method can be found at
* http://developer.android.com/training/camera/photobasics.html
*
* @return
* @throws IOException
*/
@Throws(IOException::class)
private fun createImageFile(): File {
// Create an image file name
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
val imageFileName = "JPEG_" + timeStamp + "_"
val storageDir =
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
return File.createTempFile(imageFileName, ".jpg", storageDir)
}
}

封装WebView回调接口

1
2
3
4
5
6
7
8
9
10
11
interface WebViewCallBack {
fun pageStarted(url: String) // 页面开始加载
fun pageFinished(url: String) // 页面加载完成
fun overrideUrlLoading(url: String): Boolean // 拦截url
fun onError(errorCode: Int,description: String,failingUrl:String) // 错误回调
// 执行操作
fun onShowFileChooser(
cameraIntent: Intent,
filePathCallback: ValueCallback<Array<Uri>>
)
}

封装进度条

大部分app展示web页面顶部有个web页的加载进度条提升交互体验

定义进度条操作接口

1
2
3
4
5
6
interface BaseProgressSpec {
fun show()
fun hide()
fun reset()
fun setProgress(newProgress: Int)
}

封装进度条控制逻辑

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
class IndicatorHandler{
var baseProgressSpec: BaseProgressSpec? = null // 进度条接口

/**
* 单例
*/
companion object{
@Volatile private var instance: IndicatorHandler? = null
fun getInstance() = instance?: synchronized(this){
instance?: IndicatorHandler().also { instance = it }
}
}

/**
* 进度刷新
* @param newProgress Int
*/
fun progress(newProgress: Int) {
when (newProgress) {
0 -> {
reset()
}
in 1..10 -> {
showProgressBar()
}
in 11..94 -> {
setProgressBar(newProgress)
}
else -> {
setProgressBar(newProgress)
finish()
}
}
}


fun reset() {
baseProgressSpec?.reset()
}

fun finish() {
baseProgressSpec?.hide()
}

fun setProgressBar(n: Int) {
baseProgressSpec?.setProgress(n)
}

fun showProgressBar() {
baseProgressSpec?.show()
}

fun inJectProgressView(baseProgressSpec: BaseProgressSpec?):IndicatorHandler {
this.baseProgressSpec = baseProgressSpec
return this
}
}

自定义进度条

自定义FrameLayout布局,实现进度条操作接口,进度小于95时渲染匀速动画,大于等于95时渲染透明加减速动画

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
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
class WebProgressBar @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr),BaseProgressSpec {
/**
* 进度条颜色
*/
private var mColor = 0

/**
* 进度条的画笔
*/
private var mPaint: Paint = Paint()

/**
* 进度条动画
*/
private var mAnimator: Animator? = null

/**
* 控件的宽度
*/
private var mTargetWidth = 0

companion object{
/**
* 默认匀速动画最大的时长
*/
const val MAX_UNIFORM_SPEED_DURATION = 5 * 1000

/**
* 默认加速后减速动画最大时长
*/
const val MAX_DECELERATE_SPEED_DURATION = 600

/**
* 结束动画时长 , Fade out 。
*/
const val DO_END_ANIMATION_DURATION = 300

/**
* 当前匀速动画最大的时长
*/
var CURRENT_MAX_UNIFORM_SPEED_DURATION = MAX_UNIFORM_SPEED_DURATION

/**
* 当前加速后减速动画最大时长
*/
var CURRENT_MAX_DECELERATE_SPEED_DURATION = MAX_DECELERATE_SPEED_DURATION

/**
* 标志当前进度条的状态
*/
private var TAG = 0
const val UN_START = 0
const val STARTED = 1
const val FINISH = 2

/**
* 默认的高度
*/
var WEB_PROGRESS_DEFAULT_HEIGHT = 3
}

init {
init(context)
}

private fun init(context: Context) {
mColor = Color.parseColor("#c15d3e")
mPaint.isAntiAlias = true
mPaint.color = mColor
mPaint.isDither = true
mPaint.strokeCap = Paint.Cap.SQUARE
mTargetWidth = context.resources.displayMetrics.widthPixels
WEB_PROGRESS_DEFAULT_HEIGHT = dipToPx(context, 2.5f)
}

private fun setColor(color: Int) {
mColor = color
mPaint.color = color
}

fun setColor(color: String) {
this.setColor(Color.parseColor(color))
}

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val wMode = MeasureSpec.getMode(widthMeasureSpec)
val hMode = MeasureSpec.getMode(heightMeasureSpec)
var w = MeasureSpec.getSize(widthMeasureSpec)
var h = MeasureSpec.getSize(heightMeasureSpec)
if (wMode == MeasureSpec.AT_MOST) {
w =
if (w <= context.resources.displayMetrics.widthPixels) w else context.resources.displayMetrics.widthPixels
}
if (hMode == MeasureSpec.AT_MOST) {
h = WEB_PROGRESS_DEFAULT_HEIGHT
}
setMeasuredDimension(w, h)
}

private var currentProgress = 0f

override fun onDraw(canvas: Canvas?) {}

override fun dispatchDraw(canvas: Canvas) {
canvas.drawRect(
0f,
0f,
currentProgress / 100 * java.lang.Float.valueOf(this.width.toFloat()),
this.height.toFloat(),
mPaint
)
}

override fun show() {
if (visibility == GONE) {
this.visibility = VISIBLE
currentProgress = 0f
startAnim(false)
}
}

override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
mTargetWidth = measuredWidth
val screenWidth = context.resources.displayMetrics.widthPixels
if (mTargetWidth >= screenWidth) {
CURRENT_MAX_DECELERATE_SPEED_DURATION = MAX_DECELERATE_SPEED_DURATION
CURRENT_MAX_UNIFORM_SPEED_DURATION = MAX_UNIFORM_SPEED_DURATION
} else {
//取比值
val rate = mTargetWidth / java.lang.Float.valueOf(screenWidth.toFloat())
CURRENT_MAX_UNIFORM_SPEED_DURATION = (MAX_UNIFORM_SPEED_DURATION * rate).toInt()
CURRENT_MAX_DECELERATE_SPEED_DURATION = (MAX_DECELERATE_SPEED_DURATION * rate).toInt()
}
}

fun setProgress(progress: Float) {
if (visibility == GONE) {
visibility = VISIBLE
}
if (progress < 95f) return
if (TAG != FINISH) {
startAnim(true)
}
}

override fun hide() {
TAG = FINISH
}


private var target = 0f


/**
* 开始动画
* @param isFinished Boolean
*/
private fun startAnim(isFinished: Boolean) {
val v: Float = if (isFinished) 100F else 95.toFloat()
if (mAnimator?.isStarted == true) {
mAnimator?.cancel()
}
// 刷新当前进度
currentProgress = if (currentProgress == 0f) 0.00000001f else currentProgress
// 进度为0-94,匀速动画
if (!isFinished) {
val mAnimator = ValueAnimator.ofFloat(currentProgress, v)
val residue = 1f - currentProgress / 100 - 0.05f
// 使用匀速插值器
mAnimator.interpolator = LinearInterpolator()
mAnimator.duration = (residue * CURRENT_MAX_UNIFORM_SPEED_DURATION).toLong()
mAnimator.addUpdateListener(mAnimatorUpdateListener)
mAnimator.start()
this.mAnimator = mAnimator
}
// 进度大于等于95后执行透明减速的动画
else {
var segment95Animator: ValueAnimator? = null
// 使用减速插值器
if (currentProgress < 95f) {
segment95Animator = ValueAnimator.ofFloat(currentProgress, 95f)
val residue = 1f - currentProgress / 100f - 0.05f
segment95Animator.duration =
(residue * CURRENT_MAX_DECELERATE_SPEED_DURATION).toLong()
segment95Animator.interpolator = DecelerateInterpolator()
segment95Animator.addUpdateListener(mAnimatorUpdateListener)
}
// alpha动画
val mObjectAnimator = ObjectAnimator.ofFloat(this, "alpha", 1f, 0f)
mObjectAnimator.duration = DO_END_ANIMATION_DURATION.toLong()
val mValueAnimatorEnd = ValueAnimator.ofFloat(95f, 100f)
mValueAnimatorEnd.duration = DO_END_ANIMATION_DURATION.toLong()
mValueAnimatorEnd.addUpdateListener(mAnimatorUpdateListener)
var mAnimatorSet = AnimatorSet()
mAnimatorSet.playTogether(mObjectAnimator, mValueAnimatorEnd)
if (segment95Animator != null) {
val mAnimatorSet1 = AnimatorSet()
// 执行alpha动画并刷新进度,后执行减速动画
mAnimatorSet1.play(mAnimatorSet).after(segment95Animator)
mAnimatorSet = mAnimatorSet1
}
mAnimatorSet.addListener(mAnimatorListenerAdapter)
mAnimatorSet.start()
mAnimator = mAnimatorSet
}
TAG = STARTED
target = v
}

// 动画刷新回调,刷新进度
private val mAnimatorUpdateListener =
ValueAnimator.AnimatorUpdateListener { animation ->
val t = animation.animatedValue as Float
currentProgress = t
this@WebProgressBar.invalidate()
}

// 监听动画完成
private val mAnimatorListenerAdapter: AnimatorListenerAdapter =
object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
doEnd()
}
}

// 释放动画资源
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
/**
* animator cause leak , if not cancel;
*/
if (mAnimator?.isStarted == true) {
mAnimator?.cancel()
mAnimator = null
}
}

// 进度条执行完处理
private fun doEnd() {
if (TAG == FINISH && currentProgress == 100f) {
visibility = GONE
currentProgress = 0f
this.alpha = 1f
}
TAG = UN_START
}

// 状态重置
override fun reset() {
currentProgress = 0f
if (mAnimator?.isStarted == true) mAnimator?.cancel()
}

override fun setProgress(newProgress: Int) {
setProgress(newProgress.toFloat())
}
}

封装带进度条的WebView

设置自定义进度条,添加到WebView,传入主线程handler调用刷新进度条逻辑

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
class ProgressWebView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : BaseWebView(context,attrs,defStyleAttr) {
private var indicatorHandler: IndicatorHandler? = null
private var progressBar: WebProgressBar = WebProgressBar(context)
// 主线程调刷新进度条逻辑
private val mHandler: Handler = object :Handler(Looper.getMainLooper()){
override fun handleMessage(msg: Message) {
val progress = msg.obj as Int
indicatorHandler?.progress(progress)
}
}

init {
init()
}

private fun init() {
progressBar.layoutParams = LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
progressBar.visibility = GONE
addView(progressBar)
indicatorHandler = IndicatorHandler.getInstance().inJectProgressView(progressBar)
webChromeClient = BaseWebChromeClient(mHandler)
}
}

封装带WebView的Fragment

  • fragment基类,统一设置标题

    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
    open class BaseFragment: Fragment() {
    protected var mContext: Context? = null

    fun setTitle(titleId: Int) {
    activity?.setTitle(titleId)
    }

    fun setTitle(title: CharSequence?) {
    activity?.title = title
    }

    override fun onAttach(context: Context) {
    super.onAttach(context)
    this.mContext = context
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    if (mContext == null){
    mContext = context
    }
    }

    override fun getContext(): Context? {
    return if (super.getContext() == null) mContext else super.getContext()
    }
    }
  • 封装带Webview的fragment基类,处理带header请求,定义页面开始加载,结束加载,指定url拦截,加载错误回调,是否重定向拦截,网页返回按键处理,相册文件选择回调处理,页面销毁时释放WebView

    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
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    abstract class BaseWebviewFragment : BaseFragment(), WebViewCallBack {
    companion object {
    const val INFO_HEADERS = "info_headers"
    const val REDIRECT_INTERCEPT = "redirect_intercept"
    const val REQUEST_CODE = 1
    }

    var webView: BaseWebView? = null
    protected var headers: HashMap<String, String> = HashMap()
    var webUrl: String? = null
    var pageStarted: ((String) -> Unit)? = null
    var pageFinished: ((String) -> Unit)? = null
    var overrideUrlLoading: ((String) -> Boolean)? = null
    var onError: ((Int, String, String) -> Unit)? = null
    var onShowFileChooser: ((Intent, ValueCallback<Array<Uri>>) -> Unit)? = null
    private var redirectIntercept = false
    @LayoutRes
    protected abstract fun getLayoutRes(): Int

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val bundle = arguments
    if (bundle != null) {
    webUrl = bundle.getString(WebConstants.INTENT_TAG_URL)
    if (bundle.containsKey(INFO_HEADERS)) {
    headers =
    bundle.getSerializable(INFO_HEADERS) as HashMap<String, String>
    redirectIntercept = bundle.getBoolean(REDIRECT_INTERCEPT)
    }
    }
    // 注册吐司命令
    CommandsManager.getInstance().registerCommand(ToastCommand())
    }

    override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
    ): View? {
    val view = inflater.inflate(getLayoutRes(), container, false)
    webView = view.findViewById(R.id.web_view)
    webView?.setHeaders(headers)
    webView?.setRedirectIntercept(redirectIntercept)
    return view
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    webView?.registerWebViewCallBack(this)
    loadUrl()
    }

    protected open fun loadUrl() {
    webUrl?.let {
    webView?.loadUrl(it)
    }
    }

    override fun onResume() {
    super.onResume()
    Log.v("BaseWebviewFragment","onResume")
    webView?.dispatchEvent("pageResume")
    webView?.onResume()
    }

    override fun onPause() {
    super.onPause()
    Log.v("BaseWebviewFragment","onPause")
    webView?.dispatchEvent("pagePause")
    webView?.onPause()
    }

    override fun onStop() {
    super.onStop()
    webView?.dispatchEvent("pageStop")
    }

    override fun onDestroyView() {
    super.onDestroyView()
    webView?.dispatchEvent("pageDestroy")
    clearWebView(webView)
    }

    open fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
    return if (keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_DOWN) {
    onBackHandle()
    } else false
    }


    override fun pageStarted(url: String) {
    this.pageStarted?.invoke(url)
    }

    override fun pageFinished(url: String) {
    this.pageFinished?.invoke(url)
    }

    override fun overrideUrlLoading(url: String): Boolean {
    this.overrideUrlLoading?.let {
    return it.invoke(url)
    }
    return false
    }

    override fun onError(errorCode: Int, description: String, failingUrl: String) {
    onError?.invoke(errorCode, description, failingUrl)
    }

    /**
    * 处理返回
    * @return Boolean
    */
    protected open fun onBackHandle(): Boolean {
    return if (webView != null) {
    if (webView!!.canGoBack()) {
    webView?.goBack()
    true
    } else {
    false
    }
    } else false
    }

    private fun clearWebView(m: WebView?) {
    val m: WebView? = m ?: return
    // 非主线程退出
    if (Looper.myLooper() != Looper.getMainLooper()) return
    // 停止加载处理
    m?.stopLoading()
    if (m?.handler != null) {
    m.handler.removeCallbacksAndMessages(null)
    }
    // 移除webview的所有view
    m?.removeAllViews()
    // 获取父布局移除webview
    val mViewGroup: ViewGroup? = m?.parent as? ViewGroup
    mViewGroup?.removeView(m)
    // 回调置空
    m?.webChromeClient = null
    m?.webViewClient = null
    m?.tag = null
    // 清理历史并销毁
    m?.clearHistory()
    m?.destroy()
    }

    override fun onShowFileChooser(
    cameraIntent: Intent,
    filePathCallback: ValueCallback<Array<Uri>>
    ) {
    if (onShowFileChooser != null) {
    onShowFileChooser!!.invoke(cameraIntent, filePathCallback)
    } else {
    mFilePathCallback = filePathCallback
    //------------------------------------
    //弹出选择框有:相机、相册(Android9.0,Android8.0)
    //如果是小米Android6.0系统上,依然是:相机、相册、文件管理
    //如果安装了其他的相机(百度魔拍)、文件管理程序(ES文件管理器),也有可能会弹出
    val selectionIntent = Intent(Intent.ACTION_PICK, null)
    selectionIntent.type = "image/*"
    //------------------------------------
    val intentArray: Array<Intent?> = arrayOf(cameraIntent)
    val chooserIntent = Intent(Intent.ACTION_CHOOSER)
    chooserIntent.putExtra(Intent.EXTRA_TITLE, getString(R.string.file_chooser))
    chooserIntent.putExtra(Intent.EXTRA_INTENT, selectionIntent)
    chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, intentArray)
    startActivityForResult(chooserIntent, REQUEST_CODE)
    }
    }

    private var mFilePathCallback: ValueCallback<Array<Uri>>? = null
    private val mCameraPhotoPath: String? = null

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    when (requestCode) {
    REQUEST_CODE -> {
    var results: Array<Uri>? = null
    if (resultCode == Activity.RESULT_OK) {
    if (data == null) {
    if (mCameraPhotoPath != null) {
    Log.d("AppChooserFragment", mCameraPhotoPath)
    results = arrayOf(Uri.parse(mCameraPhotoPath))
    }
    } else {
    val dataString = data.dataString
    if (dataString != null) {
    results = arrayOf(Uri.parse(dataString))
    }
    }
    }
    mFilePathCallback?.onReceiveValue(results)
    mFilePathCallback = null
    }
    }
    }
    }
  • WebViewFragment实现类,header传参,是否拦截重定向,是否同步header到cookie

    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
    class WebviewFragment : BaseWebviewFragment() {
    override fun getLayoutRes(): Int {
    return R.layout.fragment_common_webview
    }

    companion object{
    fun newInstance(keyUrl: String, headers: HashMap<String, String> = HashMap(), redirectIntercept: Boolean,isSyncToCookie: Boolean): WebviewFragment? {
    val fragment = WebviewFragment()
    fragment.arguments = getBundle(keyUrl, headers, redirectIntercept)
    if (isSyncToCookie) {
    syncCookie(keyUrl, headers)
    }
    return fragment
    }

    private fun getBundle(url: String, headers: HashMap<String, String>, redirectIntercept:Boolean): Bundle? {
    val bundle = Bundle()
    bundle.putString(WebConstants.INTENT_TAG_URL, url)
    bundle.putSerializable(INFO_HEADERS, headers)
    bundle.putBoolean(REDIRECT_INTERCEPT, redirectIntercept)
    return bundle
    }


    /**
    * cookie同步到WebView
    *
    * @param url WebView要加载的url
    * @return true 同步cookie成功,false同步cookie失败
    * @Author JPH
    */
    private fun syncCookie(url: String?, map: Map<String, String?>): Boolean {
    val cookieManager = CookieManager.getInstance()
    for (key in map.keys) {
    cookieManager.setCookie(url, key + "=" + map[key])
    }
    val newCookie = cookieManager.getCookie(url)
    return !TextUtils.isEmpty(newCookie)
    }
    }

    }

    封装带WebView的Activity

    支持自定义标题布局,设置带WebView的Fragment并添加到Activity,按键处理,注册使用网页标题事件,网页Activity关闭事件

    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
    open class WebActivity : AppCompatActivity() {
    private var title: String? = null // 标题设置

    protected var webviewFragment: BaseWebviewFragment? = null
    // 标题布局可自定义
    @LayoutRes
    protected fun getLayoutTitle(): Int = R.layout.title_normal

    // 添加标题布局
    val titleView by lazy { LayoutInflater.from(this).inflate(getLayoutTitle(), fl_title) }

    companion object{
    // 启动入口
    fun startCommonWeb(context: Context, title: String?, url: String?,header: HashMap<String, String> = HashMap()) {
    val intent = Intent(context, WebActivity::class.java)
    intent.putExtra(WebConstants.INTENT_TAG_TITLE, title)
    intent.putExtra(WebConstants.INTENT_TAG_URL, url)
    intent.putExtra(WebConstants.INTENT_TAG_HEADERS, header)
    if (context is Service) {
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
    }
    context.startActivity(intent)
    }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    CommandsManager.getInstance().registerCommand(titleUpdateCommand)
    setContentView(R.layout.activity_common_web)
    // 注册标题刷新回调
    // 设置标题和url
    title = intent.getStringExtra(WebConstants.INTENT_TAG_TITLE)
    val url = intent.getStringExtra(WebConstants.INTENT_TAG_URL)?:""

    // 如果是默认布局设置标题
    if (getLayoutTitle() == R.layout.title_normal){
    val textView = titleView.findViewById(R.id.tv_title) as TextView
    textView.text = title
    }

    // 填充webviewFragment
    val fm = supportFragmentManager
    val transaction = fm.beginTransaction()

    webviewFragment = null
    val params = intent.extras?.getSerializable(WebConstants.INTENT_TAG_HEADERS) as HashMap<String, String>
    webviewFragment = WebviewFragment.newInstance(
    url,
    params,
    redirectIntercept = false,
    isSyncToCookie = true
    )
    transaction.replace(R.id.web_view_fragment, webviewFragment as Fragment).commit()
    }

    override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
    // 调用webviewFragment的按键回调
    if (webviewFragment != null && webviewFragment is BaseWebviewFragment && event != null) {
    val flag: Boolean = webviewFragment!!.onKeyDown(keyCode, event)
    if (flag) {
    return flag
    }
    }
    return super.onKeyDown(keyCode, event)
    }

    /**
    * 标题刷新命令
    */
    private val titleUpdateCommand: Command = object : Command {
    override fun name(): String {
    return WebConstants.COMMAND_UPDATE_TITLE
    }

    override fun exec(context: Context, params: ArrayMap<String, String>, resultBack: ResultBack) {
    if (params.containsKey(WebConstants.COMMAND_UPDATE_TITLE_PARAMS)) {
    if (getLayoutTitle() == R.layout.title_normal){
    val textView = titleView.findViewById(R.id.tv_title) as TextView
    textView.text = params[WebConstants.COMMAND_UPDATE_TITLE_PARAMS]
    }
    }
    }
    }

    /**
    * 默认title布局返回 回调
    * @param view View
    */
    fun back(view: View){
    finish()
    }

    override fun finish() {
    val map = ArrayMap<String, String>().apply {
    this[WebFinishCommand.FINISH_COMMAND_PARAMS] = "true"
    }
    CommandDispatcher.getInstance().execByOther(WebFinishCommand.FINISH_COMMAND, Gson().toJson(map))
    super.finish()
    }

    /**
    * 点击右侧button 回调
    * @param view View
    */
    fun clickRight(view: View){

    }
    }

    分发消息

  • 命令模式封装消息管理器,对当前分发的消息采用当前进程自行处理,或跨进程处理,根据传参判断是否需要回调给h5

1
2
3
4
5
6
7
8
9
10
11
/**
* 命令实现接口
*/
interface Command {


// 命令名称
fun name():String
// 执行命令传参和定义返回接口
fun exec(context: Context, map: ArrayMap<String, String>, resultBack: ResultBack)
}
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
/**
* 命令管理器
* @property instance CommandsManager?
* @property commands ArrayMap<String, Command>
*/
class CommandsManager private constructor(){
private var instance: CommandsManager? = null
// 命令缓存
val commands:ArrayMap<String, Command> = ArrayMap()

// 返回单例
companion object{
@Volatile private var instance:CommandsManager? = null
fun getInstance() = instance?: synchronized(this){
instance?:CommandsManager().also { instance = it }
}
}
/**
* 注册web进程处理的命令
* @param command Command
*/
fun registerCommand(command: Command) {
commands[command.name()] = command
}


/**
* 非UI线程执行
*/
fun execCommand(
context: Context,
action: String,
params: ArrayMap<String, String>?,
resultBack: ResultBack
) {
// 命令/传参不为空,执行命令逻辑
if (commands[action] != null && params != null) {
commands[action]?.exec(context, params, resultBack)
}
// 返回错误
else {
val aidlError =
AidlError(WebConstants.NO_METHOD, WebConstants.NO_METHOD_STR)
resultBack.onResult(WebConstants.FAILED, action, aidlError)
}
}


/**
* 判断命令是否存在
* @param action String?
* @return Boolean
*/
fun isCommandExist(action: String?): Boolean {
return commands[action] != null
}
}
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
class CommandDispatcher {
/**
* json解析对象
*/
private val gson by lazy { Gson() }

// 跨进程通信接口
protected var iHandleAction: IHandleAction? = null


/**
* 单例返回
*/
companion object {
@Volatile
private var instance: CommandDispatcher? = null

fun getInstance() = instance ?: synchronized(this) {
return instance ?: CommandDispatcher().also { instance = it }
}
}

/**
* 跨进程连接
* @param context Context?
*/
fun initAidlConnect(context: Context) {
// 避免重复初始化
if (iHandleAction != null) {
return
}
// 开启子线程,获取跨进程调用实例
Thread {
iHandleAction = IHandleAction.Stub.asInterface(
ProcessConnector.getInstance(context).getAidlInterface()
)
}.start()
}

/** 其它进程执行命令
* @param cmd String
* @param params String
*/
fun execByOther(cmd: String, params: String) {
Log.i("CommandDispatcher", "${Process.myPid()}进程调跨进程处理,command: $cmd params: $params")
try {
iHandleAction?.handleAction(cmd, params, object : ICallback.Stub() {
override fun onResult(
responseCode: Int,
actionName: String?,
response: String?
) {
// 当前进程回调结果
handleCallback(responseCode, actionName, response)
}
})
} catch (e: Exception) {
Log.e("CommandDispatcher", "Command exec error!!!!", e)
}
}

/**
* 当前进程执行命令
* @param context Context
* @param cmd String
* @param params String
* @param webView WebView
*/
fun execBySelf(context: Context, cmd: String, params: String, webView: WebView? = null) {
Log.i("CommandDispatcher", "${Process.myPid()}进程自己处理,command: $cmd params: $params")
try {
// json传参数据反序列化为map对象
val mapParams = gson.fromJson<ArrayMap<String, String>>(
params,
ArrayMap::class.java
)
// 命令管理器执行该命令逻辑
CommandsManager.getInstance()
.execCommand(context, cmd, mapParams, object : ResultBack {
// 结果回调
override fun onResult(status: Int, action: String, result: Any?) {
handleCallback(status, action, gson.toJson(result), webView)
}
})
} catch (e: Exception) {
Log.e("CommandDispatcher", "Command exec error!!!!", e)
}
}

private fun handleCallback(
responseCode: Int, actionName: String?, response: String?,
webView: WebView? = null
) {
Log.d(
"CommandDispatcher",
String.format(
"Callback result: responseCode= %s, action= %s, result= %s",
responseCode,
actionName,
response
)
)
runOnUiThread(Runnable {
// 返回结果反序列化为json
val params = Gson().fromJson<ArrayMap<String, String>>(response, ArrayMap::class.java)
// 从传参判断是否需要回调给h5
if (webView != null && params[WebConstants.NATIVE2WEB_CALLBACK] != null && !TextUtils.isEmpty(
params[WebConstants.NATIVE2WEB_CALLBACK].toString()
)
) {
// web进程调js
if (webView is BaseWebView) {
webView.handleCallback(response)
}
}
})
}
}
  • 进程连接管理类,绑定另一个服务进程,连接断开时解绑重连,返回跨进程通信接口
    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
    class ProcessConnector private constructor(context: Context) {
    private var mContext: Context = context.applicationContext
    // 跨进程通信接口
    private var iHandleAction: IHandleAction? = null
    private var mConnectBinderPoolCountDownLatch: CountDownLatch = CountDownLatch(1) // 个数为1的同步变量

    // 单例对象
    companion object {
    @Volatile
    private var instance: ProcessConnector? = null
    fun getInstance(context: Context) = instance ?: synchronized(this) {
    instance ?: ProcessConnector(context).also { instance = it }
    }
    }

    init {
    connectToOtherProcessService()
    }

    // 绑定另一个服务进程
    @Synchronized
    private fun connectToOtherProcessService() {
    val targetClass = if(!isMainProcess(mContext)) RemoteBindMainService::class.java else MainBindRemoteService::class.java
    // 绑定主进程服务
    mContext.bindService(Intent(mContext,targetClass), object : ServiceConnection {

    // 服务断开
    override fun onServiceDisconnected(name: ComponentName) {
    Log.v("ServiceConnect","跨进程连接断开")
    }

    // 服务连接
    override fun onServiceConnected(name: ComponentName, service: IBinder) {
    Log.v("ServiceConnect","跨进程连接成功")
    // 绑定断开回调
    iHandleAction = IHandleAction.Stub.asInterface(service)
    try {
    iHandleAction?.asBinder()?.linkToDeath(object : IBinder.DeathRecipient {
    // 解绑重连
    override fun binderDied() {
    iHandleAction?.asBinder()?.unlinkToDeath(this, 0)
    iHandleAction = null
    connectToOtherProcessService()
    }
    }, 0)
    } catch (e: RemoteException) {
    e.printStackTrace()
    }
    mConnectBinderPoolCountDownLatch.countDown()
    }
    }, Context.BIND_AUTO_CREATE)

    // 线程同步阻塞
    try {
    mConnectBinderPoolCountDownLatch.await()
    } catch (e: InterruptedException) {
    e.printStackTrace()
    }
    }


    // 返回跨进程调用接口
    fun getAidlInterface(): IBinder? {
    return iHandleAction?.asBinder()
    }

    }

    跨进程通信

    跨进程接口定义

    定义AIDL跨进程调用接口
    1
    2
    3
    interface IHandleAction {
    void handleAction(String actionName, String jsonParams, in ICallback callback);
    }
    定义AIDL跨进程调用回调接口
    1
    2
    3
    interface ICallback {
    void onResult(int responseCode, String actionName, String response);
    }

    跨进程接口实现

    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
    class AidlInterface (val context: Context): IHandleAction.Stub() {
    private val gson: Gson by lazy { Gson() }
    override fun handleAction(
    actionName: String?,
    jsonParams: String?,
    callback: ICallback?
    ) {
    Log.v("AidlInterface","${Process.myPid()}进程正在跨进程执行命令$actionName")
    if (actionName != null){
    // 跨进程处理命令
    CommandsManager.getInstance().execCommand(context, actionName,
    gson.fromJson(jsonParams, ArrayMap::class.java) as? ArrayMap<String, String>, object : ResultBack {
    override fun onResult(status: Int, action: String, result: Any?) {
    try {
    // 原进程处理回调
    callback?.onResult(status, actionName, Gson().toJson(result))
    } catch (e: Exception) {
    e.printStackTrace()
    }
    }

    })
    }
    }

    }

    声明主进程服务和Web进程服务

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    /**
    * 主进程绑定web进程
    */
    class MainBindRemoteService : Service() {
    override fun onBind(intent: Intent?): IBinder? {
    val pid = Process.myPid()
    Log.d(
    "MainBindRemoteService", String.format(
    "web进程: %s",
    "当前进程ID为:$pid---主进程连接web进程成功"
    )
    )
    // web进程操作对象
    return AidlInterface(this)
    }

    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    /**
    * web绑定主进程服务
    */
    class RemoteBindMainService: Service() {
    override fun onBind(intent: Intent?): IBinder? {
    val pid = Process.myPid()
    Log.d(
    "RemoteBindMainService", String.format(
    "主进程: %s",
    "当前进程ID为:$pid----web连接主进程成功"
    )
    )
    // 主进程操作对象
    return AidlInterface(this)
    }
    }
    1
    2
    3
    <service android:name="com.example.weblib.service.MainBindRemoteService"
    android:process=":remoteweb"/>
    <service android:name="com.example.weblib.service.RemoteBindMainService" />
    最后利用ContentProvider初始化封装的SDK,兼容>=Android 9.0 不同进程中使用Webview需要配置不同的缓存目录
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class WebInitializer : ContentProvider() {
    override fun onCreate(): Boolean {
    Log.v("SdkInitializer","WebLib初始化进程:${getProcessName(context!!)}")
    if (context == null) return true
    // >=Android 9.0 在不同进程中使用Webview需要配置缓存目录,配置WebLib进程使用的Webview缓存目录
    if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
    WebView.setDataDirectorySuffix("remote")
    }
    return true
    }
    ...
    }
    1
    2
    3
    4
    5
    6
    <provider
    android:authorities="${applicationId}.library-installer"
    android:name="com.example.weblib.WebInitializer"
    android:multiprocess="true"
    android:process=":remoteweb"
    android:exported="false"/>


最近在做性能优化的东西,研究了一下相关的内存监测,卡顿监测,以及ANR监测开源框架,对里面的核心原理做了总结,并手写一份简易版以便加深印象,为后续搭建线上日志监控做铺垫,在此做一个记录,线上监控框架可根据业务在此基础上做扩展。收集必要的日志信息,排查问题及时修复BUG,提升性能和稳定性,也是每个Android工程师必不可少的技能。


BlockCanary

  1. 通过采样工具类,在子线程中获取主线程的堆栈信息并保存到Map,暴露一个接口返回采样结果
  2. 通过日志监控类,实现Printer接口,判断是否卡顿(采样开始到采样结束的时间间隔是否超过阈值),如果卡顿调用接口获取返回的采样结果在子线程中日志打印
  3. App出现卡顿,会阻塞主线程的dispatchMessage,主线程Looper的loop方法中有一个Printer在每个Message处理前后被调用,所以设置主线程的MessageLogging为自定义的Printer

    采样工具类

    开启一个采样子线程,设置原子变量记录本次是否采样保证多线程同步,避免重复开始和结束,开始采样post一个采样任务到子线程,存入当前时间戳和对应的主线程堆栈信息到Map中,如果本次仍然需采样继续延迟间隔时间执行采样任务,暴露一个获取主线程堆栈信息的接口方法,返回当前堆栈信息列表
    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
    public class StackSampler {
    public static final String SEPARATOR = "\r\n";
    public static final SimpleDateFormat TIME_FORMATTER =
    new SimpleDateFormat("MM-dd HH:mm:ss.SSS");
    private Handler mHandler;
    private Map<Long, String> mStackMap = new LinkedHashMap<>(); //保存的主线程堆栈信息
    private int mMaxCount = 100; // 最多保存100条
    private long mSampleInterval; // 采样时间间隔
    // 本次是否采样
    protected AtomicBoolean mShouldSample = new AtomicBoolean(false);

    public StackSampler(long sampleInterval) {
    mSampleInterval = sampleInterval;
    // 开启采样子线程
    HandlerThread handlerThread = new HandlerThread("block-canary-sampler");
    handlerThread.start();
    mHandler = new Handler(handlerThread.getLooper());
    }

    /**
    * 开始采样 执行堆栈
    */
    public void startDump() {
    // 避免重复开始
    if (mShouldSample.get()) {
    return;
    }
    // 设置采样标记
    mShouldSample.set(true);
    // 移除上一个采样任务,在采样间隔时间后执行采样
    mHandler.removeCallbacks(mRunnable);
    mHandler.postDelayed(mRunnable, mSampleInterval);
    }

    public void stopDump() {
    // 避免重复结束
    if (!mShouldSample.get()) {
    return;
    }
    // 设置采样标记
    mShouldSample.set(false);

    mHandler.removeCallbacks(mRunnable);
    }


    public List<String> getStacks(long startTime, long endTime) {
    ArrayList<String> result = new ArrayList<>();
    synchronized (mStackMap) {
    for (Long entryTime : mStackMap.keySet()) {
    // 记录时间大于开始时间小于结束时间就放入返回列表中
    if (startTime < entryTime && entryTime < endTime) {
    result.add(TIME_FORMATTER.format(entryTime)
    + SEPARATOR
    + SEPARATOR
    + mStackMap.get(entryTime));
    }
    }
    }
    return result;
    }

    private Runnable mRunnable = new Runnable() {
    @Override
    public void run() {
    StringBuilder sb = new StringBuilder();
    // 获得主线程堆栈信息并拼接到字符串
    StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace();
    for (StackTraceElement s : stackTrace) {
    sb.append(s.toString()).append("\n");
    }
    synchronized (mStackMap) {
    //最多保存100条堆栈信息,到了数量上限移除
    if (mStackMap.size() == mMaxCount) {
    mStackMap.remove(mStackMap.keySet().iterator().next());
    }
    // 存入当前时间戳和对应的堆栈信息
    mStackMap.put(System.currentTimeMillis(), sb.toString());
    }
    // 如果本次要采样,设置延迟继续执行此任务
    if (mShouldSample.get()) {
    mHandler.postDelayed(mRunnable, mSampleInterval);
    }
    }
    };

    }

    卡顿监控工具类

    初始化采样工具类,开启打印日志子线程,记录采样的开始时间和结束时间,如果时间大于设定的卡顿阈值则判定为卡顿状态,调用采样工具类获取主线程堆栈信息
    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
    // 卡顿监控工具类
    public class LogMonitor implements Printer {

    private StackSampler mStackSampler; // 采样工具类
    private boolean mPrintingStarted = false; // 开始打印标记
    private long mStartTimestamp; // 开始时间戳
    private long mBlockThresholdMillis = 3000; // 卡顿阈值
    private long mSampleInterval = 1000; // 采样频率

    private Handler mLogHandler;

    public LogMonitor() {
    // 初始化采样工具类
    mStackSampler = new StackSampler(mSampleInterval);
    // 开启打印子线程
    HandlerThread handlerThread = new HandlerThread("block-canary-io");
    handlerThread.start();
    mLogHandler = new Handler(handlerThread.getLooper());
    }

    @Override
    public void println(String x) {
    //从if到else会执行消息分发,如果执行耗时超过阈值,输出卡顿信息
    if (!mPrintingStarted) {
    //记录开始时间
    mStartTimestamp = System.currentTimeMillis();
    mPrintingStarted = true;
    mStackSampler.startDump();
    } else {
    final long endTime = System.currentTimeMillis();
    mPrintingStarted = false;
    //出现卡顿,通知卡顿事件
    if (isBlock(endTime)) {
    notifyBlockEvent(endTime);
    }
    mStackSampler.stopDump();
    }
    }

    private void notifyBlockEvent(final long endTime) {
    // 获得卡顿时主线程堆栈信息在子线程打印
    mLogHandler.post(new Runnable() {
    @Override
    public void run() {
    List<String> stacks = mStackSampler.getStacks(mStartTimestamp, endTime);
    for (String stack : stacks) {
    Log.e("block-canary", stack);
    }
    }
    });
    }


    private boolean isBlock(long endTime) {
    return endTime - mStartTimestamp > mBlockThresholdMillis;
    }
    }

    卡顿监控入口

    设置主线程的MessageLogging为自定义的Printer
    1
    2
    3
    4
    5
    6
    public class BlockCanary {
    public static void install() {
    LogMonitor logMonitor = new LogMonitor();
    Looper.getMainLooper().setMessageLogging(logMonitor);
    }
    }

LeakCanary

Java中WeakReference和ReferenceQueue联合使用是监控某个对象是否被gc回收的手段,LeakCanary正是利用这个原理实现的。

WeakReference和ReferenceQueue联合使用

创建一个对象,包装到弱引用对象中并关联引用队列,把对象置空,强制GC,取出引用队列的弱引用对象是否与关联时的弱引用对象相同

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
public static void main(String[] args) {
// 创建一个引用队列
ReferenceQueue referenceQueue = new ReferenceQueue();
// 创建一个对象
Object obj = new Object();

//把obj放入弱引用对象,并和一个引用队列关联
//当obj被gc回收后,weakReference会被添加到与之关联的referenceQueue
WeakReference weakReference = new WeakReference(obj,referenceQueue);

//把obj置空,让它没有强引用
obj = null;
Runtime.getRuntime().gc(); //强制gc

try{
Thread.sleep(1000);
}catch (Exception e){}

Reference findRef = null;
do{
findRef = referenceQueue.poll();
//如果能找到上面的weakReference对象,说明obj被gc回收了
System.out.println("findRef = " +findRef + "是否等于上面的weakReference = " + (findRef == weakReference));
}while(findRef !=null);// 把所有referenceQueue的weakReference对象找出来
}

封装包含key和name的弱引用类

继承WeakReference弱引用类,方便根据key删除对象

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 KeyWeakReference<T> extends WeakReference<T> {

private String key;
private String name;

public KeyWeakReference(T referent, String key, String name) {
super(referent);
this.key = key;
this.name = name;
}

public KeyWeakReference(T referent, ReferenceQueue<? super T> q, String key, String name) {
super(referent, q);
this.key = key;
this.name = name;
}

public String getKey() {
return key;
}

public void setKey(String key) {
this.key = key;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

@Override
public String toString() {
final StringBuffer sb = new StringBuffer("KeyWeakReference{");
sb.append("key='").append(key).append('\'');
sb.append(", name='").append(name).append('\'');
sb.append('}');
return sb.toString();
}
}

监控工具类

创建一个观察对象Map,怀疑对象Map和引用队列,先清理一遍被GC的对象,遍历引用队列的所有弱引用对象,清理观察对象Map和怀疑对象Map中对应的对象,被监控的对象生成UUID作为key,把对象放入弱引用并与引用队列关联,放入到观察对象Map中,5秒后判断该对象是否还存在观察对象Map中,还存在则说明没有被GC,将该对象移动到怀疑对象Map

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
public class Watcher {
// 观察对象Map
private HashMap<String, KeyWeakReference> watchedReferences = new HashMap<>();
// 怀疑对象Map
private HashMap<String, KeyWeakReference> retainedReferences = new HashMap<>();

//当被监视的对象被gc回收后,对象的弱引用就会被加入到引用队列
private ReferenceQueue queue = new ReferenceQueue();

public Watcher() {
}

/**
* 取出引用队列的所有弱引用对象,清理观察对象Map和怀疑对象Map中对应的对象
*/
private void removeWeaklyReachableReferences() {
KeyWeakReference findRef = null;
do {
// 取出引用队列的弱引用对象
findRef = (KeyWeakReference) queue.poll();
// 不为空说明对象被gc回收了,把对应的弱引用对象从观察对象Map,怀疑对象Map移除
if (findRef != null) {
// 根据key把它从观察对象Map移除
Reference removedRef = watchedReferences.remove(findRef.getKey());
// 如果removedRef为空,有可能被放入到怀疑对象Map了
// 尝试从怀疑对象Map中移除
if (removedRef == null) {
retainedReferences.remove(findRef.getKey());
}
}
} while (findRef != null);// 把referenceQueue的所有弱引用取出来
}

/**
* 根据key把对应的弱引用对象从观察对象Map移动到怀疑对象Map
*
* @param key
*/
private synchronized void moveToRetained(String key) {
System.out.println("加入到怀疑列表...");
// 加入怀疑对象Map前,做一次清理
removeWeaklyReachableReferences();
// 根据key从观察对象Map中去找弱引用对象
KeyWeakReference retainedRef = watchedReferences.remove(key);
// 发现还没有被删除,说明没有被回收
if (retainedRef != null) {
//从观察对象Map中移除,加入到怀疑对象Map
retainedReferences.put(key, retainedRef);
}
}


public void watch(Object watchedReference, String referenceName) {
//1. 先清理下观察对象Map和怀疑对象Map
removeWeaklyReachableReferences();
//2. 被监视的对象生成唯一的uuid作为key
final String key = UUID.randomUUID().toString();
//3. 被监视的对象放入weakReference,并和一个引用队列关联
KeyWeakReference reference = new KeyWeakReference(watchedReference, queue, key, "");
//4. 加入到观察对象Map
watchedReferences.put(key, reference);

//5. 延迟5秒后检查是否还在观察对象Map,如果还在,则加入到怀疑对象Map
Executor executor = Executors.newSingleThreadExecutor();
executor.execute(() -> {
Utils.sleep(5000);
moveToRetained(key);
});

}

// 获取泄漏对象Map
public HashMap<String, KeyWeakReference> getRetainedReferences() {
retainedReferences.forEach((key, keyWeakReference) -> {
System.out.println("key: " + key + " , obj: " + keyWeakReference.get() + " , keyWeakReference: " + keyWeakReference);
}
);
return retainedReferences;
}
}

监控泄漏对象

创建对象,调用监控工具类的监控方法,查看怀疑对象Map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) {
// 初始化监控工具类
Watcher watcher = new Watcher();
// 创建对象并开始对象监控
Object obj = new Object();
watcher.watch(obj,"");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 释放对象
obj = null;
// 强制GC
Runtime.getRuntime().gc();
sleep(100);
System.runFinalization();

System.out.println("查看是否在怀疑对象Map:" + watcher.getRetainedReferences().size());
}

ANRWatchDog

FileObserver

监控Android系统的anr日志目录/data/anr/,利用FileObserver监控目录下的文件操作,间接监控ANR问题

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
public class ANRFileObserver extends FileObserver {

public ANRFileObserver(String path) {
super(path);
}

public ANRFileObserver(String path, int mask) {
super(path, mask);
}

@Override
public void onEvent(int event, @Nullable String path) {
switch (event)
{
case FileObserver.ACCESS://文件被访问
Log.i("Zero", "ACCESS: " + path);
break;
case FileObserver.ATTRIB://文件属性被修改,如 chmod、chown、touch 等
Log.i("Zero", "ATTRIB: " + path);
break;
case FileObserver.CLOSE_NOWRITE://不可写文件被 close
Log.i("Zero", "CLOSE_NOWRITE: " + path);
break;
case FileObserver.CLOSE_WRITE://可写文件被 close
Log.i("Zero", "CLOSE_WRITE: " + path);
break;
case FileObserver.CREATE://创建新文件
Log.i("Zero", "CREATE: " + path);
break;
case FileObserver.DELETE:// 文件被删除,如 rm
Log.i("Zero", "DELETE: " + path);
break;
case FileObserver.DELETE_SELF:// 自删除,即一个可执行文件在执行时删除自己
Log.i("Zero", "DELETE_SELF: " + path);
break;
case FileObserver.MODIFY://文件被修改
Log.i("Zero", "MODIFY: " + path);
break;
case FileObserver.MOVE_SELF://自移动,即一个可执行文件在执行时移动自己
Log.i("Zero", "MOVE_SELF: " + path);
break;
case FileObserver.MOVED_FROM://文件被移走,如 mv
Log.i("Zero", "MOVED_FROM: " + path);
break;
case FileObserver.MOVED_TO://文件被移来,如 mv、cp
Log.i("Zero", "MOVED_TO: " + path);
break;
case FileObserver.OPEN://文件被 open
Log.i("Zero", "OPEN: " + path);
break;
default:
//CLOSE : 文件被关闭,等同于(IN_CLOSE_WRITE | IN_CLOSE_NOWRITE)
//ALL_EVENTS : 包括上面的所有事件
Log.i("Zero", "DEFAULT(" + event + "): " + path);
break;
}
}
}

线程

开启ANR监控后台线程,检查是否ANR,通过检查标志位和时间差判断规定时间内是否执行完成,如果没执行完成则可能发生ANR卡住了,那么打印主线程堆栈信息并调用回调接口

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
public class ANRWatchDog extends Thread {

private static final String TAG = "ANR";
private int timeout = 5000; // 超时阈值
private boolean ignoreDebugger = true; // 开启开关

static ANRWatchDog sWatchdog;

private Handler mainHandler = new Handler(Looper.getMainLooper());

private class ANRChecker implements Runnable {

private boolean mCompleted; // 是否完成
private long mStartTime; // 开始时间
private long executeTime = SystemClock.uptimeMillis(); // 执行时间

@Override
public void run() {
synchronized (ANRWatchDog.this) {
mCompleted = true;
executeTime = SystemClock.uptimeMillis();
}
}

void schedule() {
// 设置是否完成标记
mCompleted = false;
// 记录开始时间
mStartTime = SystemClock.uptimeMillis();
// 结束任务,在主线程中重置完成标记,记录结束时间
mainHandler.postAtFrontOfQueue(this);
}

boolean isBlocked() {
return !mCompleted || executeTime - mStartTime >= timeout;
}
}

// anr监听接口
public interface ANRListener {
void onAnrHappened(String stackTraceInfo);
}

private ANRChecker anrChecker = new ANRChecker();

private ANRListener anrListener;


public void addANRListener(ANRListener listener){
this.anrListener = listener;
}

public static ANRWatchDog getInstance(){
if(sWatchdog == null){
sWatchdog = new ANRWatchDog();
}
return sWatchdog;
}

private ANRWatchDog(){
super("ANR-WatchDog-Thread");
}

@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
@Override
public void run() {
// 设置为后台线程
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
while(true){
// 没有被打断停止
while (!isInterrupted()) {
synchronized (this) {
// 检查是否anr
anrChecker.schedule();
long waitTime = timeout;
long start = SystemClock.uptimeMillis();
// 防止假唤醒
while (waitTime > 0) {
try {
// 等待超时时长后检查标志位
wait(waitTime);
} catch (InterruptedException e) {
Log.w(TAG, e.toString());
}
// 发现没有到阈值继续休眠
waitTime = timeout - (SystemClock.uptimeMillis() - start);
}
// 如果没有阻塞跳过此次循环
if (!anrChecker.isBlocked()) {
continue;
}
}
// 如果开关关闭并且在调试跳过此次循环
if (!ignoreDebugger && Debug.isDebuggerConnected()) {
continue;
}
// 打印堆栈信息并回调
String stackTraceInfo = getStackTraceInfo();
if (anrListener != null) {
anrListener.onAnrHappened(stackTraceInfo);
}
}
anrListener = null;
}
}

// 获取主线程堆栈信息返回字符串
private String getStackTraceInfo() {
StringBuilder stringBuilder = new StringBuilder();
for (StackTraceElement stackTraceElement : Looper.getMainLooper().getThread().getStackTrace()) {
stringBuilder
.append(stackTraceElement.toString())
.append("\r\n");
}
return stringBuilder.toString();
}
}

ANRWatchDog.getInstance().addANRListener(new ANRWatchDog.ANRListener() {
@Override
public void onAnrHappened(String stackTraceInfo) {
Log.i(TAG, "发生了ANR: "+ stackTraceInfo);
}
});
ANRWatchDog.getInstance().start();


最近公司的项目做性能优化,正在折腾Fragment懒加载的东西,由于项目迁移到了AndroidX,懒加载方案和以前有所不同,不过总体来说代码量少了很多,正好可以对新老版本的懒加载方案做一个对比,下面是关于AndroidX前和AndroidX后的fragment懒加载方案总结


AndroidX之前采用旧懒加载方案

4步优化

  1. View已加载且fragment可见时懒加载
  • 当onViewCreated()方法执行时,表明View已经加载完毕,isViewCreated标记为true,并调lazyLoad()方法
  • 当setUserVisibleHint(boolean isVisibleToUser)执行时,isVisibleToUser为true并调lazyLoad()方法
  • 在lazyLoad()方法双重标记判断,再进行停止一切/加载数据事件分发
  • 定义抽象方法loadData(),子类重写进行加载数据
    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
    private boolean isViewCreated = false;
    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    ...
    isViewCreated = true; // View已加载
    // 此时正好可见
    if (getUserVisibleHint()) {
    setUserVisibleHint(true);
    }

    return rootView;
    }

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
    super.setUserVisibleHint(isVisibleToUser);
    // View已加载
    if (isViewCreated) {
    // 根据当前可见状态分发事件
    if (isVisibleToUser) {
    dispatchUserVisibleHint(true);
    } else {
    dispatchUserVisibleHint(false);
    }
    }
    }

    private void dispatchUserVisibleHint(boolean visibleState) {
    if (visibleState) {
    // 加载网络数据请求
    onFragmentLoad();
    } else {
    // 停止网络数据请求
    onFragmentLoadStop();
    }
    }

    public void onFragmentLoadStop() {
    E("onFragmentLoadStop");
    }

    public void onFragmentLoad() {
    E("onFragmentLoad");
    }
  1. 记录上一次状态与当前状态比较,保证变化过程,状态未改变不需要分发事件
  • 从可见到不可见,停止一切操作
  • 从不可见到可见,加载数据操作
    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
    private boolean isVisibleStateUP = false; // 记录上一次可见的状态
    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
    super.setUserVisibleHint(isVisibleToUser);

    if (isViewCreated) {
    // 当前可见且上一次不可见
    if (isVisibleToUser && !isVisibleStateUP) {
    dispatchUserVisibleHint(true);
    }
    // 当前不可见且上一次可见
    else if (!isVisibleToUser && isVisibleStateUP){
    dispatchUserVisibleHint(false);
    }

    }
    }

    private void dispatchUserVisibleHint(boolean visibleState) {
    this.isVisibleStateUP = visibleState;
    if (visibleState) {
    // 加载网络数据请求
    onFragmentLoad();
    } else {
    // 停止网络数据请求
    onFragmentLoadStop();
    }
    }
  1. 启动新的Activity没有分发事件
  • 除了onCreate和setUserVisibleHint在onResume/onPause中也要分发事件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @Override
    public void onResume() {
    super.onResume();
    // 当前可见且上一次不可见
    if (getUserVisibleHint() && !isVisibleStateUP) {
    dispatchUserVisibleHint(true);
    }
    }

    @Override
    public void onPause() {
    super.onPause();
    // 当前不可见且上一次可见
    if (getUserVisibleHint() && isVisibleStateUP) {
    dispatchUserVisibleHint(false);
    }
    }
  1. 双重嵌套下子Fragment无法接收到事件
    父fragment不可见直接返回,判断当前fragment是否可见,手动遍历分发每个子fragment停止一切操作/加载数据事件
    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
    // 判断父fragment是否可见 
    private boolean isParentInvisible() {
    // 获取父fragment
    Fragment parentFragment = getParentFragment();
    // 父fragment是懒加载fragment的实例
    if (parentFragment instanceof LazyFragment) {
    LazyFragment fragment = (LazyFragment) parentFragment;
    // 父fragment不可见
    return !fragment.isVisibleStateUP;
    }
    return false;
    }

    private void dispatchUserVisibleHint(boolean visibleState) {
    this.isVisibleStateUP = visibleState;
    // 当前fragment可见且父fragment不可见,不分发事件
    if (visibleState && isParentInvisible()) {
    return;
    }

    if (visibleState) {
    onFragmentLoad();
    // 分发子fragment可见事件
    dispatchChildVisibleState(true);

    } else {
    onFragmentLoadStop();
    // 分发子fragment不可见事件
    dispatchChildVisibleState(false);
    }
    }

    protected void dispatchChildVisibleState(boolean state) {
    FragmentManager fragmentManager = getChildFragmentManager();
    List<Fragment> fragments = fragmentManager.getFragments();
    if (fragments != null) {
    // 遍历子Fragment分发事件
    for (Fragment fragment: fragments) {
    // fragment是懒加载实例未被隐藏且可见
    if (fragment instanceof LazyFragment &&
    !fragment.isHidden() &&
    fragment.getUserVisibleHint()) {
    ((LazyFragment5)fragment).dispatchUserVisibleHint(state);
    }
    }
    }
    }

    AndroidX之后采用新懒加载方案

    Google在Androidx在FragmentTransaction中增加了setMaxLifecycle方法控制Fragment 调用的最大的生命周期函数。该方法可以设置活跃状态下Fragment最大状态,如果该Fragment 超过了设置的最大状态,会强制将Fragment降级到正确的状态

    viewPager下设置adapter的behavior

    BEHAVIOR_SET_USER_VISIBLE_HINT:Fragment对用户可见状态发生改变时,setUserVisibleHint方法会被调用。
    BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT:当前选中的Fragment在Lifecycle.State#RESUMED状态,其他不可见的 Fragment限制在Lifecycle.State#STARTED状态
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    open class FragmentLazyPagerAdapter(
    fragmentManager: FragmentManager,
    private val fragments: MutableList<Fragment>,
    private val titles: MutableList<String>
    ) : FragmentPagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {

    override fun getItem(position: Int) = fragments[position]

    override fun getCount() = fragments.size

    override fun getPageTitle(position: Int) = titles[position]

    }

    封装懒加载fragment

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    abstract class LazyFragment : Fragment() {

    // 是否加载过标记
    private var isLoaded = false

    override fun onResume() {
    super.onResume()
    if (!isLoaded) {
    lazyInit()
    Log.d(TAG, "lazyInit:!!!!!!!")
    isLoaded = true
    }
    }

    override fun onDestroyView() {
    super.onDestroyView()
    isLoaded = false
    }

    abstract fun lazyInit()
    }

    add/show/hide时设置最大生命周期(未使用viewPager)

    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
    // 初始化fragment
    private fun loadFragmentsTransaction(
    @IdRes containerViewId: Int,
    showPosition: Int,
    fragmentManager: FragmentManager,
    vararg fragments: Fragment
    ) {
    if (fragments.isNotEmpty()) {
    fragmentManager.beginTransaction().apply {
    for (index in fragments.indices) {
    val fragment = fragments[index]
    add(containerViewId, fragment, fragment.javaClass.name)
    if (showPosition == index) {
    setMaxLifecycle(fragment, Lifecycle.State.RESUMED)
    } else {
    hide(fragment)
    setMaxLifecycle(fragment, Lifecycle.State.STARTED)
    }
    }

    }.commit()
    } else {
    throw IllegalStateException(
    "fragments must not empty"
    )
    }
    }

    // 展示/隐藏fragment
    private fun showHideFragmentTransaction(fragmentManager: FragmentManager, showFragment: Fragment) {
    fragmentManager.beginTransaction().apply {
    show(showFragment)
    setMaxLifecycle(showFragment, Lifecycle.State.RESUMED)

    //获取其中所有的fragment,其他的fragment进行隐藏
    val fragments = fragmentManager.fragments
    for (fragment in fragments) {
    if (fragment != showFragment) {
    hide(fragment)
    setMaxLifecycle(fragment, Lifecycle.State.STARTED)
    }
    }
    }.commit()
    }

    Fragment嵌套下的问题

    第一次初始化时,同级不可见的Fragment仍然要调生命周期方法,需增加Fragment是否可见的判断
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    abstract class LazyFragment : Fragment() {

    private var isLoaded = false

    override fun onResume() {
    super.onResume()
    //增加Fragment是否可见的判断
    if (!isLoaded && !isHidden) {
    lazyInit()
    Log.d(TAG, "lazyInit:!!!!!!!")
    isLoaded = true
    }
    }

    override fun onDestroyView() {
    super.onDestroyView()
    isLoaded = false
    }

    abstract fun lazyInit()

    }

    ViewPager2懒加载

    最新的ViewPager2默认就实现了懒加载,可以说不用任何处理

但是ViewPager2中的RecyclerView可以缓存Fragment的数量是有限的,会造成Fragment的多次销毁和创建,也可通过setOffscreenPageLimit()方法设置预加载数量,再用AndroidX下的懒加载fragment方式去处理


加密技术这块不仅涉及到很多JAVA基础,加密技术还涉及到很多Android底层知识,JAVA反射,JAVA IO,apk的启动流程,类的加载机制,dex文件的构造,APK打包的过程,而这些东西又正好是面试的常考点,是深入学习Android的必经之路,这次从原理入手,手写一个简单的加固框架,在这里做一个记录,如果有不对的地方欢迎指出和交流。


加固的主要目的是为了防止反编译,代码遭到阅读和窃取甚至重新打包上架的事情发生,那反编译的过程是什么呢?

  1. zip解压apk
  2. dex2jar把class.dex转成jar包
  3. jd-gui看class文件源码

加固的原理

所以加固的关键是对dex文件用加密算法进行加密,防止可执行部分的源码被阅读,此时就需要一个壳程序负责解密原dex文件,然后再合并原dex和壳dex重新签名打包成新的apk,运行时壳程序解密,获得原dex重新手动类加载

apk的打包流程

  1. APT工具处理资源文件(xml资源如布局、AndroidManifest),生成R.java
  2. AIDL工具处理AIDL文件,生成相应的Java文件
  3. Javac工具编译Java,生成Class文件
  4. DX工具将Class文件转换成DEX文件
  5. ApkBuilder工具将资源文件和DEX文件打包成APK
  6. KeyStore签名APK
  7. 正式版APK用ZipAlign工具对齐

实现加固

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
public static void main(String[] args) throws Exception {

byte[] mainDexData; // 存储源apk中的源dex文件
byte[] aarData; // 存储壳中的壳dex文件
byte[] mergeDex; // 存储壳dex 和源dex 的合并的新dex文件

// 删除source/apk/temp目录下所有文件
File tempFileApk = new File("source/apk/temp");
if (tempFileApk.exists()) {
File[]files = tempFileApk.listFiles();
for(File file: files){
if (file.isFile()) {
file.delete();
}
}
}
// 删除source/aar/temp目录下所有文件
File tempFileAar = new File("source/aar/temp");
if (tempFileAar.exists()) {
File[]files = tempFileAar.listFiles();
for(File file: files){
if (file.isFile()) {
file.delete();
}
}
}

//第一步 处理原始apk 加密dex
AES.init(AES.DEFAULT_PWD);
//待加固的apk
File apkFile = new File("source/apk/app-debug.apk");
//创建临时文件夹
File newApkFile = new File(apkFile.getParent() + File.separator + "temp");
if(!newApkFile.exists()) {
newApkFile.mkdirs();
}
//加密apk文件并写入到临时文件夹获取主dex
File mainDexFile = AES.encryptAPKFile(apkFile,newApkFile);
//临时文件夹存在,重命名dex文件
if (newApkFile.isDirectory()) {
File[] listFiles = newApkFile.listFiles();
for (File file : listFiles) {
if (file.isFile()) {
if (file.getName().endsWith(".dex")) {
String name = file.getName();
System.out.println("rename step1:"+name);
int cursor = name.indexOf(".dex");
String newName = file.getParent()+ File.separator + name.substring(0, cursor) + "_" + ".dex";
System.out.println("rename step2:"+newName);
file.renameTo(new File(newName));
}
}
}
}

// 第二步 处理aar 获得壳dex,其实这就是一个解密程序
File aarFile = new File("source/aar/mylibrary-debug.aar");
// jar包转dex文件
File aarDex = Dx.jar2Dex(aarFile);
//读取dex文件为byte数组
aarData = Utils.getBytes(aarDex);
// 创建一个classes.dex文件
File tempMainDex = new File(newApkFile.getPath() + File.separator + "classes.dex");
if (!tempMainDex.exists()) {
tempMainDex.createNewFile();
}
// 写入byte数组到classes.dex文件
FileOutputStream fos = new FileOutputStream(tempMainDex);
byte[] fbytes = Utils.getBytes(aarDex);
fos.write(fbytes);
fos.flush();
fos.close();


/**
* 第三步 打包签名
*/
// 创建未签名apk的文件夹
File unsignedApk = new File("result/apk-unsigned.apk");
unsignedApk.getParentFile().mkdirs();
// 合并壳dex和加密dex,压缩newApkFile中的文件为unsignedApk
Zip.zip(newApkFile, unsignedApk);
// 对unsignedApk文件签名输出签名后的文件apk-signed.apk
File signedApk = new File("result/apk-signed.apk");
Signature.signature(unsignedApk, signedApk);
}
}

Zip压缩工具类

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
public class Zip {

// 解压文件到目标文件夹
public static void unZip(File zip, File dir) {
try {
// 删除已存在的目标文件夹
dir.delete();
// 包装成压缩文件对象
ZipFile zipFile = new ZipFile(zip);
// 获取被压缩的所有文件
Enumeration<? extends ZipEntry> entries = zipFile.entries();
// 遍历被压缩的文件
while (entries.hasMoreElements()) {
ZipEntry zipEntry = entries.nextElement();
String name = zipEntry.getName();
// 如果是META-INF/CERT.RSA,META-INF/CERT.SF,META-INF/MANIFEST.MF文件就跳过
if (name.equals("META-INF/CERT.RSA") || name.equals("META-INF/CERT.SF") || name
.equals("META-INF/MANIFEST.MF")) {
continue;
}
// 如果当前压缩文件不是一个文件夹,就输出到目标文件夹
if (!zipEntry.isDirectory()) {
File file = new File(dir, name);
if (!file.getParentFile().exists()) file.getParentFile().mkdirs();
FileOutputStream fos = new FileOutputStream(file);
InputStream is = zipFile.getInputStream(zipEntry);
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
is.close();
fos.close();
}
}
zipFile.close();
} catch (Exception e) {
e.printStackTrace();
}
}

public static void zip(File dir, File zip) throws Exception {
// 删除已存在的压缩文件
zip.delete();
// 压缩文件并对输出文件做CRC32校验
CheckedOutputStream cos = new CheckedOutputStream(new FileOutputStream(
zip), new CRC32());
ZipOutputStream zos = new ZipOutputStream(cos);
compress(dir, zos, "");
zos.flush();
zos.close();
}

private static void compress(File srcFile, ZipOutputStream zos,
String basePath) throws Exception {
if (srcFile.isDirectory()) {
compressDir(srcFile, zos, basePath);
} else {
compressFile(srcFile, zos, basePath);
}
}

private static void compressDir(File dir, ZipOutputStream zos,
String basePath) throws Exception {
File[] files = dir.listFiles();
// 文件夹为空,构建空目录
if (files.length < 1) {
ZipEntry entry = new ZipEntry(basePath + dir.getName() + "/");
zos.putNextEntry(entry);
zos.closeEntry();
}
// 递归压缩
for (File file : files) {
compress(file, zos, basePath + dir.getName() + "/");
}
}

private static void compressFile(File file, ZipOutputStream zos, String dir)
throws Exception {
// 当前文件路径
String dirName = dir + file.getName();
// 文件新名称拼接
String[] dirNameNew = dirName.split("/");
StringBuffer buffer = new StringBuffer();

if (dirNameNew.length > 1) {
for (int i = 1; i < dirNameNew.length; i++) {
buffer.append("/");
buffer.append(dirNameNew[i]);
}
} else {
buffer.append("/");
}
// 创建压缩文件并写入数据
ZipEntry entry = new ZipEntry(buffer.toString().substring(1));
zos.putNextEntry(entry);
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(
file));
int count;
byte data[] = new byte[1024];
while ((count = bis.read(data, 0, 1024)) != -1) {
zos.write(data, 0, count);
}
bis.close();
zos.closeEntry();
}
}

AES对称加密工具类

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
public class AES {
// 默认密码
public static final String DEFAULT_PWD = "abcdefghijklmnop";
//
private static final String algorithmStr = "AES/ECB/PKCS5Padding";

private static Cipher encryptCipher;
private static Cipher decryptCipher;

public static void init(String password) {
try {
// 创建加密对象,ECB模式,PKCS5Padding填充方式
encryptCipher = Cipher.getInstance(algorithmStr);
// 创建解密对象
decryptCipher = Cipher.getInstance(algorithmStr);
// 获取密码字节数组
byte[] keyStr = password.getBytes();
// 生成加密密钥
SecretKeySpec key = new SecretKeySpec(keyStr, "AES");
// 初始化加密对象
encryptCipher.init(Cipher.ENCRYPT_MODE, key);
// 初始化解密对象
decryptCipher.init(Cipher.DECRYPT_MODE, key);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
}
}

/**
*
* @param srcAPKfile 源文件所在位置
* @param dstApkFile 目标文件
* @return 加密后的新dex 文件
* @throws Exception
*/
public static File encryptAPKFile(File srcAPKfile, File dstApkFile) throws Exception {
if (srcAPKfile == null) {
System.out.println("encryptAPKFile :srcAPKfile null");
return null;
}
// 解压源文件到目标文件夹
Zip.unZip(srcAPKfile, dstApkFile);
// 获得目标文件夹所有的dex
File[] dexFiles = dstApkFile.listFiles(new FilenameFilter() {
@Override
public boolean accept(File file, String s) {
return s.endsWith(".dex");
}
});

File mainDexFile = null;
byte[] mainDexData = null;
// 遍历所有的dex文件,找到并记录主dex文件并获得加密后的字节数组
for (File dexFile: dexFiles) {
// 获取dex的字节数组
byte[] buffer = Utils.getBytes(dexFile);
// 加密后的字节数组
byte[] encryptBytes = AES.encrypt(buffer);

if (dexFile.getName().endsWith("classes.dex")) {
mainDexData = encryptBytes;
mainDexFile = dexFile;
}
//用加密后的字节数组替换原来的数据
FileOutputStream fos = new FileOutputStream(dexFile);
fos.write(encryptBytes);
fos.flush();
fos.close();
}

// 返回主dex文件
return mainDexFile;
}

// 对字节数组加密返回
public static byte[] encrypt(byte[] content) {
try {
byte[] result = encryptCipher.doFinal(content);
return result;
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
}
return null;
}

// 字节数组解密返回
public static byte[] decrypt(byte[] content) {
try {
byte[] result = decryptCipher.doFinal(content);
return result;
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
}
return null;
}
}

dx转换工具类

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
public class Dx {

public static File jar2Dex(File aarFile) throws IOException, InterruptedException {
// 创建临时文件夹
File fakeDex = new File(aarFile.getParent() + File.separator + "temp");
// 解压aar到临时文件夹下
Zip.unZip(aarFile, fakeDex);
// 过滤找到classes.jar
File[] files = fakeDex.listFiles(new FilenameFilter() {
@Override
public boolean accept(File file, String s) {
return s.equals("classes.jar");
}
});
// aar文件不存在抛异常
if (files == null || files.length <= 0) {
throw new RuntimeException("the aar is invalidate");
}
// 将classes.jar转classes.dex
File classes_jar = files[0];
// 创建classes.dex文件
File aarDex = new File(classes_jar.getParentFile(), "classes.dex");

//使用android tools里面的dx.bat,调windows下的命令
Dx.dxCommand(aarDex, classes_jar);
return aarDex;
}

public static void dxCommand(File aarDex, File classes_jar) throws IOException, InterruptedException {
Runtime runtime = Runtime.getRuntime();
Process process = runtime.exec("cmd.exe /C dx --dex --output=" + aarDex.getAbsolutePath() + " " +
classes_jar.getAbsolutePath());

try {
process.waitFor();
} catch (InterruptedException e) {
e.printStackTrace();
throw e;
}
// 转换失败,输出错误到文件并抛异常
if (process.exitValue() != 0) {
InputStream inputStream = process.getErrorStream();
int len;
byte[] buffer = new byte[2048];
ByteArrayOutputStream bos = new ByteArrayOutputStream();
while((len=inputStream.read(buffer)) != -1){
bos.write(buffer,0,len);
}
System.out.println(new String(bos.toByteArray(),"GBK"));
throw new RuntimeException("dx run failed");
}
process.destroy();
}
}

签名工具类

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

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;


public class Signature {
public static void signature(File unsignedApk, File signedApk) throws InterruptedException, IOException {
// 执行windows下的签名命令
String cmd[] = {"cmd.exe", "/C ","jarsigner", "-sigalg", "MD5withRSA",
"-digestalg", "SHA1",
"-keystore", "C:/Users/allen/.android/debug.keystore",
"-storepass", "android",
"-keypass", "android",
"-signedjar", signedApk.getAbsolutePath(),
unsignedApk.getAbsolutePath(),
"androiddebugkey"};
Process process = Runtime.getRuntime().exec(cmd);
System.out.println("start sign");
try {
int waitResult = process.waitFor();
System.out.println("waitResult: " + waitResult);
} catch (InterruptedException e) {
e.printStackTrace();
throw e;
}
System.out.println("process.exitValue() " + process.exitValue() );
// 执行失败,输出错误到文件并抛异常
if (process.exitValue() != 0) {
InputStream inputStream = process.getErrorStream();
int len;
byte[] buffer = new byte[2048];
ByteArrayOutputStream bos = new ByteArrayOutputStream();
while((len=inputStream.read(buffer)) != -1){
bos.write(buffer,0,len);
}
System.out.println(new String(bos.toByteArray(),"GBK"));
throw new RuntimeException("sign run failed");
}
System.out.println("finish signed");
process.destroy();
}
}

实现壳程序

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
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
public class ShellApplication extends Application {
private static final String TAG = "ShellApplication";

public static String getPassword(){
return "abcdefghijklmnop";
}

@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
// 初始化AES加密
AES.init(getPassword());
// 待解密文件路径data/data/包名/files/fake_apk/
File apkFile = new File(getApplicationInfo().sourceDir);
File unZipFile = getDir("fake_apk", MODE_PRIVATE);
// 待解密的文件目录data/data/包名/files/fake_apk/app
File app = new File(unZipFile, "app");
// 如果不存在待解密文件目录,解压apk
if (!app.exists()) {
Zip.unZip(apkFile, app);
// 过滤不为classes.dex的.dex文件,对读取的字节数组解密写出到文件
File[] files = app.listFiles();
for (File file : files) {
String name = file.getName();
if (name.equals("classes.dex")) {

} else if (name.endsWith(".dex")) {
try {
byte[] bytes = getBytes(file);
FileOutputStream fos = new FileOutputStream(file);
byte[] decrypt = AES.decrypt(bytes);
fos.write(decrypt);
fos.flush();
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
// 获取待解密的文件目录.dex文件列表
List list = new ArrayList<>();
Log.d("FAKE", Arrays.toString(app.listFiles()));
for (File file : app.listFiles()) {
if (file.getName().endsWith(".dex")) {
list.add(file);
}
}

Log.d("FAKE", list.toString());
try {
V19.install(getClassLoader(), list, unZipFile);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}

// 反射获取对象某个变量的值
private static Field findField(Object instance, String name) throws NoSuchFieldException {
Class clazz = instance.getClass();

while (clazz != null) {
try {
Field e = clazz.getDeclaredField(name);
if (!e.isAccessible()) {
e.setAccessible(true);
}

return e;
} catch (NoSuchFieldException var4) {
clazz = clazz.getSuperclass();
}
}

throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass());
}

// 反射获取对象的某个方法
private static Method findMethod(Object instance, String name, Class... parameterTypes)
throws NoSuchMethodException {
Class clazz = instance.getClass();
while (clazz != null) {
try {
Method e = clazz.getDeclaredMethod(name, parameterTypes);
if (!e.isAccessible()) {
e.setAccessible(true);
}

return e;
} catch (NoSuchMethodException var5) {
clazz = clazz.getSuperclass();
}
}
throw new NoSuchMethodException("Method " + name + " with parameters " + Arrays.asList
(parameterTypes) + " not found in " + instance.getClass());
}

// 扩展某个对象的某个变量数组
private static void expandFieldArray(Object instance, String fieldName, Object[]
extraElements) throws NoSuchFieldException, IllegalArgumentException,
IllegalAccessException {
Field jlrField = findField(instance, fieldName);
Object[] original = (Object[]) ((Object[]) jlrField.get(instance));
Object[] combined = (Object[]) ((Object[]) Array.newInstance(original.getClass()
.getComponentType(), original.length + extraElements.length));
System.arraycopy(original, 0, combined, 0, original.length);
System.arraycopy(extraElements, 0, combined, original.length, extraElements.length);
jlrField.set(instance, combined);
}

// 动态加载类
private static final class V19 {
private V19() {
}
private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
File optimizedDirectory) throws IllegalArgumentException,
IllegalAccessException, NoSuchFieldException, InvocationTargetException,
NoSuchMethodException {
// 获取ClassLoader对象的pathList变量的值
Field pathListField = findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
ArrayList suppressedExceptions = new ArrayList();
Log.d(TAG, "Build.VERSION.SDK_INT " + Build.VERSION.SDK_INT);
// 根据当前SDK版本动态批量加载类
if (Build.VERSION.SDK_INT >= 23) {
expandFieldArray(dexPathList, "dexElements", makePathElements(dexPathList, new
ArrayList(additionalClassPathEntries), optimizedDirectory,
suppressedExceptions));
} else {
expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new
ArrayList(additionalClassPathEntries), optimizedDirectory,
suppressedExceptions));
}
// 如果有异常,遍历打印
if (suppressedExceptions.size() > 0) {
Iterator suppressedExceptionsField = suppressedExceptions.iterator();

while (suppressedExceptionsField.hasNext()) {
IOException dexElementsSuppressedExceptions = (IOException)
suppressedExceptionsField.next();
Log.w("MultiDex", "Exception in makeDexElement",
dexElementsSuppressedExceptions);
}
// 获取dexElementsSuppressedExceptions变量的值
Field suppressedExceptionsField1 = findField(loader,
"dexElementsSuppressedExceptions");
IOException[] dexElementsSuppressedExceptions1 = (IOException[]) ((IOException[])
suppressedExceptionsField1.get(loader));
// 如果值为空,赋值为异常数组
if (dexElementsSuppressedExceptions1 == null) {
dexElementsSuppressedExceptions1 = (IOException[]) suppressedExceptions
.toArray(new IOException[suppressedExceptions.size()]);
} else {
// 否则扩展异常数组,合并dexElementsSuppressedExceptions并重新赋值
IOException[] combined = new IOException[suppressedExceptions.size() +
dexElementsSuppressedExceptions1.length];
suppressedExceptions.toArray(combined);
System.arraycopy(dexElementsSuppressedExceptions1, 0, combined,
suppressedExceptions.size(), dexElementsSuppressedExceptions1.length);
dexElementsSuppressedExceptions1 = combined;
}

suppressedExceptionsField1.set(loader, dexElementsSuppressedExceptions1);
}

}

// 调用dexPathList对象的makeDexElements方法
private static Object[] makeDexElements(Object dexPathList,
ArrayList<File> files, File
optimizedDirectory,
ArrayList<IOException> suppressedExceptions) throws
IllegalAccessException, InvocationTargetException, NoSuchMethodException {

Method makeDexElements = findMethod(dexPathList, "makeDexElements", new
Class[]{ArrayList.class, File.class, ArrayList.class});
return ((Object[]) makeDexElements.invoke(dexPathList, new Object[]{files,
optimizedDirectory, suppressedExceptions}));
}
}
// 调用dexPathList对象的makePathElements方法
private static Object[] makePathElements(
Object dexPathList, ArrayList<File> files, File optimizedDirectory,
ArrayList<IOException> suppressedExceptions)
throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
// 查找返回类型为List的makePathElements方法
Method makePathElements;
try {
makePathElements = findMethod(dexPathList, "makePathElements", List.class, File.class,
List.class);
} catch (NoSuchMethodException e) {
Log.e(TAG, "NoSuchMethodException: makePathElements(List,File,List) failure");
// 查找返回类型为ArrayList的makePathElements方法
try {
makePathElements = findMethod(dexPathList, "makePathElements", ArrayList.class, File.class, ArrayList.class);
} catch (NoSuchMethodException e1) {
Log.e(TAG, "NoSuchMethodException: makeDexElements(ArrayList,File,ArrayList) failure");
// 调用dexPathList对象的makeDexElements方法
try {
Log.e(TAG, "NoSuchMethodException: try use v19 instead");
return V19.makeDexElements(dexPathList, files, optimizedDirectory, suppressedExceptions);
} catch (NoSuchMethodException e2) {
Log.e(TAG, "NoSuchMethodException: makeDexElements(List,File,List) failure");
throw e2;
}
}
}
return (Object[]) makePathElements.invoke(dexPathList, files, optimizedDirectory, suppressedExceptions);
}

private byte[] getBytes(File file) throws Exception {
RandomAccessFile r = new RandomAccessFile(file, "r");
byte[] buffer = new byte[(int) r.length()];
r.readFully(buffer);
r.close();
return buffer;
}
}