Quantcast
Channel: CSDN博客移动开发推荐文章
Viewing all 5930 articles
Browse latest View live

Flutter学习之旅(四)----Flutter开发语言Dart的基础语法

$
0
0

Dart官网镇楼(英文版)

先从一段简单的Dart代码入手。

// 定义一个方法
printNumber(num aNumber) {//参数类型为num,
  print('The number is $aNumber.'); // 打印信息到控制台,$aNumber等价于${aNumber},表示取aNumber变量的值。
}
// APP运行入口
main() {
  var number = 42; //定义并初始化变量,var表示没有给变量制定类型
  printNumber(number); // 调用方法
}

如果把Dart语言开发比喻成玩游戏,那么我们必须遵守它的游戏规则,Dart语言有下面这些规则:

*给变量赋值时,只能把对象赋值给变量,每一个对象都是类的实例。而且,数字,方法和null值都是对象,所有的对象继承自Object类。
*声明变量的时候指定具体类型(像num aNumber这样指定类型为num)可以清晰的表明你的意图,并且可以被工具静态检测,不过你也可以不指定。如果你没有为变量指定具体类型的话,那么你在调试程序的时候会看到该变量为特殊的类型dynamic
*Dart会在运行前对所有的代码进行语法分析。
*Dart支持顶层方法(比如说main()方法),同时支持类级别或对象级别的方法,你还可以在方法内部新建方法(嵌套方法或本地方法)。
*同样,Dart支持顶层变量,同时支持类级别或对象级别的变量,实例变量有时候被称为域或者属性。
*与Java不同的是,Dart没有关键词public,protected和private,不过可以用下划线(_)表示private。
*标识符(变量名或方法名等)可以以字母或下划线_开头,
*Dart工具能报告两类问题:告警warning和错误error。Warning仅仅暗示代码可能无法正常工作,但是程序还能运行。Error可能出现在编译时或运行时,而编译时出现错误后程序就无法运行,运行时错误就会出现异常。
*任何变量的初始值为null,即使num类型的初始值也是null,因为它也是对象。

Dart的七种数据类型
1.numbers
2.strings
3.booleans
4.lists (即arrays)
5.maps
6.runes (用字符串表示Unicode characters)
7.symbols
1.numbers包括int(范围为-2^53到2^53)和double,num类型操作符请查看dart:math

//将string类型转换为number类型:
var one=int.parse('1');
//将int类型转换为string类型
String oneAsString = 1.toString();

2.Dart中的string是UTF-16编码单元的序列,可用单引号或双引号括起来。

var s1 = 'Single quotes work well for string literals.';
var s2 = "Double quotes work just as well.";
//字符串拼接的两种情况
var s1 = '这是 ' '字符串' " 拼接";
var s2 = '这是字符串 '+ '拼接';
//多行字符
var s1 = '''
这里是
多行
字符
''';

3.booleans
在Dart中,只有true值被认为是true,其他所有的值都被认为是false,所以像值1,”aString”等等都被认为是false,这点与JavaScript不同。
4.lists (即arrays)
以后单独讲解。
5.Maps

//map赋值的第一种方式
var myMap1={"one":"1","two":"2"};
var myMap2={1:"1",2:"2"};
//map赋值的第二种方式
var myMap3=new Map();
myMap3["one"]="1";

6.Runes(是字符串的UTF-32编码格式)

main() {
  var clapping = '\u{1f44f}';
  print(clapping);
  print(clapping.codeUnits);
  print(clapping.runes.toList());
  Runes input = new Runes(
      '\u2665  \u{1f605}  \u{1f60e}  \u{1f47b}  \u{1f596}  \u{1f44d}');
  print(new String.fromCharCodes(input));
}

把上面代码复制到DartPad,点击Run,运行结果:
这里写图片描述

7.Functions
Dart是真正面向对象的语言,甚至方法也是对象,类型为Function,这意味着方法也能作为参数传递给另外一个方法。

可选参数

String say(String s1,[String s2]){//调用时可以只传第一个参数,也可以传两个参数
    if(s2!=null)print(s1+s2);
}
//调用:
say('s1');
say('s1','s2');

条件表达式
condition ? expr1 : expr2
假如condition为 true, 返回 expr1 否则返回 expr2.
expr1 ?? expr2
假如expr1 为 non-null, 返回expr1,否则返回expr2.

串联符号..

querySelector('#button') // Get an object.
  ..text = 'Confirm'   // Use its members.
  ..classes.add('important')
  ..onClick.listen((e) => window.alert('Confirmed!'));

等价于

var button = querySelector('#button');
button.text = 'Confirm';
button.classes.add('important');
button.onClick.listen((e) => window.alert('Confirmed!'));

语法糖

class Point {
  num x;
  num y;
  // 设置x和y的语法糖
  Point(this.x, this.y);
}

给指定的包加前缀
假如导入的两个包有冲突的地方,比如library1 和library2 都有Element类,如下

import 'package:lib1/lib1.dart';
import 'package:lib2/lib2.dart' as lib2;
Element element1 = new Element();           // 使用lib1的 Element 
lib2.Element element2 = new lib2.Element(); // 使用lib2的 Element

假如你只需要导入包的部分内容,可如下操作:

// 只导入foo.
import 'package:lib1/lib1.dart' show foo;
// 不导入foo.
import 'package:lib2/lib2.dart' hide foo;

懒加载导包
懒加载是指只在需要的时候导入包,下面三种情况可能使用到懒加载:
*减少APP初始化启动时间。
*执行A/B测试,比如说切换算法的实现。
*导入很少使用的包。
使用 deferred as来指定懒加载,具体使用如下:

import 'package:deferred/hello.dart' deferred as hello;

当你需要加载包的时候,调用loadLibrary()方法:

greet() async {
  await hello.loadLibrary();//await(必须放在async方法里面)表示等待执行完再执行下面代码,hello.loadLibrary调用一次,该包只加载一次,需要多处加载,则多次调用。
  hello.printGreeting();
}

除此之外,像if-else,for()循环,while循环,switch-case,try-catch等知识与Java里面类似,就没有列出来。本文内容可以作为Dart语法基本了解,更深层次的内容后面还会介绍。如果看完这篇文章您有所收获,不忘点个赞或者留言,您的鼓励是我前进的动力,谢谢。

作者:zhangxiangliang2 发表于2017/7/25 21:37:33 原文链接
阅读:239 评论:0 查看评论

iOS 高级网络番外:来自GitHub

$
0
0

原文:
[https://github.com/woai30231/http/tree/master/%E7%AC%AC%E5%9B%9B%E7%AB%A0%20%E8%BF%9E%E6%8E%A5%E7%AE%A1%E7%90%86]

内容提要

  • 这一章主要讲解了http的下层协议tcp/ip的一些知识点:tcp/ip建立连接需要做的事情,tcp/ip所带来的时延,以及从http的角度出发,提升网络性能的一些方法,涉及到串行连接、并行连接、持久连接、管道连接等概念!以及介绍了如何关闭连接等概念。

TCP/IP连接

  • TCP/IP是全球计算机及网络设备都在使用的一种常用的分组交换网络分层协议集,位于http下层。其实常谈论的http连接实际上就是tcp连接加上一些使用连接的规则,tcp为http提供了一条可靠的比特传输管道。一旦连接建立起来,在客户端和服务器的计算机之间交换的报文就永远不会丢失、受损或失序。

  • 通常http事务发生时会经过几个步骤,下面以访问http://www.xxx.com:80/path/index.html为例说明:

  1. 浏览器从地址栏中解析处域名(主机名),也就是拿到www.xxx.com

  2. 浏览器根据得到的主机名查询出ip地址,比如算出ip为202.43.78.3,(中间可能经过查找host文件或去查询dns服务器)

  3. 浏览器解析出端口(http默认为80,https默认为443)

  4. 浏览器发起一条到202.43.78.3端口为80的链接,(重建需要经过几次确定相关参数的来回“握手”)

  5. 浏览器发起请求报文

  6. 服务器返回响应报文

  7. 浏览器关闭连接(其实浏览器和服务器都可以在不通知对方的情况关闭连接)

  • TCP流是分段的,由IP分组传输,也就是说最终http报文是以ip分组的形式在网络之间传输。一个ip分组包含的数据信息如下:
 1. ip分组首部(通常为20字节)
    2. tcp段首部 (通常为20字节)
    3. tcp数据块 (0个或多个字节,实际http报文数据就在这里) 

用一句话描述这个过程就是,http报文流给到tcp,tcp把报文分成一段一段的,然后tcp把每个tcp段交给ip,ip封装成一个ip分组,最后传输的是ip分组。(当然了这里我们忽略了ip下面的数据链路层和物理层)

TCP确定一个连接

  • TCP用四个信息来唯一确定一条连接:源ip地址、源端口号、目的ip地址、目的端口号。只要其中有一个不同,那么就不是同一条连接。在任意时刻计算机都可以有几条tcp连接在打开状态。

对TCP性能的考虑

  • 首先相对建立tcp连接、发送http请求报文以及响应报文相比,http事务处理的时间相对短很多很多,此时延可不用讨论,除非你的服务器超负载了或正在处理复杂的运算。因此,http事务的时延往往由以下原因组成:
  1. 首先客户端解析ip地址或者端口号需要时间,如果当前没有访问过相关资源,那么解析还需要查询dns服务器,此操作,造成的时延较多,可能花费数十秒。

  2. 建立tcp链接会有建立时延,通常2s左右,如果当前的http事务较多,那么会很快叠加上去。

  3. 传输、处理请求报文需要时间

  4. 回传响应报文需要时间

  5. 当然还有其他因素,比如硬件、网络负载,以及报文尺寸等!

  • 性能聚焦区域

这里简要说明一下,建立tcp链接这个过程可能存在的时延分析,包括:经典三次“握手”、tcp慢启动拥塞控制机制等!

  • 经典三次“握手”说的就是http事务在建立tcp连接是需要做的相关参数确认过程,大概如下:
  1. 客户端发送携带“SYN”标记的TCP段说明发起连接请求

  2. 服务端返回“SYN”和“ACK”的TCP段说明已接受

  3. 最后客户端发送确认信息以确认连接

  • tcp慢启动说明了,tcp连接会随着时间的增加进行自我调谐。这个主要目的防止突然的tcp连接增多导致网络瘫痪,所以它会慢慢的调整传输速度,这个机制就叫做TCP慢启动。

HTTP连接的处理

常被误解的connection首部
  • connection能承载三种字段值:

    HTTP首部字段名,列出了只与此有关的首部;

    任意标签值,用于描述此链接的非标准选项;

    值close,说明操作完成之后需关闭这条持久连接。

接收端在收到请求报文之后,对报文进行解析,并查看connection首部中列出的首部列表,并在转发出去之前,删除相关首部,这一行为称为:“对首部的保护”。

串行处理事务时延
  • 此种机制描述了http事务一个一个接着发起,不能同时下载更多的资源,使得界面上用户看不到东西,体验不够好。串行连接没有很好的利用tcp/ip连接的慢启动机制!

  • 优化方法主要有:

    并行连接

    通过多条TCP连接发起并发的HTTP连接

    持久连接

    重用TCP连接,以消除连接及关闭时延

    管道化连接

    通过共享的TCP连接发起并发的HTTP请求


并行连接
  • 浏览同时发起过个http事务,因为是并行的,所以时延也并行的,这样总时延较小,页面呈现更快,体验较好。但也不是总是这样,因为如果在网络速度很慢的时候,多个连接会去竞争本来不多的带宽,那么就谈不上加快速度了。还有就是并行连接也是需要付出代价的,比如增加系统内训消耗、服务器负载,比如有一个100客户端同时对服务发起100tcp并行连接的话,那么服务器就得负责10000个处理请求,很快的你的服务器就会爆掉。当然了,并行连接确实能带来视觉上的速度提升,因为相比于串行连接慢慢地显示数据而并行一下子能全部显示完信息,视觉上并行连接会给人速度更快的感觉!
持久连接
  • 持久连接描述的是:如果对同ip、同端口的发起多个http事务连接,那么可以在前一个事务处理完成之后不要关闭tcp连接,以此来减小建立tcp、tcp慢启动所带来的时延。相关概念不在赘述!
管道化连接

HTTP/1.1允许在持久连接上可选地使用请求管道。这是在keep-alive连接上的进一步性能优化。在响应到达之前,可以将多条请求放入队列。当第一条请求通过网络流向地球另一端的服务器时,第二条和第三条请求也可以开始发送了。在高时延网络条件下,这样做可以降低网络的环回时间,提高性能。

  • 管道连接的限制
    • 如果不是持久连接就不要使用管道连接

    • 接收端必须按收到请求报文的顺序返回响应报文,因为HTTP报文中没有序列号标签。所以必须靠按序发送响应报文来达到“数据对应”

    • 发送端应该做好数据没有发送完连接就关闭的准备并开始重新发送数据。

    • HTTP客户端不应该用管道化的方式发送会产生副作用的请求(比如POST)。

关闭连接的奥秘

作者:Xoxo_x 发表于2017/7/26 0:08:51 原文链接
阅读:203 评论:0 查看评论

Android:关于ContentProvider的知识都在这里了!

$
0
0

前言

  • ContentProvider属于 Android的四大组件之一
  • 本文全面解析了 ContentProvider ,包括ContentProvider 原理、使用方法 & 实例讲解,希望你们会喜欢。

目录

示意图


1. 定义

即内容提供者,是 Android 四大组件之一


2. 作用

进程间 进行数据交互 & 共享,即跨进程通信

示意图


3. 原理


4. 具体使用

关于ContentProvider的使用主要介绍以下内容:

image.png

4.1 统一资源标识符(URI)

  • 定义:Uniform Resource Identifier,即统一资源标识符
  • 作用:唯一标识 ContentProvider & 其中的数据

    外界进程通过 URI 找到对应的ContentProvider & 其中的数据,再进行数据操作

  • 具体使用
    URI分为 系统预置 & 自定义,分别对应系统内置的数据(如通讯录、日程表等等)和自定义数据库
    1. 关于 系统预置URI 此处不作过多讲解,需要的同学可自行查看
    2. 此处主要讲解 自定义URI

示意图
// 设置URI
Uri uri = Uri.parse("content://com.carson.provider/User/1") 
// 上述URI指向的资源是:名为 `com.carson.provider`的`ContentProvider` 中表名 为`User` 中的 `id`为1的数据

// 特别注意:URI模式存在匹配通配符* & #

// *:匹配任意长度的任何有效字符的字符串
// 以下的URI 表示 匹配provider的任何内容
content://com.example.app.provider/* 
// #:匹配任意长度的数字字符的字符串
// 以下的URI 表示 匹配provider中的table表的所有行
content://com.example.app.provider/table/# 

4.2 MIME数据类型

  • 作用:指定某个扩展名的文件用某种应用程序来打开
    如指定.html文件采用text应用程序打开、指定.pdf文件采用flash应用程序打开

  • 具体使用:

4.2.1 ContentProvider根据 URI 返回MIME类型

ContentProvider.geType(uri) ;

4.2.2 MIME类型组成
每种MIME类型 由2部分组成 = 类型 + 子类型

MIME类型是 一个 包含2部分的字符串

text / html
// 类型 = text、子类型 = html

text/css
text/xml
application/pdf

4.2.3 MIME类型形式
MIME类型有2种形式:


// 形式1:单条记录  
vnd.android.cursor.item/自定义
// 形式2:多条记录(集合)
vnd.android.cursor.dir/自定义 

// 注:
  // 1. vnd:表示父类型和子类型具有非标准的、特定的形式。
  // 2. 父类型已固定好(即不能更改),只能区别是单条还是多条记录
  // 3. 子类型可自定义

实例说明

<-- 单条记录 -->
  // 单个记录的MIME类型
  vnd.android.cursor.item/vnd.yourcompanyname.contenttype 

  // 若一个Uri如下
  content://com.example.transportationprovider/trains/122   
  // 则ContentProvider会通过ContentProvider.geType(url)返回以下MIME类型
  vnd.android.cursor.item/vnd.example.rail


<-- 多条记录 -->
  // 多个记录的MIME类型
  vnd.android.cursor.dir/vnd.yourcompanyname.contenttype 
  // 若一个Uri如下
  content://com.example.transportationprovider/trains 
  // 则ContentProvider会通过ContentProvider.geType(url)返回以下MIME类型
  vnd.android.cursor.dir/vnd.example.rail

4.3 ContentProvider类

4.3.1 组织数据方式

  • ContentProvider主要以 表格的形式 组织数据
    同时也支持文件数据,只是表格形式用得比较多
  • 每个表格中包含多张表,每张表包含行 & 列,分别对应记录 & 字段
    同数据库

4.3.2 主要方法

  • 进程间共享数据的本质是:添加、删除、获取 & 修改(更新)数据
  • 所以ContentProvider的核心方法也主要是上述4个作用
<-- 4个核心方法 -->
  public Uri insert(Uri uri, ContentValues values) 
  // 外部进程向 ContentProvider 中添加数据

  public int delete(Uri uri, String selection, String[] selectionArgs) 
  // 外部进程 删除 ContentProvider 中的数据

  public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs)
  // 外部进程更新 ContentProvider 中的数据

  public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,  String sortOrder)  
  // 外部应用 获取 ContentProvider 中的数据

// 注:
  // 1. 上述4个方法由外部进程回调,并运行在ContentProvider进程的Binder线程池中(不是主线程)
 // 2. 存在多线程并发访问,需要实现线程同步
   // a. 若ContentProvider的数据存储方式是使用SQLite & 一个,则不需要,因为SQLite内部实现好了线程同步,若是多个SQLite则需要,因为SQL对象之间无法进行线程同步
  // b. 若ContentProvider的数据存储方式是内存,则需要自己实现线程同步

<-- 2个其他方法 -->
public boolean onCreate() 
// ContentProvider创建后 或 打开系统后其它进程第一次访问该ContentProvider时 由系统进行调用
// 注:运行在ContentProvider进程的主线程,故不能做耗时操作

public String getType(Uri uri)
// 得到数据类型,即返回当前 Url 所代表数据的MIME类型
  • Android为常见的数据(如通讯录、日程表等)提供了内置了默认的ContentProvider
  • 但也可根据需求自定义ContentProvider,但上述6个方法必须重写
    本文主要讲解自定义ContentProvider
  • ContentProvider类并不会直接与外部进程交互,而是通过ContentResolver

4.4 ContentResolver类

4.1 作用

统一管理不同 `ContentProvider`间的操作
  1. 即通过 URI 即可操作 不同的ContentProvider 中的数据
  2. 外部进程通过 ContentResolver类 从而与ContentProvider类进行交互

4.2 为什么要使用通过ContentResolver类从而与ContentProvider类进行交互,而不直接访问ContentProvider类?

答:

  • 一般来说,一款应用要使用多个ContentProvider,若需要了解每个ContentProvider的不同实现从而再完成数据交互,操作成本高 & 难度大
  • 所以再ContentProvider类上加多了一个 ContentResolver类对所有的ContentProvider进行统一管理。

4.3 具体使用

ContentResolver 类提供了与ContentProvider类相同名字 & 作用的4个方法

// 外部进程向 ContentProvider 中添加数据
public Uri insert(Uri uri, ContentValues values)  

// 外部进程 删除 ContentProvider 中的数据
public int delete(Uri uri, String selection, String[] selectionArgs)

// 外部进程更新 ContentProvider 中的数据
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs)  

// 外部应用 获取 ContentProvider 中的数据
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
  • 实例说明
// 使用ContentResolver前,需要先获取ContentResolver
// 可通过在所有继承Context的类中 通过调用getContentResolver()来获得ContentResolver
ContentResolver resolver =  getContentResolver(); 

// 设置ContentProvider的URI
Uri uri = Uri.parse("content://cn.scu.myprovider/user"); 

// 根据URI 操作 ContentProvider中的数据
// 此处是获取ContentProvider中 user表的所有记录 
Cursor cursor = resolver.query(uri, null, null, null, "userid desc"); 

Android 提供了3个用于辅助ContentProvide的工具类:

  • ContentUris
  • UriMatcher
  • ContentObserver

4.5 ContentUris类

  • 作用:操作 URI
  • 具体使用
    核心方法有两个:withAppendedId() &parseId()
// withAppendedId()作用:向URI追加一个id
Uri uri = Uri.parse("content://cn.scu.myprovider/user") 
Uri resultUri = ContentUris.withAppendedId(uri, 7);  
// 最终生成后的Uri为:content://cn.scu.myprovider/user/7

// parseId()作用:从URL中获取ID
Uri uri = Uri.parse("content://cn.scu.myprovider/user/7") 
long personid = ContentUris.parseId(uri); 
//获取的结果为:7

4.6 UriMatcher类

  • 作用

    1. ContentProvider 中注册URI
    2. 根据 URI 匹配 ContentProvider 中对应的数据表
  • 具体使用

// 步骤1:初始化UriMatcher对象
    UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH); 
    //常量UriMatcher.NO_MATCH  = 不匹配任何路径的返回码
    // 即初始化时不匹配任何东西

// 步骤2:在ContentProvider 中注册URI(addURI())
    int URI_CODE_a = 1int URI_CODE_b = 2;
    matcher.addURI("cn.scu.myprovider", "user1", URI_CODE_a); 
    matcher.addURI("cn.scu.myprovider", "user2", URI_CODE_b); 
    // 若URI资源路径 = content://cn.scu.myprovider/user1 ,则返回注册码URI_CODE_a
    // 若URI资源路径 = content://cn.scu.myprovider/user2 ,则返回注册码URI_CODE_b

// 步骤3:根据URI 匹配 URI_CODE,从而匹配ContentProvider中相应的资源(match())

@Override   
    public String getType(Uri uri) {   
      Uri uri = Uri.parse(" content://cn.scu.myprovider/user1");   

      switch(matcher.match(uri)){   
     // 根据URI匹配的返回码是URI_CODE_a
     // 即matcher.match(uri) == URI_CODE_a
      case URI_CODE_a:   
        return tableNameUser1;   
        // 如果根据URI匹配的返回码是URI_CODE_a,则返回ContentProvider中的名为tableNameUser1的表
      case URI_CODE_b:   
        return tableNameUser2;
        // 如果根据URI匹配的返回码是URI_CODE_b,则返回ContentProvider中的名为tableNameUser2的表
    }   
}

4.7 ContentObserver类

  • 定义:内容观察者
  • 作用:观察 Uri引起 ContentProvider 中的数据变化 & 通知外界(即访问该数据访问者)
    ContentProvider 中的数据发生变化(增、删 & 改)时,就会触发该 ContentObserver
  • 具体使用
// 步骤1:注册内容观察者ContentObserver
    getContentResolver().registerContentObserver(uri);
    // 通过ContentResolver类进行注册,并指定需要观察的URI

// 步骤2:当该URI的ContentProvider数据发生变化时,通知外界(即访问该ContentProvider数据的访问者)
    public class UserContentProvider extends ContentProvider { 
      public Uri insert(Uri uri, ContentValues values) { 
      db.insert("user", "userid", values); 
      getContext().getContentResolver().notifyChange(uri, null); 
      // 通知访问者
   } 
}

// 步骤3:解除观察者
 getContentResolver().unregisterContentObserver(uri);
    // 同样需要通过ContentResolver类进行解除
至此,关于`ContentProvider`的使用已经讲解完毕

5. 实例说明

  • 由于ContentProvider不仅常用于进程间通信,同时也适用于进程内通信
  • 所以本实例会采用ContentProvider讲解:
    1. 进程内通信
    2. 进程间通信
  • 实例说明:采用的数据源是Android中的SQLite数据库

5.1 进程内通信

  • 步骤说明:

    1. 创建数据库类
    2. 自定义 ContentProvider
    3. 注册 创建的 ContentProvider
    4. 进程内访问 ContentProvider的数据
  • 具体使用

步骤1:创建数据库类
关于数据库操作请看文章:Android:SQLlite数据库操作最详细解析

DBHelper.java

public class DBHelper extends SQLiteOpenHelper {

    // 数据库名
    private static final String DATABASE_NAME = "finch.db";

    // 表名
    public static final String USER_TABLE_NAME = "user";
    public static final String JOB_TABLE_NAME = "job";

    private static final int DATABASE_VERSION = 1;
    //数据库版本号

    public DBHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {

        // 创建两个表格:用户表 和职业表
        db.execSQL("CREATE TABLE IF NOT EXISTS " + USER_TABLE_NAME + "(_id INTEGER PRIMARY KEY AUTOINCREMENT," + " name TEXT)");
        db.execSQL("CREATE TABLE IF NOT EXISTS " + JOB_TABLE_NAME + "(_id INTEGER PRIMARY KEY AUTOINCREMENT," + " job TEXT)");
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)   {

    }
}

步骤2:自定义 ContentProvider 类

public class MyProvider extends ContentProvider {

    private Context mContext;
    DBHelper mDbHelper = null;
    SQLiteDatabase db = null;

    public static final String AUTOHORITY = "cn.scu.myprovider";
    // 设置ContentProvider的唯一标识

    public static final int User_Code = 1;
    public static final int Job_Code = 2;

    // UriMatcher类使用:在ContentProvider 中注册URI
    private static final UriMatcher mMatcher;
    static{
        mMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        // 初始化
        mMatcher.addURI(AUTOHORITY,"user", User_Code);
        mMatcher.addURI(AUTOHORITY, "job", Job_Code);
        // 若URI资源路径 = content://cn.scu.myprovider/user ,则返回注册码User_Code
        // 若URI资源路径 = content://cn.scu.myprovider/job ,则返回注册码Job_Code
    }

    // 以下是ContentProvider的6个方法

    /**
     * 初始化ContentProvider
     */
    @Override
    public boolean onCreate() {

        mContext = getContext();
        // 在ContentProvider创建时对数据库进行初始化
        // 运行在主线程,故不能做耗时操作,此处仅作展示
        mDbHelper = new DBHelper(getContext());
        db = mDbHelper.getWritableDatabase();

        // 初始化两个表的数据(先清空两个表,再各加入一个记录)
        db.execSQL("delete from user");
        db.execSQL("insert into user values(1,'Carson');");
        db.execSQL("insert into user values(2,'Kobe');");

        db.execSQL("delete from job");
        db.execSQL("insert into job values(1,'Android');");
        db.execSQL("insert into job values(2,'iOS');");

        return true;
    }

    /**
     * 添加数据
     */

    @Override
    public Uri insert(Uri uri, ContentValues values) {

        // 根据URI匹配 URI_CODE,从而匹配ContentProvider中相应的表名
        // 该方法在最下面
        String table = getTableName(uri);

        // 向该表添加数据
        db.insert(table, null, values);

        // 当该URI的ContentProvider数据发生变化时,通知外界(即访问该ContentProvider数据的访问者)
        mContext.getContentResolver().notifyChange(uri, null);

//        // 通过ContentUris类从URL中获取ID
//        long personid = ContentUris.parseId(uri);
//        System.out.println(personid);

        return uri;
        }

    /**
     * 查询数据
     */
    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {
        // 根据URI匹配 URI_CODE,从而匹配ContentProvider中相应的表名
        // 该方法在最下面
        String table = getTableName(uri);

//        // 通过ContentUris类从URL中获取ID
//        long personid = ContentUris.parseId(uri);
//        System.out.println(personid);

        // 查询数据
        return db.query(table,projection,selection,selectionArgs,null,null,sortOrder,null);
    }

    /**
     * 更新数据
     */
    @Override
    public int update(Uri uri, ContentValues values, String selection,
                      String[] selectionArgs) {
        // 由于不展示,此处不作展开
        return 0;
    }

    /**
     * 删除数据
     */
    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        // 由于不展示,此处不作展开
        return 0;
    }

    @Override
    public String getType(Uri uri) {

        // 由于不展示,此处不作展开
        return null;
    }

    /**
     * 根据URI匹配 URI_CODE,从而匹配ContentProvider中相应的表名
     */
    private String getTableName(Uri uri){
        String tableName = null;
        switch (mMatcher.match(uri)) {
            case User_Code:
                tableName = DBHelper.USER_TABLE_NAME;
                break;
            case Job_Code:
                tableName = DBHelper.JOB_TABLE_NAME;
                break;
        }
        return tableName;
        }
    }

步骤3:注册 创建的 ContentProvider类
AndroidManifest.xml

<provider android:name="MyProvider"
                android:authorities="cn.scu.myprovider"
                    />

步骤4:进程内访问 ContentProvider中的数据

MainActivity.java

public class MainActivity extends AppCompatActivity {

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

        /**
         * 对user表进行操作
         */

        // 设置URI
        Uri uri_user = Uri.parse("content://cn.scu.myprovider/user");

        // 插入表中数据
        ContentValues values = new ContentValues();
        values.put("_id", 3);
        values.put("name", "Iverson");


        // 获取ContentResolver
        ContentResolver resolver =  getContentResolver();
        // 通过ContentResolver 根据URI 向ContentProvider中插入数据
        resolver.insert(uri_user,values);

        // 通过ContentResolver 向ContentProvider中查询数据
        Cursor cursor = resolver.query(uri_user, new String[]{"_id","name"}, null, null, null);
        while (cursor.moveToNext()){
            System.out.println("query book:" + cursor.getInt(0) +" "+ cursor.getString(1));
            // 将表中数据全部输出
        }
        cursor.close();
        // 关闭游标

        /**
         * 对job表进行操作
         */
        // 和上述类似,只是URI需要更改,从而匹配不同的URI CODE,从而找到不同的数据资源
        Uri uri_job = Uri.parse("content://cn.scu.myprovider/job");

        // 插入表中数据
        ContentValues values2 = new ContentValues();
        values2.put("_id", 3);
        values2.put("job", "NBA Player");

        // 获取ContentResolver
        ContentResolver resolver2 =  getContentResolver();
        // 通过ContentResolver 根据URI 向ContentProvider中插入数据
        resolver2.insert(uri_job,values2);

        // 通过ContentResolver 向ContentProvider中查询数据
        Cursor cursor2 = resolver2.query(uri_job, new String[]{"_id","job"}, null, null, null);
        while (cursor2.moveToNext()){
            System.out.println("query job:" + cursor2.getInt(0) +" "+ cursor2.getString(1));
            // 将表中数据全部输出
        }
        cursor2.close();
        // 关闭游标
}
}

结果

示意图

源码地址

Carson-Ho Github地址:ContentProvider

至此,进程内对ContentProvider中的数据进行共享讲解完毕。


5.2 进程间进行数据共享

  • 实例说明:本文需要创建2个进程,即创建两个工程,作用如下

示意图

  • 具体使用

进程1

使用步骤如下:
1. 创建数据库类
2. 自定义 ContentProvider
3. 注册 创建的 ContentProvider

前2个步骤同上例相同,此处不作过多描述,此处主要讲解步骤3.

步骤3:注册 创建的 ContentProvider类
AndroidManifest.xml

<provider 
               android:name="MyProvider"
               android:authorities="scut.carson_ho.myprovider"

               // 声明外界进程可访问该Provider的权限(读 & 写)
               android:permission="scut.carson_ho.PROVIDER"             

               // 权限可细分为读 & 写的权限
               // 外界需要声明同样的读 & 写的权限才可进行相应操作,否则会报错
               // android:readPermisson = "scut.carson_ho.Read"
               // android:writePermisson = "scut.carson_ho.Write"

               // 设置此provider是否可以被其他进程使用
               android:exported="true"

  />

// 声明本应用 可允许通信的权限
    <permission android:name="scut.carson_ho.Read" android:protectionLevel="normal"/>
    // 细分读 & 写权限如下,但本Demo直接采用全权限
    // <permission android:name="scut.carson_ho.Write" android:protectionLevel="normal"/>
    // <permission android:name="scut.carson_ho.PROVIDER" android:protectionLevel="normal"/>

至此,进程1创建完毕,即创建ContentProvider & 数据 准备好了。

源码地址

Carson-Ho Github地址:ContentProvider1


进程2

步骤1:声明可访问的权限

AndroidManifest.xml

    // 声明本应用可允许通信的权限(全权限)
    <uses-permission android:name="scut.carson_ho.PROVIDER"/>

    // 细分读 & 写权限如下,但本Demo直接采用全权限
    // <uses-permission android:name="scut.carson_ho.Read"/>
    //  <uses-permission android:name="scut.carson_ho.Write"/>

// 注:声明的权限必须与进程1中设置的权限对应

步骤2:访问 ContentProvider的类

public class MainActivity extends AppCompatActivity {

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

        /**
         * 对user表进行操作
         */

        // 设置URI
        Uri uri_user = Uri.parse("content://scut.carson_ho.myprovider/user");

        // 插入表中数据
        ContentValues values = new ContentValues();
        values.put("_id", 4);
        values.put("name", "Jordan");


        // 获取ContentResolver
        ContentResolver resolver =  getContentResolver();
        // 通过ContentResolver 根据URI 向ContentProvider中插入数据
        resolver.insert(uri_user,values);

        // 通过ContentResolver 向ContentProvider中查询数据
        Cursor cursor = resolver.query(uri_user, new String[]{"_id","name"}, null, null, null);
        while (cursor.moveToNext()){
            System.out.println("query book:" + cursor.getInt(0) +" "+ cursor.getString(1));
            // 将表中数据全部输出
        }
        cursor.close();
        // 关闭游标

        /**
         * 对job表进行操作
         */
        // 和上述类似,只是URI需要更改,从而匹配不同的URI CODE,从而找到不同的数据资源
        Uri uri_job = Uri.parse("content://scut.carson_ho.myprovider/job");

        // 插入表中数据
        ContentValues values2 = new ContentValues();
        values2.put("_id", 4);
        values2.put("job", "NBA Player");

        // 获取ContentResolver
        ContentResolver resolver2 =  getContentResolver();
        // 通过ContentResolver 根据URI 向ContentProvider中插入数据
        resolver2.insert(uri_job,values2);

        // 通过ContentResolver 向ContentProvider中查询数据
        Cursor cursor2 = resolver2.query(uri_job, new String[]{"_id","job"}, null, null, null);
        while (cursor2.moveToNext()){
            System.out.println("query job:" + cursor2.getInt(0) +" "+ cursor2.getString(1));
            // 将表中数据全部输出
        }
        cursor2.close();
        // 关闭游标
    }
}

至此,访问ContentProvider数据的进程2创建完毕

源码地址

Carson-Ho Github地址:ContentProvider2


结果展示

在进程展示时,需要先运行准备数据的进程1,再运行需要访问数据的进程2
1. 运行准备数据的进程1
在进程1中,我们准备好了一系列数据
示意图

  1. 运行需要访问数据的进程2
    在进程2中,我们先向ContentProvider中插入数据,再查询数据

示意图

至此,关于ContentProvider在进程内 & 进程间的使用讲解完毕。


6. 优点

6.1 安全

ContentProvider为应用间的数据交互提供了一个安全的环境:允许把自己的应用数据根据需求开放给 其他应用 进行 增、删、改、查,而不用担心因为直接开放数据库权限而带来的安全问题

6.2 访问简单 & 高效

对比于其他对外共享数据的方式,数据访问方式会因数据存储的方式而不同:

  • 采用 文件方式 对外共享数据,需要进行文件操作读写数据;
  • 采用 Sharedpreferences 共享数据,需要使用sharedpreferences API读写数据

这使得访问数据变得复杂 & 难度大。


  • 而采用ContentProvider方式,其 解耦了 底层数据的存储方式,使得无论底层数据存储采用何种方式,外界对数据的访问方式都是统一的,这使得访问简单 & 高效

如一开始数据存储方式 采用 SQLite 数据库,后来把数据库换成 MongoDB,也不会对上层数据ContentProvider使用代码产生影响
示意图


7. 总结

  • 我用一张图总结本文内容

示意图


请 帮顶或评论点赞!因为你的鼓励是我写作的最大动力!

作者:carson_ho 发表于2017/7/26 8:49:29 原文链接
阅读:366 评论:0 查看评论

React-Native 组件之 Modal

$
0
0

Modal组件可以用来覆盖包含React Native根视图的原生视图(如UIViewController,Activity),用它可以实现遮罩的效果。

属性

Modal提供的属性有:

animationType(动画类型) PropTypes.oneOf([‘none’, ‘slide’, ‘fade’]

  • none:没有动画
  • slide:从底部滑入
  • fade:淡入视野

onRequestClose(被销毁时会调用此函数)

  • 在 ‘Android’ 平台,必需调用此函数

onShow(模态显示的时候被调用)

transparent (透明度) bool

  • 为true时,使用透明背景渲染模态。

visible(可见性) bool

onOrientationChange(方向改变时调用)

  • 在模态方向变化时调用,提供的方向只是 ” 或 ”。在初始化渲染的时候也会调用,但是不考虑当前方向。

supportedOrientations(允许模态旋转到任何指定取向)[‘portrait’, ‘portrait-upside-down’, ‘landscape’,’landscape-left’,’landscape-right’])

  • 在iOS上,模态仍然受 info.plist 中的 UISupportedInterfaceOrientations字段中指定的限制。

示例

Modal的使用非常简单,例如:

<Modal
 animationType='slide'           // 从底部滑入  
 transparent={false}             // 不透明
 visible={this.state.isModal}    // 根据isModal决定是否显示
 onRequestClose={() => {this.onRequestClose()}}  // android必须实现
 >

综合例子:

import React, { Component} from 'react';
import  {
    AppRegistry,
    View,
    Modal,
    TouchableOpacity,
    Text
} from 'react-native';
export default class ModalView extends Component {
    constructor(props) {
        super(props);
        this.state = {
            modalVisible: false,
        }
    }
    setModalVisible = (visible)=> {
        this.setState({
           modalVisible: visible
        })
    };
    render(){
        return(
            <View style={{flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#ffaaff'}}>
                <Modal animationType={'none'}
                       transparent={true}
                       visible={this.state.modalVisible}
                       onrequestclose={() => {alert("Modal has been closed.")}}
                       onShow={() => {alert("Modal has been open.")}}
                       supportedOrientations={['portrait', 'portrait-upside-down', 'landscape', 'landscape-left', 'landscape-right']}
                       onOrientationChange={() => {alert("Modal has been OrientationChange.")}}>
                    <View style={{flex:1, marginTop: 22, backgroundColor: '#aaaaaa', justifyContent: 'center', alignItems: 'center'}}>
                        <View>
                            <Text>Hello World!</Text>
                            <TouchableOpacity onPress={() => {
                                this.setModalVisible(false)
                            }}>
                                <Text>隐藏 Modal</Text>
                            </TouchableOpacity>
                        </View>
                    </View>
                </Modal>
                <TouchableOpacity onPress={() => {
                    this.setModalVisible(true)
                }}>
                    <Text>显示 Modal</Text>
                </TouchableOpacity>
            </View>
        )
    }
}
AppRegistry.registerComponent('ModalView', ()=>ModalView);

运行效果:
这里写图片描述

从 modal 的源码可以看出,modal 其实就是使用了 绝对定位,所以当 modal 无法满足我们的需求的时候,我们就可以通过 绝对定位 自己来封装一个 modal

作者:xiangzhihong8 发表于2017/7/26 9:33:53 原文链接
阅读:171 评论:0 查看评论

Android NDK: From Elementary to Expert Expisode 21

$
0
0

Last time, I wrote an AVI player app. When I began to generate the header file for the native function, I got an error:

Error: Cannot determine signature for Bitmap

Here is the right command:
On Mac and Linux:

javah -cp <Your android sdk path>/sdk/platforms/verision(such as:android-25)/android.jar:. <class name of your file>

On Windows:

javah -cp <Your android sdk path>/sdk/platforms/verision(such as:android-25)/android.jar;. <class name of your file>

After all, I generated the header file successfully.

作者:myfather103 发表于2017/7/26 11:14:32 原文链接
阅读:90 评论:0 查看评论

图像滤镜艺术---美颜相机之高级柔焦效果实现

$
0
0

今天给大家讲解一下,如何实现美颜相机中的高级柔焦效果,首先先看下美颜相机中这个功能的效果图:



图1 原图(图片来自网络,如有侵权敬请告知)


图2 美颜相机高级柔焦模版


图3 马赛克效果


图4,动感模糊效果

以上图3和4是两种柔焦的效果,很不错,今天我将用C来实现这个算法。

这个算法的流程如下:

1,前景抠图(自动抠图或者手动抠图)

美颜相机使用的是先手动涂抹出前景目标区域,然后使用自动抠图将该区域准确抠图;

实际上,目前有很多基于深度学习的抠图算法,效果相当不错,比如最近比较火的实时视频抠图技术等等。这里,本人还是给出一些方法的汇总如下:

Interactive graph cuts for optimal boundary and region segmentation of objects in N-D images》

Shared Sampling for Real-Time Alpha Matting》

A Closed Form Solution to Natural Image Matting》

A Bayesian Approach to Digital Matting》

Learning Based Alpha Matting using Support Vector Regression》

Natural Image Matting using Deep Convolutional Neural Networks》

Deep Image Matting》

本文主要介绍方法,这里给出白百合测试图前景抠图的效果:


图5 白百合前景MASK

2,背景特效

大家可以看到图3和图4的效果区别,主要是背景的区别,一个是马赛克,一个是放射模糊;

因此,我们需要对背景图片进行相应的马赛克和放射模糊处理,本人效果图如下:


图6 马赛克效果


图7 放射模糊效果

这里可以参考本人博客里的各种滤镜特效,实际上都可以用来做背景处理,进而得出各种不同的效果;

3,图像融合

有了前景背景之后,我们可以根据前景的MASK(区域的二值MASK),对前景和背景进行alpha融合,公式如下:

Output = F*Mask+(255-Mask)*B

其中F表示前景,B表示背景,其中MASK中白色表示前景,黑色表示背景;

最后本人的效果图如下:


图8 马赛克柔焦效果图


图9 放射模糊柔焦效果图


以上三点就是柔焦效果的具体算法实现过程,最后,给出代码逻辑(我这里实现了四个柔焦的效果):

int f_Focus(unsigned char* srcData, int width, int height, int stride, unsigned char* mask, int intensity, int mode)
{
	//background effects
	int ret = 0;
	int i, j;
	unsigned char* gaussData = (unsigned char*)malloc(sizeof(unsigned char) * stride * height);
	int radius = intensity * 20 / 100;
	switch(mode)
	{
		case  0://gauss blur effect
		    ret = f_TFastestGaussFilter(srcData, width, height, stride, gaussData, radius);
		    break;
		case 1://moscia effect
			memcpy(gaussData, srcData, sizeof(unsigned char) * stride * height);
			ret = f_TMosaic(gaussData, width, height, stride, intensity / 2);
			break;
		case 2://diffusion effect
			memcpy(gaussData, srcData, sizeof(unsigned char) * stride * height);
			ret = f_TDiffusion(gaussData, width, height, stride, intensity / 2);
			break;
		case 3://zoom blur effect
			memcpy(gaussData, srcData, sizeof(unsigned char) * stride * height);
			ret = f_TZoomBlur(gaussData, width, height, stride, width / 2, height / 2, 10, intensity);
			break;
	    default:
		break;
	}	
	//alpha blend
	unsigned char* pMask = mask;
	unsigned char* pSrc = srcData;
	unsigned char* pGauss = gaussData;
	int k, nk;
	for(j = 0; j < height; j++)
	{
		for(i = 0; i < width; i++)
		{
			k = pMask[0];
			nk = 255 - k;
			pSrc[0] = CLIP3((pSrc[0] * k + pGauss[0] * nk) >> 8, 0, 255);
			pSrc[1] = CLIP3((pSrc[1] * k + pGauss[1] * nk) >> 8, 0, 255);
			pSrc[2] = CLIP3((pSrc[2] * k + pGauss[2] * nk) >> 8, 0, 255);
			pSrc += 4;
			pGauss += 4;
			pMask += 4;
		}
	}
	free(gaussData);
	return ret;
};
int f_Test(unsigned char *srcData, int width, int height, int stride, unsigned char* mask, int intensity, int mode)
{
	int ret = 0;
	//Process
    ret = f_Focus(srcData, width, height, stride, mask, intensity, mode);
	return ret;
};

这里本人给出自己的DEMO,这个DEMO中包含了四种效果(高斯模糊柔焦特效,马赛克柔焦特效,扩散模糊柔焦特效和放射模糊柔焦特效),下载地址:点击打开链接

本人QQ1358009172,欢迎交流学习!


作者:Trent1985 发表于2017/7/26 13:23:32 原文链接
阅读:76 评论:1 查看评论

Android 双开沙箱 VirturalApp 源码分析(一)

$
0
0

最近发现了一个非常好的开源项目,基本实现了一个 Android 上的沙箱环境,不过应用场景最多的还是应用双开。
VA github: https://github.com/asLody/VirtualApp
VA 的源码注释: https://github.com/ganyao114/VA_Doc
第一章主要是分析一下项目的整体结构。

包结构

这里写图片描述

android.content

主要是 PackageParser,该类型覆盖了系统的隐藏类 android.content.pm.PackageParser

com.lody.virtual

这里就是框架的主体代码了

client

运行在客户端的代码,指加载到 VA 中的子程序在被 VA 代理(hook)之后,所运行的代码

hook

hook java 层函数的一些代码

ipc

伪造的一些 framework 层的 IPC 服务类,诸如 ActivityManager,ServiceManager 等等,使用 VXXXXX 命名。hook 之后,子程序就会运行到这里而·不是原来真正的系统 framework 代码。

stub

系统四大组件的插桩,如提前注册在 Menifest 里的几十个 StubActivity。

remote

一些可序列化 Model,继承于 Parcelable

server

server 端代码,VA 伪造了一套 framework 层系统 service 的代码,他在一个独立的服务中记录管理组件的各种 Recorder,其逻辑其实与系统原生的相近,通过 Binder 与 client 端的 ipc 包中的 VXXXXManager 通讯。诸如 AMS(VAMS),PMS(VPMS)。

mirror

系统 framework 的镜像,实现了与 framework 层相对应的结构,封装了反射获取系统隐藏字段和方法的,便于直接调用获取或者赋值以及调用方法。

Base 一些基础措施的封装

Mirror framework 层镜像

成员变量 Field 映射

根据成员变量类型,映射类型分为几个基本数据类型和对象引用类型。下面就以对象引用类型为例,其他类型类似。

类型 RefObject 代表映射 framework 层同名的泛型类型成员变量。

// Field 映射
@SuppressWarnings("unchecked")
public class RefObject<T> {

    // framework 层对应的 Field
    private Field field;

    public RefObject(Class<?> cls, Field field) throws NoSuchFieldException {
        // 获取 framework 中同名字段的 field
        this.field = cls.getDeclaredField(field.getName());
        this.field.setAccessible(true);
    }

    // 获取变量值
    public T get(Object object) {
        try {
            return (T) this.field.get(object);
        } catch (Exception e) {
            return null;
        }
    }
    // 赋值
    public void set(Object obj, T value) {
        try {
            this.field.set(obj, value);
        } catch (Exception e) {
            //Ignore
        }
    }
}

以 framework 层中隐藏类 LoadedApk 来说:

public class LoadedApk {
    public static Class Class = RefClass.load(LoadedApk.class, "android.app.LoadedApk");
    public static RefObject<ApplicationInfo> mApplicationInfo;
    @MethodParams({boolean.class, Instrumentation.class})
    public static RefMethod<Application> makeApplication;

mApplicationInfo 就是 LoadedApk 中私有 ApplicationInfo 类型字段的同名映射。

当你引用 LoadedApk Mirror 类时,类加载器加载该类,执行静态成员的初始化表达式 RefClass.load(LoadedApk.class, “android.app.LoadedApk”); Mirror 类中的同名字段将被反射赋值。

下面看一下 RefClass.load() 函数

public static Class load(Class mappingClass, Class<?> realClass) {
        // 获取 Mirror 类的所有字段
        Field[] fields = mappingClass.getDeclaredFields();
        for (Field field : fields) {
            try {
                // 必须是 static 变量
                if (Modifier.isStatic(field.getModifiers())) {
                    // 从预设的 Map 中找到 RefXXXX 的构造器
                    Constructor<?> constructor = REF_TYPES.get(field.getType());
                    if (constructor != null) {
                        // 赋值
                        field.set(null, constructor.newInstance(realClass, field));
                    }
                }
            }
            catch (Exception e) {
                // Ignore
            }
        }
        return realClass;
    }

最后调用的话 MirrorClass.mirrorField.get(instance),MirrorClass.mirrorField.set(instance),就相当于直接调用 framework 层的隐藏字段了。

Method 映射

其实与 Field 类似,只是 Field 主要是一个 call 即调用方法。

@MethodParams({File.class, int.class})
public static RefMethod<PackageParser.Package> parsePackage;

表现在 Mirror 类型中也是一个字段,不过要在字段上边加上注解以标注参数类型。

当然还有一种情况,参数类型也是隐藏的,则要使用全限定名表示

 @MethodReflectParams({"android.content.pm.PackageParser$Package", "int"})
public static RefMethod<Void> collectCertificates;

Java 层 Hook

位于 com.lody.virtual.client.hook

Java 层使用了 Java 自带的动态代理

  1. MethodProxy
    Hook 点的代理接口,动态代理中的 call。

    重要的是这三个方法:

public boolean beforeCall(Object who, Method method, Object... args) {
        return true;
    }

public Object call(Object who, Method method, Object... args) throws Throwable {
        return method.invoke(who, args);
    }

public Object afterCall(Object who, Method method, Object[] args, Object result) throws Throwable {
        return result;
    }

以 hook getServices 为例:

static class GetServices extends MethodProxy {
        @Override
        public String getMethodName() {
            return "getServices";
        }

        @Override
        public Object call(Object who, Method method, Object... args) throws Throwable {
            int maxNum = (int) args[0];
            int flags = (int) args[1];
            return VActivityManager.get().getServices(maxNum, flags).getList();
        }

        @Override
        public boolean isEnable() {
            return isAppProcess();
        }
    }

A. getMethodName 是要 Hook 的方法名
B. Hook getServices 之后发现,真正返回服务的方法变成了仿造的 VActivityManager 对象。而在后面我们会知道这些服务最后都会从 VAMS 中获取,而不是原来的 AMS。
C. 实现了 isEnable 方法,这是 Hook 开关,如果返回 false 则不 Hook 该方法,而在这里的条件是,只有在子程序环境中 Hook,而宿主即框架是不需要 Hook 的,框架仍然需要连接真正的 AMS 以获取在系统 AMS 中注册的“外部” service。

  1. 那么上面这个 call 在哪里被调用呢?
    MethodInvocationStub
    这个桩对应一个需要 Hook 的类,各种 Method 可以在内部添加.
    我们需要专注这个方法
/**
     * Add a method proxy.
     *
     * @param methodProxy proxy
     */
    public MethodProxy addMethodProxy(MethodProxy methodProxy) {
        if (methodProxy != null && !TextUtils.isEmpty(methodProxy.getMethodName())) {
            if (mInternalMethodProxies.containsKey(methodProxy.getMethodName())) {
                VLog.w(TAG, "The Hook(%s, %s) you added has been in existence.", methodProxy.getMethodName(),
                        methodProxy.getClass().getName());
                return methodProxy;
            }
            mInternalMethodProxies.put(methodProxy.getMethodName(), methodProxy);
        }
        return methodProxy;
    }

这个也是关于动态代理的知识,这里的区别其实就是 Lody 对他做了一些接口的抽象,和一些诸如 Log 的封装。

添加 Hook Method 的方式有两个,一是调用 addMethodProxy,二是在 Stub 上添加 @Inject 注解,具体见下一段。

  1. 关于 MethodProxies
    叫这个名字的类很多,一个 MethodProxies 对应一个需要 Hook 的 framework 类型,需要 Hook 的方法以内部类(MethodProxy)的形式罗列在内部。
@Inject(MethodProxies.class)
public class LibCoreStub extends MethodInvocationProxy<MethodInvocationStub<Object>> {

将要 Hook 的方法集合 MethodProxies 绑定到 Stub 上。然后就是 Stub 对自己头上注解的解析,最终还是会调用到内部的 addMethodProxy 方法。

protected void onBindMethods() {

        if (mInvocationStub == null) {
            return;
        }
        Class<? extends MethodInvocationProxy> clazz = getClass();
        Inject inject = clazz.getAnnotation(Inject.class);
        if (inject != null) {
            Class<?> proxiesClass = inject.value();
            Class<?>[] innerClasses = proxiesClass.getDeclaredClasses();
            // 遍历内部类
            for (Class<?> innerClass : innerClasses) {
                if (!Modifier.isAbstract(innerClass.getModifiers())
                        && MethodProxy.class.isAssignableFrom(innerClass)
                        && innerClass.getAnnotation(SkipInject.class) == null) {
                    addMethodProxy(innerClass);
                }
            }

        }
    }

运行时结构

这点很重要,VA 在运行时并不是一个简单的单进程的库,其需要在系统调用到其预先注册的 Stub 组件之后接手系统代理 Client App 的 四大组件,包括生命周期等一切事物。
VA 参照原生系统 framework 仿造了一套 framework service,还有配套在 client 端的 framework 库。

  1. 首先来看一下系统原生的 framework 运作方式
    简单来说,我们平时所用到的 app 运行空间中的 framework api 最终会通过 Binder 远程调用到 framework service 空间的远程服务。
    而远程服务类似 AMS 中的 Recoder 中会持有 app 空间的 Ibinder token 句柄,通过 token 也可以让 framework service 远程调用到 app 空间。

    这里写图片描述

  2. VA 环境下:
    而在 VA 环境下,情况其实也是类似,只不过在 framework service 和 client app 之间还有另外一个 VA 实现的 VAService,VAService 仿造了 framework service 的一些功能。
    因为在 VA 中运行的 Client App 都是没有(也不能注册)在 framework service 的,注册的只有 VA 预先注册在 Menifest 中的 Stub 而已。所以 frameservice 是无法像普通 App 一样管理 VA Client App 的会话的。
    这就要依靠 VA 仿造的另外一套 VAService 完成对 VA 中 Client App 的会话管理了。

    这里写图片描述

    需要注意的是 VA Client 获取 VA Service 的 IBinder 句柄是统一通过 IServiceFetcher 这个句柄,这个看上去有些奇怪。而获得 IServiceFetcher 本身的方式是通过 ContentProvider,可能是 Provider call 的性能较好一些。

作者:ganyao939543405 发表于2017/7/26 14:46:15 原文链接
阅读:23 评论:0 查看评论

React Native入门(七)之列表组件的使用(2)关于FlatList的一切

$
0
0

前言

在上一篇博客中了解了列表组件的相关内容,主要是静态的展示了一个列表数据,了解了ScrollVIew,FlatList和SectionList的基本用法,本篇文章就深入的了解一个常用的一个列表组件FlatList的用法!

属性

添加头部组件

ListHeaderComponent属性用来给FlatList添加头部组件
简单使用:

//ES6之前写法
_header = function () {
  return (
    <Text style={{fontWeight: 'bold', fontSize: 20}}>热门电影</Text>
  );
}


<FlatList
ListHeaderComponent={this._header}//header头部组件
/>

添加尾部组件

ListFooterComponent属性为FlatList添加尾部组件,接收的参数跟ListHeaderComponent相同。

//ES6的写法
_footer = () => (
  <Text style={{fontSize: 14, alignSelf: 'center'}}>到底啦,没有啦!</Text>
)

<FlatList
  ListFooterComponent={this._footer} //添加尾部组件
/>

添加头部和尾部组件比较简单,需要注意的就是上边两者ES5和6写法的区别!

添加分割线

ItemSeparatorComponent属性可以为FlatList列表之间添加分割线。
举个例子:

class ItemDivideComponent extends Component {
  render() {
    return (
      <View style={{height: 1, backgroundColor: 'skyblue'}}/>
    );
  }
};


<FlatList
  ItemSeparatorComponent={ItemDivideComponent}//分割线组件
/>

这里我们自定义了一个组件来设置分割线,当然我们像添加头部和尾部一样,在内部声明之后使用this._header的写法也是可以的!

设置空数据视图

ListEmptyComponent属性,可以为FlatList设置一个没有数据的时候展示的视图!,这个属性可以接收的参数类型比较多,可以是React Component,也可以是一个render函数,或者渲染好的element
所以设置空数据视图不仅可以像前边介绍的两种方式以外,还可以接收一个render函数
举个例子:

createEmptyView() {
  return (
   <Text style={{fontSize: 40, alignSelf: 'center'}}>还没有数据哦!</Text>
  );
}

<FlatList
  ListEmptyComponent={this.createEmptyView()}
/>

设置item的key

在前一篇博客中,我们的设置data的时候,是这样的:

data={[
  {key: '大护法'},
  {key: '绣春刀II:修罗战场'},
  ...         
]}

类似{key:你的数据value}这样的形式,是因为我们在设置data的时候,必须要为item设置key属性,否则会有一个黄色的警告弹出。而且我们需要注意的是这里每一个item的key是唯一的!,如果按照这样的写法,我们在数据中有重复的,比如{key: '大护法'},{key: '大护法'},这里的大护法只会显示一个,因为FlatList会认为这是一条数据,因为key相同!
那么为什么会这样?
因为FlatList中有一个属性:keyExtractor,用于为给定的item生成一个不重复的key若不指定此函数,则默认抽取item.key作为key值。若item.key也不存在,则使用数组下标index。因为前边没有指定该属性,所以就把item.key作为了key值,才会认定两个重复的数据是一条数据!

那么一般地,我们可以这样使用:

_keyExtractor = (item, index) => index;

<FlatList
  keyExtractor={this._keyExtractor}
/>

这样就把data中数组的下标作为了唯一的key
那么在data中指定数据的时候,就不用{key: '大护法'}这样写了,因为我们已经指定了唯一的key,而可以随意写{name: '大护法'}或者{movie: '大护法'},在渲染item的时候,取值用item.name或者item.movie即可!也不会有黄色的警告出现!

你明白了吗?

设置itemLayout

getItemLayout属性是一个可选的优化,用于避免动态测量内容尺寸的开销。如果我们知道item的高度,就可以为FlatList指定这一个属性,来使FlatList更加高效的运行!
举个例子:

getItemLayout={(data, index) => ({
  length: 44, offset: (44 + 1) * index, index
})}

我们在上边使用的时候指定了item的高度为44,所以length参数为44;我们设置了分割线,且指定分割线的高度是1,所以offset参数为44+1。综合来看,设置这个属性应这样写:

getItemLayout={(data, index) => ({
  length: 你的itemheight, offset: (你的itemheight + ItemSeparatorheight) * index, index
})}

设置这一属性,在调用FlatList的跳转函数的时候非常有用,否则可能会很卡顿!如scrollToEnd(),scrollToIndex(),这两个方法后边再说!

下拉刷新

FlatList中有两个属性,可以用来设置下拉刷新。

  • refreshing在等待加载新数据时将此属性设为true,列表就会显示出一个正在加载的符号.
  • onRefresh如果设置了此选项,则会在列表头部添加一个标准的RefreshControl控件,以便实现“下拉刷新”的功能。同时你需要正确设置refreshing属性。

这里的RefreshControl控件,非常类似于Android v4包中的SwipeRefreshLayout,这里就不多说了,需要了解的可以查看相关文档!

如何使用,举个例子:

refreshing={this.state.refreshing}
onRefresh={() => {
  this.setState({refreshing: true})//开始刷新
  //这里模拟请求网络,拿到数据,3s后停止刷新
  setTimeout(() => {
    alert('没有可刷新的内容!');
    this.setState({refreshing: false});//停止刷新
  }, 3000);
}}

上拉加载

关于上拉加载,FlatList也封装有两个属性来实现:

  • onEndReachedThreshold:这个属性决定当距离内容最底部还有多远时触发onEndReached回调。需要注意的是此参数是一个比值而非像素单位。比如,0.5表示距离内容最底部的距离为当前列表可见长度的一半时触发。所以它的取值范围为:(0,1),不包含0和1。
  • onEndReached:列表被滚动到距离内容最底部不足onEndReachedThreshold设置的的距离时调用。

具体使用,举个例子:

onEndReachedThreshold={0.1}
onEndReached={({distanceFromEnd}) => (
  setTimeout(() => {
    this.setState((state) => ({
      data: state.data.concat(this._newData),
    }));
  }, 3000)
)}

这里我们设置的距离为列表可见长度的1/10,而触发了onEndReached函数时,我们设置了一个定时器,3s后,将data中的数据添加了新数据,从而达到上拉加载更多的效果!

函数

介绍

FlatList下有两个比较常用的函数:

  • scrollToEnd() 滚动到底部。如果不设置getItemLayout属性的话,可能会比较卡。
  • scrollToIndex()如果不设置getItemLayout属性的话,无法跳转到当前可视区域以外的位置。

如官方所言,使用这两个函数的时候,最好指定设置getItemLayout属性

使用

因为这两个是FlatList组件的函数,所以在使用这两个函数之前,首先我们要得到FlatList组件的引用。
这时候就需要ref属性。React提供的这个ref属性,表示为对组件真正实例的引用
关于ref属性的使用,可以去React官网查看API!

具体的使用:

ref={(flatList) => this._flatList = flatList}


this._flatList.scrollToEnd();

//viewPosition参数:0表示顶部,0.5表示中部,1表示底部
this._flatList.scrollToIndex({viewPosition: 0, index: this.state.text});

至此,关于FlatList进阶的相关基础内容就说完了!

完整的Demo

下面看一下完整的demo:
这里写图片描述

源码:

class FlatListTest extends Component {
  constructor(props) {
    super(props);
    this.state = {
      data: this._sourceData,
      refreshing: false, //初始化不刷新
      text: ''//跳转的行
    };
  }

  _header = function () {
    return (
      <Text style={{fontWeight: 'bold', fontSize: 20}}>热门电影</Text>
    );
  }

  _footer = () => (
    <Text style={{fontSize: 14, alignSelf: 'center'}}>到底啦,没有啦!</Text>
  )

  createEmptyView() {
    return (
      <Text style={{fontSize: 40, alignSelf: 'center'}}>还没有数据哦!</Text>
    );
  }

  //此函数用于为给定的item生成一个不重复的key
  //若不指定此函数,则默认抽取item.key作为key值。若item.key也不存在,则使用数组下标index。
  _keyExtractor = (item, index) => index;

  itemClick(item, index) {
    alert('点击了第' + index + '项,电影名称为:' + item.name);
  }

  _renderItem = ({item, index}) => {
    return (
      <TouchableOpacity
        activeOpacity={0.5}
        onPress={this.itemClick.bind(this, item, index)}>
        <Text style={flatListStyles.item}>{item.name}</Text>
      </TouchableOpacity>
    );
  }

  //点击按钮跳转
  onButtonPress() {
    //viewPosition参数:0表示顶部,0.5表示中部,1表示底部
    this._flatList.scrollToIndex({viewPosition: 0, index: this.state.text});
    //this._flatList.scrollToOffset({ animated: true, offset: 2000 });
  };

  onBtnPress2Botton() {
    this._flatList.scrollToEnd();
  }

  _sourceData = [
    {name: '大护法'},
    {name: '绣春刀II:修罗战场'},
    {name: '神偷奶爸3'},
    {name: '神奇女侠'},
    {name: '摔跤吧,爸爸'},
    {name: '悟空传'},
    {name: '闪光少女'},
    {name: '攻壳机动队'},
    {name: '速度与激情8'},
    {name: '蝙蝠侠大战超人'},
    {name: '攻壳机动队'},
    {name: '速度与激情8'},
    {name: '蝙蝠侠大战超人'}
  ]

  _newData = [{name: '我是新添加的数据1'},
    {name: '我是新添加的数据2'},
    {name: '我是新添加的数据3'}]

  render() {
    return (
      <View style={flatListStyles.container}>
        <View style={{flexDirection: 'row', justifyContent: 'center', alignItems: 'center'}}>
          <TextInput
            style={{flex: 1}}
            placeholder="请输入要跳转的行号"
            onChangeText={(text) => this.setState({text})}
          />
          <Button title="跳转到行" onPress={this.onButtonPress.bind(this)} color={'skyblue'}/>
          <Button title="跳转到底部" onPress={this.onBtnPress2Botton.bind(this)} color={'green'}/>

        </View>
        <FlatList
          data={this.state.data}
          //使用 ref 可以获取到相应的组件
          ref={(flatList) => this._flatList = flatList}
          ListHeaderComponent={this._header}//header头部组件
          ListFooterComponent={this._footer}//footer尾部组件
          ItemSeparatorComponent={ItemDivideComponent}//分割线组件
          //空数据视图,可以是React Component,也可以是一个render函数,或者渲染好的element。
          ListEmptyComponent={this.createEmptyView()}
          keyExtractor={this._keyExtractor}
          //是一个可选的优化,用于避免动态测量内容尺寸的开销,不过前提是你可以提前知道内容的高度。
          //如果你的行高是固定的,getItemLayout用起来就既高效又简单.
          //注意如果你指定了SeparatorComponent,请把分隔线的尺寸也考虑到offset的计算之中
          getItemLayout={(data, index) => ( {length: 44, offset: (44 + 1) * index, index} )}
          //决定当距离内容最底部还有多远时触发onEndReached回调。
          //注意此参数是一个比值而非像素单位。比如,0.5表示距离内容最底部的距离为当前列表可见长度的一半时触发。
          onEndReachedThreshold={0.1}
          //当列表被滚动到距离内容最底部不足onEndReachedThreshold的距离时调用
          onEndReached={({distanceFromEnd}) => (
            setTimeout(() => {
              this.setState((state) => ({
                data: state.data.concat(this._newData),
              }));
            }, 3000)
          )}
          refreshing={this.state.refreshing}
          onRefresh={() => {
            this.setState({refreshing: true})//开始刷新
            //这里模拟请求网络,拿到数据,3s后停止刷新
            setTimeout(() => {
              alert('没有可刷新的内容!');
              this.setState({refreshing: false});
            }, 3000);
          }}
          renderItem={this._renderItem}
        />
      </View>
    );
  }
}
;

class ItemDivideComponent
  extends Component {
  render() {
    return (
      <View style={{height: 1, backgroundColor: 'skyblue'}}/>
    );
  }
}
;

const flatListStyles = StyleSheet.create({
  container: {
    flex: 1,
    paddingTop: 22
  },
  item: {
    padding: 10,
    fontSize: 18,
    height: 44,
  },
})

AppRegistry.registerComponent('AwesomeProject', () => FlatListTest);

结语

好了,关于FlatList基本的进阶内容都在这里了,我也是初学者,如果有什么错误或者疑问,还请不吝赐教!
我们下一篇再见!

作者:aiynmimi 发表于2017/7/26 19:07:20 原文链接
阅读:1 评论:0 查看评论

Android 双开沙箱 VirtualApp 源码分析(三)App 启动

$
0
0

上一章:Android 双开沙箱 VirtualApp 源码分析(二)

在这之前,我们还是要先了解一下 VA Client Framework 和 VAService 之间的通讯方式

VAService 与通讯

VAService

首先,VAService 是指 VA 仿造 Android 原生 framework 层 Service 实现的一套副本,举例有 VActivityManagerService,它和系统 AMS 一样,只不过他管理的是 VA 内部 Client App 的组件会话。

这里写图片描述

VAService 统一管理

首先所有 VAService 直接继承与 XXX.Stub,也就是 Binder,并且直接使用了一个 Map 储存在 VAService 进程空间中,并没有注册到系统 AMS 中,事实上在 VAService 进程中,每个 Service 都被当作一个普通对象 new 和 初始化。
最终,他们被添加到了 ServiceCache 中:

public class ServiceCache {

    private static final Map<String, IBinder> sCache = new ArrayMap<>(5);

    public static void addService(String name, IBinder service) {
        sCache.put(name, service);
    }

    public static IBinder removeService(String name) {
        return sCache.remove(name);
    }

    public static IBinder getService(String name) {
        return sCache.get(name);
    }

}

这个 cache 很简单,就是一个 Map。

而被添加的时机则在 BinderProvider 的 onCreate() 回掉中:

@Override
    public boolean onCreate() {
        Context context = getContext();
        // 这是一个空前台服务,目的是为了保活 VAService 进程,即 :x 进程
        DaemonService.startup(context);
        if (!VirtualCore.get().isStartup()) {
            return true;
        }
        VPackageManagerService.systemReady();
        addService(ServiceManagerNative.PACKAGE, VPackageManagerService.get());
        VActivityManagerService.systemReady(context);
        addService(ServiceManagerNative.ACTIVITY, VActivityManagerService.get());
        addService(ServiceManagerNative.USER, VUserManagerService.get());
        VAppManagerService.systemReady();
        addService(ServiceManagerNative.APP, VAppManagerService.get());
        BroadcastSystem.attach(VActivityManagerService.get(), VAppManagerService.get());
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            addService(ServiceManagerNative.JOB, VJobSchedulerService.get());
        }
        VNotificationManagerService.systemReady(context);
        addService(ServiceManagerNative.NOTIFICATION, VNotificationManagerService.get());
        VAppManagerService.get().scanApps();
        VAccountManagerService.systemReady();
        addService(ServiceManagerNative.ACCOUNT, VAccountManagerService.get());
        addService(ServiceManagerNative.VS, VirtualStorageService.get());
        addService(ServiceManagerNative.DEVICE, VDeviceManagerService.get());
        return true;
    }
    private void addService(String name, IBinder service) {
        ServiceCache.addService(name, service);
    }

需要注意的是 DeamonService 是一个空前台服务,目的是为了保活 VAService 进程,即 :x 进程,因为现在后台服务很容易被杀,在 Android 8.0 以后后台服务只能在后台存活5S,而前台服务则不受影响。

ServiceFetcher

VA 设计了一个单独的 ServiceFetcher 服务用于向外部暴露 VAService 中的所有服务的 IBinder 句柄,而 ServiceFetcher 本身也是 Binder 服务,也就是说,ServiceFetcher 的 Ibinder 句柄是拿到其他 VAServic IBinder 的钥匙。

    private class ServiceFetcher extends IServiceFetcher.Stub {
        @Override
        public IBinder getService(String name) throws RemoteException {
            if (name != null) {
                return ServiceCache.getService(name);
            }
            return null;
        }

        @Override
        public void addService(String name, IBinder service) throws RemoteException {
            if (name != null && service != null) {
                ServiceCache.addService(name, service);
            }
        }

        @Override
        public void removeService(String name) throws RemoteException {
            if (name != null) {
                ServiceCache.removeService(name);
            }
        }
    }

ServicecFetcher 自身的 IBnder 则通过 BinderProvicer 这个ContentProvider 暴露给其他进程:

@Override
    public Bundle call(String method, String arg, Bundle extras) {
        if ("@".equals(method)) {
            Bundle bundle = new Bundle();
            BundleCompat.putBinder(bundle, "_VA_|_binder_", mServiceFetcher);
            return bundle;
        }
        return null;
    }

那么,在 Client App 中 VA Client 就可以通过 IServiceFetcher 这个 IBinder 拿到其他服务的 IBinder 了:

    private static IServiceFetcher sFetcher;

    private static IServiceFetcher getServiceFetcher() {
        if (sFetcher == null || !sFetcher.asBinder().isBinderAlive()) {
            synchronized (ServiceManagerNative.class) {
                Context context = VirtualCore.get().getContext();
                Bundle response = new ProviderCall.Builder(context, SERVICE_CP_AUTH).methodName("@").call();
                if (response != null) {
                    IBinder binder = BundleCompat.getBinder(response, "_VA_|_binder_");
                    linkBinderDied(binder);
                    sFetcher = IServiceFetcher.Stub.asInterface(binder);
                }
            }
        }
        return sFetcher;
    }

    // 返回服务的 IBinder 句柄
    public static IBinder getService(String name) {
        // 如果是本地服务,直接本地返回
        if (VirtualCore.get().isServerProcess()) {
            return ServiceCache.getService(name);
        }
        // 通过 ServiceFetcher 的句柄找到远程 Service 的句柄
        IServiceFetcher fetcher = getServiceFetcher();
        if (fetcher != null) {
            try {
                return fetcher.getService(name);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
        VLog.e(TAG, "GetService(%s) return null.", name);
        return null;
    }

启动 App

首先要了解的是 Android App 是组件化的,Apk 其实是 N 多个组件的集合,以及一些资源文件和 Assert,App 的启动有多种情况,只要在一个新的进程中调起了 apk 中任何一个组件,App 将被初始化,Application 将被初始化。

启动 Activity

我们先看启动 Activity 的情况:

Hook startActivity(重定位 Intent 到 StubActivity)

首先在 Client App 中,startActivity 方法必须被 Hook 掉,不然 Client App 调用 startActivity 就直指外部 Activity 去了。

这部分的原理其实与 DroidPlugin 大同小异,由于插件(Client App)中的 Activity 是没有在 AMS 中注册的,AMS 自然无法找到我们的插件 Activity。

Hook 的目的是我们拿到用户的 Intent,把他替换成指向 VA 在 Menifest 中站好坑的 StubActivity 的 Intent,然后将原 Intent 当作 data 打包进新 Intent 以便日后流程再次进入 VA 时恢复。

Hook 的方法就是用我们动态代理生成的代理类对象替换系统原来的 ActiityManagerNative.geDefault 对象。

@Override
    public void inject() throws Throwable {
        if (BuildCompat.isOreo()) {
            //Android Oreo(8.X)
            Object singleton = ActivityManagerOreo.IActivityManagerSingleton.get();
            Singleton.mInstance.set(singleton, getInvocationStub().getProxyInterface());
        } else {
            if (ActivityManagerNative.gDefault.type() == IActivityManager.TYPE) {
                ActivityManagerNative.gDefault.set(getInvocationStub().getProxyInterface());
            } else if (ActivityManagerNative.gDefault.type() == Singleton.TYPE) {
                Object gDefault = ActivityManagerNative.gDefault.get();
                Singleton.mInstance.set(gDefault, getInvocationStub().getProxyInterface());
            }
        }
        BinderInvocationStub hookAMBinder = new BinderInvocationStub(getInvocationStub().getBaseInterface());
        hookAMBinder.copyMethodProxies(getInvocationStub());
        ServiceManager.sCache.get().put(Context.ACTIVITY_SERVICE, hookAMBinder);
    }

好了,下面只要调用到 startActivity 就会被 Hook 到 call。
这个函数需要注意以下几点:
1. VA 有意将安装和卸载 APP 的请求重定向到了卸载 VA 内部 APK 的逻辑。
2. resolveActivityInfo 调用到了 VPM 的 resolveIntent,最终会远程调用到 VPMS 的 resolveIntent,然后 VPMS 就会去查询 VPackage 找到目标 Activity 并将信息附加在 ResolveInfo 中返回 VPM。
3. 最后也是最重要的一点,startActivity 会调用到 VAM.startActivity,同样最终会远程调用到 VAMS 的 startActivity。

// Hook startActivity
    static class StartActivity extends MethodProxy {

        private static final String SCHEME_FILE = "file";
        private static final String SCHEME_PACKAGE = "package";

        @Override
        public String getMethodName() {
            return "startActivity";
        }

        @Override
        public Object call(Object who, Method method, Object... args) throws Throwable {
            int intentIndex = ArrayUtils.indexOfObject(args, Intent.class, 1);
            if (intentIndex < 0) {
                return ActivityManagerCompat.START_INTENT_NOT_RESOLVED;
            }
            int resultToIndex = ArrayUtils.indexOfObject(args, IBinder.class, 2);
            String resolvedType = (String) args[intentIndex + 1];
            Intent intent = (Intent) args[intentIndex];
            intent.setDataAndType(intent.getData(), resolvedType);
            IBinder resultTo = resultToIndex >= 0 ? (IBinder) args[resultToIndex] : null;
            int userId = VUserHandle.myUserId();

            if (ComponentUtils.isStubComponent(intent)) {
                return method.invoke(who, args);
            }

            // 请求安装和卸载界面
            if (Intent.ACTION_INSTALL_PACKAGE.equals(intent.getAction())
                    || (Intent.ACTION_VIEW.equals(intent.getAction())
                    && "application/vnd.android.package-archive".equals(intent.getType()))) {
                if (handleInstallRequest(intent)) {
                    return 0;
                }
            } else if ((Intent.ACTION_UNINSTALL_PACKAGE.equals(intent.getAction())
                    || Intent.ACTION_DELETE.equals(intent.getAction()))
                    && "package".equals(intent.getScheme())) {

                if (handleUninstallRequest(intent)) {
                    return 0;
                }
            }

            String resultWho = null;
            int requestCode = 0;
            Bundle options = ArrayUtils.getFirst(args, Bundle.class);
            if (resultTo != null) {
                resultWho = (String) args[resultToIndex + 1];
                requestCode = (int) args[resultToIndex + 2];
            }
            // chooser 调用选择界面
            if (ChooserActivity.check(intent)) {
                intent.setComponent(new ComponentName(getHostContext(), ChooserActivity.class));
                intent.putExtra(Constants.EXTRA_USER_HANDLE, userId);
                intent.putExtra(ChooserActivity.EXTRA_DATA, options);
                intent.putExtra(ChooserActivity.EXTRA_WHO, resultWho);
                intent.putExtra(ChooserActivity.EXTRA_REQUEST_CODE, requestCode);
                return method.invoke(who, args);
            }

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
                args[intentIndex - 1] = getHostPkg();
            }

            //解析 ActivityInfo
            ActivityInfo activityInfo = VirtualCore.get().resolveActivityInfo(intent, userId);
            if (activityInfo == null) {
                VLog.e("VActivityManager", "Unable to resolve activityInfo : " + intent);
                if (intent.getPackage() != null && isAppPkg(intent.getPackage())) {
                    return ActivityManagerCompat.START_INTENT_NOT_RESOLVED;
                }
                return method.invoke(who, args);
            }

            // 调用远程 VAMS.startActivity
            int res = VActivityManager.get().startActivity(intent, activityInfo, resultTo, options, resultWho, requestCode, VUserHandle.myUserId());
            if (res != 0 && resultTo != null && requestCode > 0) {
                VActivityManager.get().sendActivityResult(resultTo, resultWho, requestCode);
            }

            // 处理 Activity 切换动画,因为此时动画还是 Host 的 Stub Activity 默认动画,需要覆盖成子程序包的动画
            if (resultTo != null) {
                ActivityClientRecord r = VActivityManager.get().getActivityRecord(resultTo);
                if (r != null && r.activity != null) {
                    try {
                        TypedValue out = new TypedValue();
                        Resources.Theme theme = r.activity.getResources().newTheme();
                        theme.applyStyle(activityInfo.getThemeResource(), true);
                        if (theme.resolveAttribute(android.R.attr.windowAnimationStyle, out, true)) {

                            TypedArray array = theme.obtainStyledAttributes(out.data,
                                    new int[]{
                                            android.R.attr.activityOpenEnterAnimation,
                                            android.R.attr.activityOpenExitAnimation
                                    });

                            r.activity.overridePendingTransition(array.getResourceId(0, 0), array.getResourceId(1, 0));
                            array.recycle();
                        }
                    } catch (Throwable e) {
                        // Ignore
                    }
                }
            }
            return res;
        }


        private boolean handleInstallRequest(Intent intent) {
            IAppRequestListener listener = VirtualCore.get().getAppRequestListener();
            if (listener != null) {
                Uri packageUri = intent.getData();
                if (SCHEME_FILE.equals(packageUri.getScheme())) {
                    File sourceFile = new File(packageUri.getPath());
                    try {
                        listener.onRequestInstall(sourceFile.getPath());
                        return true;
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                }

            }
            return false;
        }

        private boolean handleUninstallRequest(Intent intent) {
            IAppRequestListener listener = VirtualCore.get().getAppRequestListener();
            if (listener != null) {
                Uri packageUri = intent.getData();
                if (SCHEME_PACKAGE.equals(packageUri.getScheme())) {
                    String pkg = packageUri.getSchemeSpecificPart();
                    try {
                        listener.onRequestUninstall(pkg);
                        return true;
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                }

            }
            return false;
        }

    }

逻辑最终走到 VAMS 后,VAMS 调用 ActivityStack.startActivityLocked

    // 参考 framework 的实现
    int startActivityLocked(int userId, Intent intent, ActivityInfo info, IBinder resultTo, Bundle options,
                            String resultWho, int requestCode) {
        optimizeTasksLocked();

        Intent destIntent;
        ActivityRecord sourceRecord = findActivityByToken(userId, resultTo);
        TaskRecord sourceTask = sourceRecord != null ? sourceRecord.task : null;

        // 忽略一大堆对 Flag 的处理
        .............................

        String affinity = ComponentUtils.getTaskAffinity(info);

        // 根据 Flag 寻找合适的 Task
        TaskRecord reuseTask = null;
        switch (reuseTarget) {
            case AFFINITY:
                reuseTask = findTaskByAffinityLocked(userId, affinity);
                break;
            case DOCUMENT:
                reuseTask = findTaskByIntentLocked(userId, intent);
                break;
            case CURRENT:
                reuseTask = sourceTask;
                break;
            default:
                break;
        }

        boolean taskMarked = false;
        if (reuseTask == null) {
            startActivityInNewTaskLocked(userId, intent, info, options);
        } else {
            boolean delivered = false;
            mAM.moveTaskToFront(reuseTask.taskId, 0);
            boolean startTaskToFront = !clearTask && !clearTop && ComponentUtils.isSameIntent(intent, reuseTask.taskRoot);

            if (clearTarget.deliverIntent || singleTop) {
                taskMarked = markTaskByClearTarget(reuseTask, clearTarget, intent.getComponent());
                ActivityRecord topRecord = topActivityInTask(reuseTask);
                if (clearTop && !singleTop && topRecord != null && taskMarked) {
                    topRecord.marked = true;
                }
                // Target activity is on top
                if (topRecord != null && !topRecord.marked && topRecord.component.equals(intent.getComponent())) {
                    deliverNewIntentLocked(sourceRecord, topRecord, intent);
                    delivered = true;
                }
            }
            if (taskMarked) {
                synchronized (mHistory) {
                    scheduleFinishMarkedActivityLocked();
                }
            }
            if (!startTaskToFront) {
                if (!delivered) {
                    destIntent = startActivityProcess(userId, sourceRecord, intent, info);
                    if (destIntent != null) {
                        startActivityFromSourceTask(reuseTask, destIntent, info, resultWho, requestCode, options);
                    }
                }
            }
        }
        return 0;
    }

然后 call 到了 startActivityProcess ,这就是真正替换 Intent 的地方:

// 使用 Host Stub Activity 的 Intent 包装原 Intent 瞒天过海
    private Intent startActivityProcess(int userId, ActivityRecord sourceRecord, Intent intent, ActivityInfo info) {
        intent = new Intent(intent);
        // 获得 Activity 对应的 ProcessRecorder,如果没有则表示这是 Process 第一个打开的组件,需要初始化 Application
        ProcessRecord targetApp = mService.startProcessIfNeedLocked(info.processName, userId, info.packageName);
        if (targetApp == null) {
            return null;
        }
        Intent targetIntent = new Intent();

        // 根据 Client App 的 PID 获取 StubActivity
        String stubActivityPath = fetchStubActivity(targetApp.vpid, info);

        Log.e("gy", "map activity:" + intent.getComponent().getClassName() + " -> " + stubActivityPath);

        targetIntent.setClassName(VirtualCore.get().getHostPkg(), stubActivityPath);
        ComponentName component = intent.getComponent();
        if (component == null) {
            component = ComponentUtils.toComponentName(info);
        }
        targetIntent.setType(component.flattenToString());
        StubActivityRecord saveInstance = new StubActivityRecord(intent, info,
                sourceRecord != null ? sourceRecord.component : null, userId);
        saveInstance.saveToIntent(targetIntent);
        return targetIntent;
    }

fetchStubActivity 会根据相同的进程 id 在 VA 的 Menifest 中找到那个提前占坑的 StubActivity:

// 获取合适的 StubActivity,返回 StubActivity 全限定名
    private String fetchStubActivity(int vpid, ActivityInfo targetInfo) {

        boolean isFloating = false;
        boolean isTranslucent = false;
        boolean showWallpaper = false;
        try {
            int[] R_Styleable_Window = R_Hide.styleable.Window.get();
            int R_Styleable_Window_windowIsTranslucent = R_Hide.styleable.Window_windowIsTranslucent.get();
            int R_Styleable_Window_windowIsFloating = R_Hide.styleable.Window_windowIsFloating.get();
            int R_Styleable_Window_windowShowWallpaper = R_Hide.styleable.Window_windowShowWallpaper.get();

            AttributeCache.Entry ent = AttributeCache.instance().get(targetInfo.packageName, targetInfo.theme,
                    R_Styleable_Window);
            if (ent != null && ent.array != null) {
                showWallpaper = ent.array.getBoolean(R_Styleable_Window_windowShowWallpaper, false);
                isTranslucent = ent.array.getBoolean(R_Styleable_Window_windowIsTranslucent, false);
                isFloating = ent.array.getBoolean(R_Styleable_Window_windowIsFloating, false);
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }

        boolean isDialogStyle = isFloating || isTranslucent || showWallpaper;

        // 根据在 Menifest 中注册的 pid
        if (isDialogStyle) {
            return VASettings.getStubDialogName(vpid);
        } else {
            return VASettings.getStubActivityName(vpid);
        }
    }

这里需要特别注意,VA 占坑的方式和 DroidPlugin 有些小不同,VA 没有为每个 Process 注册多个 Activity,也没有为不同的启动方式注册多个 Activity,这里确实是有改进的。
这里根本原因是因为 VA 对 VAMS 实现的更为完整,实现了原版 AMS 的基本功能,包括完整的 Recorder 管理,Task Stack 管理等,这样的话 StubActivity 的唯一作用便是携带 Client App 真正的 Intent 交给 VAMS 处理。这套机制衍生到其他的组件也是一样的。

可以简单看一下 ActivityStack, ActivityRecorder,ActivityRecord

 class ActivityStack {

    private final ActivityManager mAM;
    private final VActivityManagerService mService;

    /**
     * [Key] = TaskId [Value] = TaskRecord
     */
    private final SparseArray<TaskRecord> mHistory = new SparseArray<>();
class TaskRecord {
    public final List<ActivityRecord> activities = Collections.synchronizedList(new ArrayList<ActivityRecord>());
    public int taskId;
    public int userId;
    public String affinity;
    public Intent taskRoot;
/* package */ class ActivityRecord {
    public TaskRecord task;
    public ComponentName component;
    public ComponentName caller;
    // Client App 中 Activity 的句柄
    public IBinder token;
    public int userId;
    public ProcessRecord process;
    public int launchMode;
    public int flags;
    public boolean marked;
    public String affinity;

StubActivityRecorder:

public class StubActivityRecord  {
        public Intent intent;
        public ActivityInfo info;
        public ComponentName caller;
        public int userId;

        public StubActivityRecord(Intent intent, ActivityInfo info, ComponentName caller, int userId) {
            this.intent = intent;
            this.info = info;
            this.caller = caller;
            this.userId = userId;
        }

        // 获取原版 Intent 和一些其他信息
        public StubActivityRecord(Intent stub) {
            this.intent = stub.getParcelableExtra("_VA_|_intent_");
            this.info = stub.getParcelableExtra("_VA_|_info_");
            this.caller = stub.getParcelableExtra("_VA_|_caller_");
            this.userId = stub.getIntExtra("_VA_|_user_id_", 0);
        }

    // 将原版 Intent 塞到 Stub Intent
    public void saveToIntent(Intent stub) {
        stub.putExtra("_VA_|_intent_", intent);
        stub.putExtra("_VA_|_info_", info);
        stub.putExtra("_VA_|_caller_", caller);
        stub.putExtra("_VA_|_user_id_", userId);
    }
}

初始化 Application

还有一个非常重要的事情,注意到这一行:

// 获得 Activity 对应的 ProcessRecorder,如果没有则表示这是 Process 第一个打开的组件,需要初始化 Application
        ProcessRecord targetApp = mService.startProcessIfNeedLocked(info.processName, userId, info.packageName);

这里会先去找对应 Client App 进程的 ProcessRecorder,找不到代表 Application 刚启动尚未初始化:

private ProcessRecord performStartProcessLocked(int vuid, int vpid, ApplicationInfo info, String processName) {
        ProcessRecord app = new ProcessRecord(info, processName, vuid, vpid);
        Bundle extras = new Bundle();
        BundleCompat.putBinder(extras, "_VA_|_binder_", app);
        extras.putInt("_VA_|_vuid_", vuid);
        extras.putString("_VA_|_process_", processName);
        extras.putString("_VA_|_pkg_", info.packageName);

        // 调用子程序包的 init_process 方法,并且得到子程序包 IBinder 句柄
        Bundle res = ProviderCall.call(VASettings.getStubAuthority(vpid), "_VA_|_init_process_", null, extras);
        if (res == null) {
            return null;
        }
        int pid = res.getInt("_VA_|_pid_");
        IBinder clientBinder = BundleCompat.getBinder(res, "_VA_|_client_");
        // attach 到 Client 的 VAM , 将子程序句柄推入服务端 VAMS
        attachClient(pid, clientBinder);
        return app;
    }

ProviderCall.call 向 Client App 的 StubContentProvider 发起远程调用:

@Override
    public Bundle call(String method, String arg, Bundle extras) {
        if ("_VA_|_init_process_".equals(method)) {
            return initProcess(extras);
        }
        return null;
    }

    private Bundle initProcess(Bundle extras) {
        ConditionVariable lock = VirtualCore.get().getInitLock();
        if (lock != null) {
            lock.block();
        }
        IBinder token = BundleCompat.getBinder(extras,"_VA_|_binder_");
        int vuid = extras.getInt("_VA_|_vuid_");
        VClientImpl client = VClientImpl.get();
        client.initProcess(token, vuid);
        Bundle res = new Bundle();
        BundleCompat.putBinder(res, "_VA_|_client_", client.asBinder());
        res.putInt("_VA_|_pid_", Process.myPid());
        return res;
    }

Client App 的 IBinder 句柄(VClientImpl.asBinder) 被打包在了 Bundle 中返回给 VAMS。

最终, VAMS 调用原生 AM 的 startActivity 向真正的 AMS 发送替换成 StubActivity 的伪造 Intent。

 mirror.android.app.IActivityManager.startActivity.call(ActivityManagerNative.getDefault.call(),
                (Object[]) args);

恢复原 Intent 重定向到原 Activity

当 AMS 收到伪装的 Intent 后,就会找到 StubActivity,这时流程回到 VA 里的主线程中的消息队列中。

Hook 过程就是用我们自己的 Handler 替换 android.os.Handler.mCallback 因为主线程在这里分发一些操作:

    @Override
        public void inject() throws Throwable {
            otherCallback = getHCallback();
            mirror.android.os.Handler.mCallback.set(getH(), this);
        }

handlerMessage 判断是 LAUNCH_ACTIVITY Action 后直接调用了 handlerLaunchActivity 方法,和原版其实很像。

private boolean handleLaunchActivity(Message msg) {
            Object r = msg.obj;
            Intent stubIntent = ActivityThread.ActivityClientRecord.intent.get(r);
            // 获取原版 Intent 信息
            StubActivityRecord saveInstance = new StubActivityRecord(stubIntent);
            if (saveInstance.intent == null) {
                return true;
            }
            // 原版 Intent
            Intent intent = saveInstance.intent;
            ComponentName caller = saveInstance.caller;
            IBinder token = ActivityThread.ActivityClientRecord.token.get(r);
            ActivityInfo info = saveInstance.info;

            // 如果 token 还没初始化,代表 App 刚刚启动第一个组件
            if (VClientImpl.get().getToken() == null) {
                VActivityManager.get().processRestarted(info.packageName, info.processName, saveInstance.userId);
                getH().sendMessageAtFrontOfQueue(Message.obtain(msg));
                return false;
            }
            // AppBindData 为空,则 App 信息不明
            if (!VClientImpl.get().isBound()) {
                // 初始化并绑定 Application
                VClientImpl.get().bindApplication(info.packageName, info.processName);
                getH().sendMessageAtFrontOfQueue(Message.obtain(msg));
                return false;
            }

            // 获取 TaskId
            int taskId = IActivityManager.getTaskForActivity.call(
                    ActivityManagerNative.getDefault.call(),
                    token,
                    false
            );

            // 1.将 ActivityRecorder 加入 mActivities 2.通知服务端 VAMS Activity 创建完成
            VActivityManager.get().onActivityCreate(ComponentUtils.toComponentName(info), caller, token, info, intent, ComponentUtils.getTaskAffinity(info), taskId, info.launchMode, info.flags);
            ClassLoader appClassLoader = VClientImpl.get().getClassLoader(info.applicationInfo);
            intent.setExtrasClassLoader(appClassLoader);
            // 将 Host Stub Activity Intent 替换为原版 Intent
            ActivityThread.ActivityClientRecord.intent.set(r, intent);
            // 同上
            ActivityThread.ActivityClientRecord.activityInfo.set(r, info);
            return true;
        }

需要注意的是,如果这个 Activity 是这个 Apk 启动的第一个组件,则需要 bindApplication 初始化 Application 操作:

 private void bindApplicationNoCheck(String packageName, String processName, ConditionVariable lock) {
        mTempLock = lock;
        try {
            // 设置未捕获异常的 Callback
            setupUncaughtHandler();
        } catch (Throwable e) {
            e.printStackTrace();
        }
        try {
            // 修复 Provider 信息
            fixInstalledProviders();
        } catch (Throwable e) {
            e.printStackTrace();
        }
        mirror.android.os.Build.SERIAL.set(deviceInfo.serial);
        mirror.android.os.Build.DEVICE.set(Build.DEVICE.replace(" ", "_"));
        ActivityThread.mInitialApplication.set(
                VirtualCore.mainThread(),
                null
        );
        // 从 VPMS 获取 apk 信息
        AppBindData data = new AppBindData();
        InstalledAppInfo info = VirtualCore.get().getInstalledAppInfo(packageName, 0);
        if (info == null) {
            new Exception("App not exist!").printStackTrace();
            Process.killProcess(0);
            System.exit(0);
        }
        // dex 优化的开关,dalvik 和 art 处理不同
        if (!info.dependSystem && info.skipDexOpt) {
            VLog.d(TAG, "Dex opt skipped.");
            if (VirtualRuntime.isArt()) {
                ARTUtils.init(VirtualCore.get().getContext());
                ARTUtils.setIsDex2oatEnabled(false);
            } else {
                DalvikUtils.init();
                DalvikUtils.setDexOptMode(DalvikUtils.OPTIMIZE_MODE_NONE);
            }
        }
        data.appInfo = VPackageManager.get().getApplicationInfo(packageName, 0, getUserId(vuid));
        data.processName = processName;
        data.providers = VPackageManager.get().queryContentProviders(processName, getVUid(), PackageManager.GET_META_DATA);
        Log.i(TAG, "Binding application " + data.appInfo.packageName + " (" + data.processName + ")");
        mBoundApplication = data;
        // 主要设置进程的名字
        VirtualRuntime.setupRuntime(data.processName, data.appInfo);
        int targetSdkVersion = data.appInfo.targetSdkVersion;
        if (targetSdkVersion < Build.VERSION_CODES.GINGERBREAD) {
            StrictMode.ThreadPolicy newPolicy = new StrictMode.ThreadPolicy.Builder(StrictMode.getThreadPolicy()).permitNetwork().build();
            StrictMode.setThreadPolicy(newPolicy);
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            if (mirror.android.os.StrictMode.sVmPolicyMask != null) {
                mirror.android.os.StrictMode.sVmPolicyMask.set(0);
            }
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && targetSdkVersion < Build.VERSION_CODES.LOLLIPOP) {
            mirror.android.os.Message.updateCheckRecycle.call(targetSdkVersion);
        }
        if (VASettings.ENABLE_IO_REDIRECT) {
            // IO 重定向
            startIOUniformer();
        }
        // hook native 函数
        NativeEngine.hookNative();
        Object mainThread = VirtualCore.mainThread();
        // 准备 dex 列表
        NativeEngine.startDexOverride();
        // 获得子 pkg 的 Context 前提是必须在系统中安装的(疑问?)
        Context context = createPackageContext(data.appInfo.packageName);
        // 设置虚拟机系统环境 临时文件夹 codeCacheDir
        System.setProperty("java.io.tmpdir", context.getCacheDir().getAbsolutePath());
        // oat 的 cache 目录
        File codeCacheDir;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            codeCacheDir = context.getCodeCacheDir();
        } else {
            codeCacheDir = context.getCacheDir();
        }
        // 硬件加速的 cache 目录
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
            if (HardwareRenderer.setupDiskCache != null) {
                HardwareRenderer.setupDiskCache.call(codeCacheDir);
            }
        } else {
            if (ThreadedRenderer.setupDiskCache != null) {
                ThreadedRenderer.setupDiskCache.call(codeCacheDir);
            }
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            if (RenderScriptCacheDir.setupDiskCache != null) {
                RenderScriptCacheDir.setupDiskCache.call(codeCacheDir);
            }
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            if (RenderScript.setupDiskCache != null) {
                RenderScript.setupDiskCache.call(codeCacheDir);
            }
        }

        // 修复子 App 中 ActivityThread.AppBinderData 的参数,因为之前用的是在 Host 程序中注册的 Stub 的信息
        Object boundApp = fixBoundApp(mBoundApplication);
        mBoundApplication.info = ContextImpl.mPackageInfo.get(context);
        mirror.android.app.ActivityThread.AppBindData.info.set(boundApp, data.info);

        // 同样修复 targetSdkVersion 原来也是可 Host 程序一样的
        VMRuntime.setTargetSdkVersion.call(VMRuntime.getRuntime.call(), data.appInfo.targetSdkVersion);

        boolean conflict = SpecialComponentList.isConflictingInstrumentation(packageName);
        if (!conflict) {
            InvocationStubManager.getInstance().checkEnv(AppInstrumentation.class);
        }

        // 开始构建子程序包的 Application 对象,并且替换原来通过 Host Stub 生成的 mInitialApplication
        mInitialApplication = LoadedApk.makeApplication.call(data.info, false, null);
        mirror.android.app.ActivityThread.mInitialApplication.set(mainThread, mInitialApplication);
        ContextFixer.fixContext(mInitialApplication);
        if (data.providers != null) {
            // 注册 Providers
            installContentProviders(mInitialApplication, data.providers);
        }
        // 初始化锁开,异步调用的初始化函数可以返回了
        if (lock != null) {
            lock.open();
            mTempLock = null;
        }
        try {
            // 调用 Application.onCreate
            mInstrumentation.callApplicationOnCreate(mInitialApplication);
            InvocationStubManager.getInstance().checkEnv(HCallbackStub.class);
            if (conflict) {
                InvocationStubManager.getInstance().checkEnv(AppInstrumentation.class);
            }
            Application createdApp = ActivityThread.mInitialApplication.get(mainThread);
            if (createdApp != null) {
                mInitialApplication = createdApp;
            }
        } catch (Exception e) {
            if (!mInstrumentation.onException(mInitialApplication, e)) {
                throw new RuntimeException(
                        "Unable to create application " + mInitialApplication.getClass().getName()
                                + ": " + e.toString(), e);
            }
        }
        VActivityManager.get().appDoneExecuting();
    }

    private void setupUncaughtHandler() {
        ThreadGroup root = Thread.currentThread().getThreadGroup();
        while (root.getParent() != null) {
            root = root.getParent();
        }
        ThreadGroup newRoot = new RootThreadGroup(root);
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
            final List<ThreadGroup> groups = mirror.java.lang.ThreadGroup.groups.get(root);
            //noinspection SynchronizationOnLocalVariableOrMethodParameter
            synchronized (groups) {
                List<ThreadGroup> newGroups = new ArrayList<>(groups);
                newGroups.remove(newRoot);
                mirror.java.lang.ThreadGroup.groups.set(newRoot, newGroups);
                groups.clear();
                groups.add(newRoot);
                mirror.java.lang.ThreadGroup.groups.set(root, groups);
                for (ThreadGroup group : newGroups) {
                    mirror.java.lang.ThreadGroup.parent.set(group, newRoot);
                }
            }
        } else {
            final ThreadGroup[] groups = ThreadGroupN.groups.get(root);
            //noinspection SynchronizationOnLocalVariableOrMethodParameter
            synchronized (groups) {
                ThreadGroup[] newGroups = groups.clone();
                ThreadGroupN.groups.set(newRoot, newGroups);
                ThreadGroupN.groups.set(root, new ThreadGroup[]{newRoot});
                for (Object group : newGroups) {
                    ThreadGroupN.parent.set(group, newRoot);
                }
                ThreadGroupN.ngroups.set(root, 1);
            }
        }
    }

bindApplication 主要做了以下几个事情:
1. 从 VPMS 获取 APK 的信息,根据设置控制 Dex 优化的开关。
2. 调用 mirror.android.os.Process.setArgV0.call(processName); 设置进程的名称,如果不设置则还是 p0 p1 这样。
3. 做 nativeHook 主要 Hook 一些 native 的函数,主要是一些 IO 函数,包括文件访问重定向等等。
4. 准备一些 cache 临时文件夹。
5. 设置 AppBinderData,AppBinderData 内部包含了 ApplicationInfo 和 provider 信息等重要的 apk 信息。可以理解为 framework 所需要的关键数据结构。
6. 安装 ContentProvider
7. 初始化用户的 Application 对象,并通过 Instrumentation 调用其 onCreate,代表着 Client App 的生命周期正式开始。

最后成功从 StubActivity Intent 还原出来的原版 Intent 被继续交给原生的 AM:

// 将 Host Stub Activity Intent 替换为原版 Intent
ActivityThread.ActivityClientRecord.intent.set(r, intent);
// 同上
ActivityThread.ActivityClientRecord.activityInfo.set(r, info);

最后,最后一个 Hook 点在 Instrumentation.callActivityOnCreate:

因为 AMS 实际上启动的是 StubActivity 的关系,真正的 Activity 的一些信息还不是其真正的信息,比如主题之类的,所以需要在这个时机修复一下,选择这个时间修复的原因也是因为 Activity 已经被 new 出来了,而且资源已经准备完毕。

public void callActivityOnCreate(Activity activity, Bundle icicle) {
        VirtualCore.get().getComponentDelegate().beforeActivityCreate(activity);
        IBinder token = mirror.android.app.Activity.mToken.get(activity);
        ActivityClientRecord r = VActivityManager.get().getActivityRecord(token);
        // 替换 Activity 对象
        if (r != null) {
            r.activity = activity;
        }
        ContextFixer.fixContext(activity);
        ActivityFixer.fixActivity(activity);
        ActivityInfo info = null;
        if (r != null) {
            info = r.info;
        }
        // 设置主题和屏幕纵横控制
        if (info != null) {
            if (info.theme != 0) {
                activity.setTheme(info.theme);
            }
            if (activity.getRequestedOrientation() == ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
                    && info.screenOrientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
                activity.setRequestedOrientation(info.screenOrientation);
            }
        }
        super.callActivityOnCreate(activity, icicle);
        VirtualCore.get().getComponentDelegate().afterActivityCreate(activity);
    }

下一章介绍 Service 的代理。

作者:ganyao939543405 发表于2017/7/27 13:17:10 原文链接
阅读:0 评论:0 查看评论

android动态拼接生成界面 ,大量界面

$
0
0

不知道各位小伙伴在开发应用客户端的时候有没有碰到过这样的需求,就是需要在app端维护大量的表数据。

一般的应用可能需要客户端上传数据的时候 就几张表或者几个功能,但是大量的表格就很复杂。

举列:我们碰到的需求是 维护表格数据 ,这些表格都是政府部门的一些表格文件,而且非常多,一个轨道系统有将近400张表格。对  是400多张,而且没有任何规律,字段什么的都不一样,在之前有个房建系统做了大概几十张,之前的小伙伴是写了几十个activity... 但是这次400张怎么处理?

如图下面的表格:






类似上面的表格,400多个,都要可以实现在app端填写,然后上传到服务器。


app端最后实现效果(手机比较小,截图搞了很多张,gif做出来都太大,传不上来,大概意思能看)







界面不美观,凑合看吧...说重点..


实现的整体思路:


1.处理 400张表的数据 


将400张表的字段存到一张表里面,整理这400张表的数据是个比较大的工作,我们有个做delphi的工程师专门做了一个配置工具,打开每张表配置这张表格的字段。

数据库主要包括4张表格:T_Table  存储所有表格数据  T_TableDetail 存储所有表的字段数据 T_Type 字典表  存储所有控件类型  T_DIC存储所有表下拉数据

T_Table  字段包括:id  name  order parentID(因为我们这个表显示有层级结构)  

T_TableDetail字段主要包括 :字段id   字段名称  字段顺序   表格id(属于哪张表)    类型id(这个字段要用什么控件显示)  isRequired 是否必填 defaultValue默认值 

row   col  行列   dicid关联字典表数据

核心的就这些   这个根据自己的具体业务再搭建自己的数据库表结构。

2.搭建界面

有了基础数据后,我们要做的就是写一个通用Activity去动态拼装整个界面 

我实现的思路是这样的 :将每种类型的控件都单独写成xml,比如有13种类型的控件就写15个xml,根据字段类型判断加载哪种类型的xml。

判断加载完界面  处理界面控件设置默认值,设置点击事件,设置是否可以编辑点击等(因为查看  修改  添加都用一套界面),这一些基本逻辑。


我就直接将初始化布局的地方贴出来,大家看一下。

public class ViewTypeUtil {
	
	//表格名称类型 输入框  默认带值 且要单独获取值
    public static final int item_view_type_zero = 0;
    //单行输入框
    public static final int item_view_type_one = 1;
    //多行输入框
    public static final int item_view_type_two = 2;
    //短日期 如2012-01-12
    public static final int item_view_type_three = 3;
    //数值
    public static final int item_view_type_four = 4;
    //下拉框
    public static final int item_view_type_five = 5;
    //分部分项
    public static final int item_view_type_six = 6;
    //部位栏
    public static final int item_view_type_seven = 7;
    //单选框
    public static final int item_view_type_eight = 8;
    //多选框
    public static final int item_view_type_nine= 9;
    //列表
    public static final int item_view_type_ten = 10;
    //图纸标准
    public static final int item_view_type_eleven = 11;
    //图片
    public static final int item_view_type_twelve = 12;
    //组
    public static final int item_view_type_thirteen = 13;
    //长日期 2017-07-02 10:38:41
    public static final int item_view_type_fourteen  = 14;
    //时间格式 如 10:25:14
    public static final int item_view_type_fifteen  = 15;
    //标签
    public static final int item_view_type_sixteen  = 16;
    

}


	public void initView(final LinearLayout linearLayout, List<FormDetail> formDetailList) {
		for (int i = 0; i < formDetailList.size(); i++) {
			final FormDetail formDetail = formDetailList.get(i);
			final int mType = formDetail.getTypeId();
			if (mType == ViewTypeUtil.item_view_type_zero) {
				mTypeZeroView = LayoutInflater.from(this).inflate(R.layout.item_type_zero, null);
				mItemZeroFieldTv = (TextView) mTypeZeroView.findViewById(R.id.item_zero_field_tv);
				mItemZeroValueEt = (EditText) mTypeZeroView.findViewById(R.id.item_zero_value_et);
				if (mIntentFrom == FLAG_SHOW) {
					mItemZeroValueEt.setEnabled(false);
				}
				mItemZeroValueEt.setText(formDetail.getuValue());
				mItemZeroValueEt.setTag(formDetail.getTableDetailID());
				mItemZeroFieldTv.setText(formDetail.getCaption());
				mTypeZeroView.setTag(mType);
				linearLayout.addView(mTypeZeroView);

			} else if (mType == ViewTypeUtil.item_view_type_one) {
				mTypeOneView = LayoutInflater.from(this).inflate(R.layout.item_type_one, null);
				mItemOneFieldTv = (TextView) mTypeOneView.findViewById(R.id.item_one_field_tv);
				mItemOneValueEt = (EditText) mTypeOneView.findViewById(R.id.item_one_value_et);
				if (mIntentFrom == FLAG_SHOW) {
					mItemOneValueEt.setEnabled(false);
				}
				mItemOneValueEt.setText(formDetail.getuValue());
				mItemOneValueEt.setTag(formDetail.getTableDetailID());
				mItemOneFieldTv.setText(formDetail.getCaption());
				mTypeOneView.setTag(mType);
				linearLayout.addView(mTypeOneView);
			} else if (mType == ViewTypeUtil.item_view_type_two) {
				mTypeTwoView = LayoutInflater.from(this).inflate(R.layout.item_type_two, null);
				mItemTwoValueEt = (EditText) mTypeTwoView.findViewById(R.id.content_et);
				mItemTwoValueEt.setTypeface(Constants.fontFace);
				mItemTwoFieldTv = (TextView) mTypeTwoView.findViewById(R.id.item_two_field_tv);
				if (mIntentFrom == FLAG_SHOW) {
					mItemTwoValueEt.setEnabled(false);
				} else {
					mTypeTwoView.findViewById(R.id.content_iv).setOnClickListener(this);
					mTypeTwoView.findViewById(R.id.voice_iv).setOnClickListener(this);
					mTypeTwoView.findViewById(R.id.special_iv).setOnClickListener(this);
				}
				mItemTwoValueEt.setText(formDetail.getuValue());
				mItemTwoFieldTv.setText(formDetail.getCaption());
				mItemTwoValueEt.setTag(formDetail.getTableDetailID());
				mTypeTwoView.findViewById(R.id.voice_iv).setTag(formDetail.getTableDetailID());
				mTypeTwoView.findViewById(R.id.special_iv).setTag(formDetail.getTableDetailID());
				mTypeTwoView.findViewById(R.id.content_iv).setTag(formDetail.getTableDetailID());

				mTypeTwoView.setTag(mType);
				linearLayout.addView(mTypeTwoView);
			} else if (mType == ViewTypeUtil.item_view_type_three) {
				mTypeThreeView = LayoutInflater.from(this).inflate(R.layout.item_type_three, null);
				mDateKeyTv = (TextView) mTypeThreeView.findViewById(R.id.tv_test_date_key);
				mDateValueEt = (EditText) mTypeThreeView.findViewById(R.id.et_test_date_value);
				mDateKeyTv.setText(formDetail.getCaption());
				if (mIntentFrom == FLAG_SHOW) {
					mDateValueEt.setEnabled(false);
				} else {
					mDateValueEt.setOnClickListener(this);
				}
				if (!TextUtils.isEmpty(formDetail.getuValue())) {
					mDateValueEt.setText(formDetail.getuValue());
				} else {
					mDateValueEt.setText(TimeUtil.getCurDateStr().substring(0, 12));
				}
				mDateValueEt.setTag(formDetail.getTableDetailID());

				mTypeThreeView.setTag(mType);
				linearLayout.addView(mTypeThreeView);
			} else if (mType == ViewTypeUtil.item_view_type_four) {
				mTypeFourView = LayoutInflater.from(context).inflate(R.layout.item_type_four, null);
				mNumEt = (EditText) mTypeFourView.findViewById(R.id.et_num);
				mItemFourFieldTv = (TextView) mTypeFourView.findViewById(R.id.item_four_field_tv);
				mNumEt.setText(formDetail.getuValue());
				mNumEt.setTag(formDetail.getTableDetailID());
				mItemFourFieldTv.setText(formDetail.getCaption());
				if (mIntentFrom == FLAG_SHOW) {
					mNumEt.setEnabled(false);
				}
				mTypeFourView.setTag(mType);
				linearLayout.addView(mTypeFourView);
			} else if (mType == ViewTypeUtil.item_view_type_five) {
				mTypeFiveView = LayoutInflater.from(this).inflate(R.layout.item_type_five, null);
				mSelectKeyTv = (TextView) mTypeFiveView.findViewById(R.id.et_select_key);
				mSelectValueEt = (EditText) mTypeFiveView.findViewById(R.id.et_select_value);
				mSelectValueEt.setText(formDetail.getuValue());
				if (mIntentFrom == FLAG_SHOW) {
					mSelectValueEt.setEnabled(false);
				} else {
					mSelectValueEt.setOnClickListener(this);
				}
				mSelectKeyTv.setText(formDetail.getCaption());
				mSelectValueEt.setTag(R.id.select_view_position, i);
				mSelectValueEt.setTag(R.id.select_view_detial_id, formDetail.getTableDetailID());
				mTypeFiveView.setTag(mType);
				linearLayout.addView(mTypeFiveView);
			} else if (mType == ViewTypeUtil.item_view_type_six) {

			} else if (mType == ViewTypeUtil.item_view_type_seven) {// 部位栏
				mTypeSevenView = LayoutInflater.from(this).inflate(R.layout.item_type_seven, null);
				mPartSelectKeyTv = (TextView) mTypeSevenView.findViewById(R.id.tv_select_part_key);
				mPartSelectValueEt = (EditText) mTypeSevenView.findViewById(R.id.et_select_part_value);
				mPartSelectKeyTv.setText(formDetail.getCaption());
				mPartSelectValueEt.setText(formDetail.getuValue());
				if (mIntentFrom == FLAG_SHOW) {
					mPartSelectValueEt.setEnabled(false);
				} else {
					mPartSelectValueEt.setOnClickListener(this);
				}
				mPartSelectValueEt.setTag(formDetail.getTableDetailID());
				mTypeSevenView.setTag(mType);
				linearLayout.addView(mTypeSevenView);

			} else if (mType == ViewTypeUtil.item_view_type_eight) {// 单选框
				mTypeEightView = LayoutInflater.from(this).inflate(R.layout.item_type_eight, null);
				mTypeEightKeyTv = (TextView) mTypeEightView.findViewById(R.id.tv_type_eight_key);
				rgTypeEight = (RadioGroup) mTypeEightView.findViewById(R.id.rb_type_eight);
				mTypeEightKeyTv.setText(formDetail.getCaption());

				List<DicBean> dicBeans = formDetail.getDicBeanList();
				if (dicBeans == null) {
					return;
				}
				for (int j = 0; j < dicBeans.size(); j++) {
					RadioButton radioButton = new RadioButton(context);
					LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
					radioButton.setText(dicBeans.get(j).getDicName());
					radioButton.setTag(formDetail.getTableDetailID() + "," + dicBeans.get(j).getDicID());
					radioButton.setId(j);
					rgTypeEight.addView(radioButton, params);
					if (mIntentFrom == FLAG_SHOW) {// 查看
						radioButton.setEnabled(false);
						if ((dicBeans.get(j).getDicID() + "").equals(formDetail.getuValue())) {
							rgTypeEight.check(j);
						}
					} else if (mIntentFrom == FLAG_UPDATE) {// 修改
						radioButton.setEnabled(true);
						if ((dicBeans.get(j).getDicID() + "").equals(formDetail.getuValue())) {
							rgTypeEight.check(j);
						}
					} else {// 添加
						rgTypeEight.check(0);
						radioButton.setEnabled(true);
					}
				}
				mTypeEightView.setTag(mType);
				linearLayout.addView(mTypeEightView);
			} else if (mType == ViewTypeUtil.item_view_type_nine) {// 多选框
				mTypeNineView = LayoutInflater.from(this).inflate(R.layout.item_type_nine, null);
				mTypeNineKeyTv = (TextView) mTypeNineView.findViewById(R.id.tv_type_nine_key);// 没有用到
				cbxTypeNine = (CheckBox) mTypeNineView.findViewById(R.id.cbx_type_nine);
				cbxTypeNine.setText(formDetail.getCaption());
				cbxTypeNine.setTag(formDetail.getTableDetailID() + "," + formDetail.getDicID());
				cbxTypeNine.setOnClickListener(this);
				mTypeNineView.setTag(mType);
				linearLayout.addView(mTypeNineView);
			} else if (mType == ViewTypeUtil.item_view_type_ten) {// 列表
				final String detailId = formDetail.getTableDetailID();
				mTypeTenView = LayoutInflater.from(context).inflate(R.layout.item_type_ten, null);
				mTypeTenKeyTv = (TextView) mTypeTenView.findViewById(R.id.tv_type_ten_key);
				mTypeTenKeyTv.setText(formDetail.getCaption());
				mTypeTenLinearLayout = (LinearLayout) mTypeTenView.findViewById(R.id.ly_type_ten);
				mTypeTenLinearLayout.setTag(formDetail.getTableDetailID());
				mTypeTenView.setTag(mType);
				linearLayout.addView(mTypeTenView);
				new AsyncTask<Void, Void, Void>() {
					@Override
					protected Void doInBackground(Void... params) {
						if (mIntentFrom == FLAG_SHOW || mIntentFrom == FLAG_UPDATE) {// 查看,修改
							mFormDetailMap = GdjtUserDataDBOperation.getInstance().getChildListDetailValueData(mUTableID, formDetail.getTableDetailID());
						} else {// 添加
							mFormDetailChildList = GdjtFormDBOperation.getInstance().getChildListDetailData(formDetail.getTableDetailID());
						}
						return null;
					}

					@Override
					protected void onPostExecute(Void result) {
						if (mIntentFrom == FLAG_SHOW || mIntentFrom == FLAG_UPDATE) {
							mTypeTenView.findViewById(R.id.iv_type_ten_add).setVisibility(View.GONE);
							mTypeTenLinearLayout.setWeightSum(3.0f);
							ShowChildView();
						} else {
							AddListChildView();
							mTypeTenLinearLayout.setWeightSum(2.5f);
							mTypeTenView.findViewById(R.id.iv_type_ten_add).setVisibility(View.VISIBLE);
							mTypeTenView.findViewById(R.id.iv_type_ten_add).setOnClickListener(CommonFormViewActivity.this);
						}
					}
				}.execute();

			} else if (mType == ViewTypeUtil.item_view_type_eleven) {// 图纸标准
				mTypeElevenView = LayoutInflater.from(this).inflate(R.layout.item_type_eleven, null);
				mTypeElevenKeyTv = (TextView) mTypeElevenView.findViewById(R.id.tv_type_eleven_key);
				mDrawingNameTv = (TextView) mTypeElevenView.findViewById(R.id.item_eleven_value_et);
				mDrawingNameTv.setOnClickListener(this);
				mDrawingNameTv.setTag(formDetail.getTableDetailID());
				if (!TextUtils.isEmpty(mDrawingPath)) {
					mDrawingNameTv.setText(mDrawingPath.subSequence(mDrawingPath.lastIndexOf("/") + 1, mDrawingPath.length()));
				}
				mTypeElevenView.findViewById(R.id.item_eleven_value_img_photo).setOnClickListener(this);
				mTypeElevenView.findViewById(R.id.item_eleven_value_img_folder).setOnClickListener(this);
				mTypeElevenKeyTv.setText(formDetail.getCaption());
				mTypeElevenView.setTag(mType);
				linearLayout.addView(mTypeElevenView);
			} else if (mType == ViewTypeUtil.item_view_type_twelve) {// 图片
				mTypeTwelveView = LayoutInflater.from(this).inflate(R.layout.item_type_twelve, null);
				mTypeTwelveKeyTv = (TextView) mTypeTwelveView.findViewById(R.id.tv_type_twelve_key);
				mTypeTwelveValueTv = (TextView) mTypeTwelveView.findViewById(R.id.item_twelve_value_path);
				mTypeTwelveTakePhoto = mTypeTwelveView.findViewById(R.id.item_twelve_value_img_photo);
				mTypeTwelveSelectPhoto = mTypeTwelveView.findViewById(R.id.item_twelve_value_img_folder);
				if (mIntentFrom == FLAG_SHOW) {
					mTypeTwelveTakePhoto.setEnabled(false);
					mTypeTwelveSelectPhoto.setEnabled(false);
				} else {
					mTypeTwelveTakePhoto.setOnClickListener(this);
					mTypeTwelveSelectPhoto.setOnClickListener(this);
				}
				mTypeTwelveValueTv.setOnClickListener(this);
				mPhotoPath = formDetail.getuValue();
				mTypeTwelveKeyTv.setText(formDetail.getCaption());
				mTypeTwelveValueTv.setText(mPhotoPath.substring(mPhotoPath.lastIndexOf("/") + 1));
				mTypeTwelveValueTv.setTag(formDetail.getTableDetailID());
				mTypeTwelveTakePhoto.setTag(formDetail.getTableDetailID());
				mTypeTwelveSelectPhoto.setTag(formDetail.getTableDetailID());
				mTypeTwelveView.setTag(mType);
				linearLayout.addView(mTypeTwelveView);
			} else if (mType == ViewTypeUtil.item_view_type_thirteen) {// 组
				final String detailId = formDetail.getTableDetailID();
				mTypeThirteenView = LayoutInflater.from(context).inflate(R.layout.item_type_thirteen, null);
				mTypeThirteenKeyTv = (TextView) mTypeThirteenView.findViewById(R.id.tv_type_thirteen_key);
				mTypeThirteenKeyTv.setText(formDetail.getCaption());
				mTypeThirteenLinearLayout = (LinearLayout) mTypeThirteenView.findViewById(R.id.ly_type_thirteen);
				mTypeThirteenLinearLayout.setTag(detailId);
				mTypeThirteenView.setTag(mType);
				linearLayout.addView(mTypeThirteenView);
				
				new AsyncTask<Void, Void, Void>() {
					@Override
					protected Void doInBackground(Void... params) {
						if (mIntentFrom == FLAG_SHOW || mIntentFrom == FLAG_UPDATE) {// 查看,修改
							mFormDetailMap = GdjtUserDataDBOperation.getInstance().getChildListDetailValueData(mUTableID, detailId);
						} else {// 添加
							mFormZuDetailChildList = GdjtFormDBOperation.getInstance().getChildListDetailData(detailId);
						}
						return null;
					}

					@Override
					protected void onPostExecute(Void result) {
						
						if (mIntentFrom == FLAG_SHOW || mIntentFrom == FLAG_UPDATE ) {
							ShowChildView();
						} else {
							AddZuChildView();
						}
						// linearLayout.refreshDrawableState();
					}
				}.execute();

			} else if (mType == ViewTypeUtil.item_view_type_fourteen) {// 日期时间格式
				mTypeFourteenView = LayoutInflater.from(this).inflate(R.layout.item_type_three, null);
				mFourteenDateKeyTv = (TextView) mTypeFourteenView.findViewById(R.id.tv_test_date_key);
				mFourteenDateValueEt = (EditText) mTypeFourteenView.findViewById(R.id.et_test_date_value);
				mFourteenDateValueEt.setTag(formDetail.getTableDetailID());
				mFourteenDateKeyTv.setText(formDetail.getCaption());
				if (mIntentFrom == FLAG_SHOW) {
					mFourteenDateValueEt.setEnabled(false);
				}
				if (!TextUtils.isEmpty(formDetail.getuValue())) {
					mFourteenDateValueEt.setText(formDetail.getuValue());
				} else {
					mFourteenDateValueEt.setText(TimeUtil.getCurDateStr());
				}
				mFourteenDateValueEt.setTag(formDetail.getTableDetailID());
				mFourteenDateValueEt.setOnClickListener(this);
				mTypeFourteenView.setTag(mType);
				linearLayout.addView(mTypeFourteenView);
			} else if (mType == ViewTypeUtil.item_view_type_fifteen) {// 时间格式
				mTypeFifteenView = LayoutInflater.from(this).inflate(R.layout.item_type_three, null);
				mFifteenDateKeyTv = (TextView) mTypeFifteenView.findViewById(R.id.tv_test_date_key);
				mFifteenDateValueEt = (EditText) mTypeFifteenView.findViewById(R.id.et_test_date_value);
				mFifteenDateValueEt.setTag(formDetail.getTableDetailID());
				mFifteenDateKeyTv.setText(formDetail.getCaption());
				if (mIntentFrom == FLAG_SHOW) {
					mFifteenDateValueEt.setEnabled(false);
				}
				if (!TextUtils.isEmpty(formDetail.getuValue())) {
					mFifteenDateValueEt.setText(formDetail.getuValue());
				} else {
					mFifteenDateValueEt.setText(TimeUtil.getStringTimeShort());
				}
				mFifteenDateValueEt.setTag(formDetail.getTableDetailID());
				mFifteenDateValueEt.setOnClickListener(this);
				mTypeFifteenView.setTag(mType);
				linearLayout.addView(mTypeFifteenView);
			} else if (mType == ViewTypeUtil.item_view_type_sixteen) {// 标签
				mTypeSixteenView = LayoutInflater.from(this).inflate(R.layout.item_type_zero, null);
				mItemSixteenFieldTv = (TextView) mTypeSixteenView.findViewById(R.id.item_zero_field_tv);
				mItemSixteenValueEt = (EditText) mTypeSixteenView.findViewById(R.id.item_zero_value_et);
				mItemSixteenValueEt.setEnabled(false);
				mItemSixteenValueEt.setText(formDetail.getuValue());
				mItemSixteenValueEt.setTag(formDetail.getTableDetailID());
				mItemSixteenFieldTv.setText(formDetail.getCaption());
				mTypeSixteenView.setTag(mType);
				linearLayout.addView(mTypeSixteenView);
			}
		}
	}

3.处理复杂控件 

其实这个里面的复杂控件就两种,一种是列表 一种是组
给简单大家解释一下列表是什么意思:  大家看有的表格里面是多行多列的,意味着同样的字段数据 可能要添写多条。
那么对应在app里面   就是 这些字段放到了一起  做成了一个 组。  那么这些字段添加多条 就是 在组后面放了一个 + 号
点击+号  这个组 就增加一组 。所以这个带+号的字段 就是 列表字段。

组和 列表 处理起来相对来说非常麻烦。

1).子项数据获取   
2).布局嵌套
3).点击事件单独处理
4)保存数据独立拼接

具体这些问题,我就不一一展开说了。大家倘若有做的,具体做的时候自然会体会到。    

4.处理点击事件

界面拼接完成后 ,要处理控件里面的点击事件。做的时候就会发现  倘若界面出现同一个item布局  ,布局里面包含点击事件 ,正常给控件设置点击事件,会发现 点击控件的时候无法区分开每个点击事件

所以处理点击事件的时候:原理  就是先循环整体的大布局 ,找到里面的每一项小布局,然后单独处理每一项小布局里面的控件。

这样才能保证点击事件没有问题。

代码贴一下:

@Override
	public void onClick(View v) {
		 if (v.getId() == R.id.content_iv) {// 范例

		} else if (v.getId() == R.id.voice_iv) {// 语音
			for (int i = 0; i < mMainView.getChildCount(); i++) {
				int viewTag = (int) mMainView.getChildAt(i).getTag();
				if (viewTag == ViewTypeUtil.item_view_type_two) {
					LinearLayout l = (LinearLayout) ((LinearLayout) mMainView.getChildAt(i)).getChildAt(0);
					EditText editText = (EditText) l.getChildAt(1);
					if (v.getTag().equals(editText.getTag())) {
						mVoice = new VoiceToWord(this, editText);
						mVoice.GetWordFromVoice();
					}
				} else if (viewTag == ViewTypeUtil.item_view_type_thirteen || viewTag == ViewTypeUtil.item_view_type_ten) {
					LinearLayout l = (LinearLayout) ((LinearLayout) ((LinearLayout) mMainView.getChildAt(i)).getChildAt(0)).getChildAt(1);
					for (int j = 0; j < l.getChildCount(); j++) {
						LinearLayout l_add = (LinearLayout) ((LinearLayout) l.getChildAt(j));
						for (int k = 0; k < l_add.getChildCount(); k++) {
							int viewTag_child = (int) l_add.getChildAt(k).getTag();
							if (viewTag_child == ViewTypeUtil.item_view_type_two) {
								LinearLayout l_view = (LinearLayout) ((LinearLayout) l_add.getChildAt(k)).getChildAt(0);
								EditText editText = (EditText) l_view.getChildAt(1);
								if (v.getTag().equals(editText.getTag())) {
									mVoice = new VoiceToWord(this, editText);
									mVoice.GetWordFromVoice();
								}
							}
						}
					}
				}
			}
		} else if (v.getId() == R.id.special_iv) {// 特殊字符
			for (int i = 0; i < mMainView.getChildCount(); i++) {
				int viewTag = (int) mMainView.getChildAt(i).getTag();
				if (viewTag == ViewTypeUtil.item_view_type_two) {
					LinearLayout l = (LinearLayout) ((LinearLayout) mMainView.getChildAt(i)).getChildAt(0);
					EditText editText = (EditText) l.getChildAt(1);
					if (v.getTag().equals(editText.getTag())) {
						mSpecialdialog = new SpecialWordsDialog(context, editText);
					}
				} else if (viewTag == ViewTypeUtil.item_view_type_thirteen || viewTag == ViewTypeUtil.item_view_type_ten) {
					LinearLayout l = (LinearLayout) ((LinearLayout) ((LinearLayout) mMainView.getChildAt(i)).getChildAt(0)).getChildAt(1);
					for (int j = 0; j < l.getChildCount(); j++) {
						LinearLayout l_add = (LinearLayout) ((LinearLayout) l.getChildAt(j));
						for (int k = 0; k < l_add.getChildCount(); k++) {
							int viewTag_child = (int) l_add.getChildAt(k).getTag();
							if (viewTag_child == ViewTypeUtil.item_view_type_two) {
								LinearLayout l_view = (LinearLayout) ((LinearLayout) l_add.getChildAt(k)).getChildAt(0);
								EditText editText = (EditText) l_view.getChildAt(1);
								if (v.getTag().equals(editText.getTag())) {
									mSpecialdialog = new SpecialWordsDialog(context, editText);
								}
							}
						}
					}
				}
			}
		} else if (v.getId() == R.id.et_test_date_value) {// 时间选择

			for (int i = 0; i < mMainView.getChildCount(); i++) {
				int viewTag = (int) mMainView.getChildAt(i).getTag();
				if (viewTag == ViewTypeUtil.item_view_type_three) {
					LinearLayout l = (LinearLayout) ((LinearLayout) mMainView.getChildAt(i)).getChildAt(0);
					EditText editText = (EditText) l.getChildAt(1);
					if (v.getTag().equals(editText.getTag())) {
						DlgUtil.showDateTimePickerDlgByType(this, editText, 1);
					}
				} else if (viewTag == ViewTypeUtil.item_view_type_fourteen) {
					LinearLayout l = (LinearLayout) ((LinearLayout) mMainView.getChildAt(i)).getChildAt(0);
					EditText editText = (EditText) l.getChildAt(1);
					if (v.getTag().equals(editText.getTag())) {
						DlgUtil.showDateTimePickerDlg(this, editText);

					}
				} else if (viewTag == ViewTypeUtil.item_view_type_fifteen) {
					LinearLayout l = (LinearLayout) ((LinearLayout) mMainView.getChildAt(i)).getChildAt(0);
					EditText editText = (EditText) l.getChildAt(1);
					if (v.getTag().equals(editText.getTag())) {
						DlgUtil.showDateTimePickerDlgByType(this, editText, 2);
					}
				} else if (viewTag == ViewTypeUtil.item_view_type_thirteen || viewTag == ViewTypeUtil.item_view_type_ten) {
					LinearLayout l = (LinearLayout) ((LinearLayout) ((LinearLayout) mMainView.getChildAt(i)).getChildAt(0)).getChildAt(1);
					for (int j = 0; j < l.getChildCount(); j++) {
						LinearLayout l_add = (LinearLayout) ((LinearLayout) l.getChildAt(j));
						for (int k = 0; k < l_add.getChildCount(); k++) {
							int viewTag_child = (int) l_add.getChildAt(k).getTag();
							if (viewTag_child == ViewTypeUtil.item_view_type_three) {
								LinearLayout l_view = (LinearLayout) ((LinearLayout) l_add.getChildAt(i)).getChildAt(0);
								EditText editText = (EditText) l_view.getChildAt(1);
								if (v.getTag().equals(editText.getTag())) {
									DlgUtil.showDateTimePickerDlgByType(this, editText, 1);
								}
							} else if (viewTag_child == ViewTypeUtil.item_view_type_fourteen) {
								LinearLayout l_view = (LinearLayout) ((LinearLayout) l_add.getChildAt(i)).getChildAt(0);
								EditText editText = (EditText) l_view.getChildAt(1);
								if (v.getTag().equals(editText.getTag())) {
									DlgUtil.showDateTimePickerDlg(this, editText);

								}
							} else if (viewTag_child == ViewTypeUtil.item_view_type_fifteen) {
								LinearLayout l_view = (LinearLayout) ((LinearLayout) l_add.getChildAt(i)).getChildAt(0);
								EditText editText = (EditText) l_view.getChildAt(1);
								if (v.getTag().equals(editText.getTag())) {
									DlgUtil.showDateTimePickerDlgByType(this, editText, 2);
								}
							}
						}
					}
				}
			}

		} else if (v.getId() == R.id.et_select_value) {
			for (int i = 0; i < mMainView.getChildCount(); i++) {
				int viewTag = (int) mMainView.getChildAt(i).getTag();
				if (viewTag == ViewTypeUtil.item_view_type_five) {
					LinearLayout l = (LinearLayout) ((LinearLayout) mMainView.getChildAt(i)).getChildAt(0);
					final EditText editText = (EditText) l.getChildAt(1);
					if (v.getTag(R.id.select_view_detial_id).equals(editText.getTag(R.id.select_view_detial_id))) {
						int position = (int) editText.getTag(R.id.select_view_position);
						mDataList.clear();
						List<DicBean> dicList = mFormDetailList.get(position).dicBeanList;
						for (DicBean dicBean : dicList) {
							mDataList.add(dicBean.getDicName());
						}
						CusSpinerAdapter<String> mAdapter = new CusSpinerAdapter<String>(this);
						mAdapter.refreshData(mDataList, 0);
						View contentView = LayoutInflater.from(context).inflate(R.layout.common_form_view, null);
						SpinerPopWindow mSpinerPopWindow = new SpinerPopWindow(this, R.layout.spiner_window_layout, true);
						mSpinerPopWindow.setAdatper(mAdapter);
						mSpinerPopWindow.setItemListener(new IOnItemSelectListener() {

							@Override
							public void onItemClick(int pos) {
								if (pos >= 0 && pos <= mDataList.size()) {
									String value = (String) mDataList.get(pos);
									editText.setText(value.toString());
								}
							}
						});
						mSpinerPopWindow.setWidth(editText.getWidth());

						int windowPos[] = PopMeasure.calculatePopWindowPos(editText, contentView);
						int xOff = 25;// 可以自己调整偏移
						windowPos[0] -= xOff;
						mSpinerPopWindow.showAtLocation(v, Gravity.TOP | Gravity.START, windowPos[0], windowPos[1]);
					}
				}else if(viewTag == ViewTypeUtil.item_view_type_thirteen || viewTag == ViewTypeUtil.item_view_type_ten){
					LinearLayout l = (LinearLayout) ((LinearLayout) ((LinearLayout) mMainView.getChildAt(i)).getChildAt(0)).getChildAt(1);
					for (int j = 0; j < l.getChildCount(); j++) {
						LinearLayout l_add = (LinearLayout) ((LinearLayout) l.getChildAt(j));//第二层布局layout
						for (int k = 0; k < l_add.getChildCount(); k++) {
							int viewTag_child = (int) l_add.getChildAt(k).getTag();
							if(viewTag_child == ViewTypeUtil.item_view_type_five){
								LinearLayout l_view = (LinearLayout) ((LinearLayout) l_add.getChildAt(k)).getChildAt(0);//第三层每一项
								final EditText editText = (EditText) l_view.getChildAt(1);
								if (v.getTag(R.id.select_view_detial_id).equals(editText.getTag(R.id.select_view_detial_id))) {
									int position = (int) editText.getTag(R.id.select_view_position);
									mDataList.clear();
									List<DicBean> dicList = new ArrayList<FormDetailBean.FormDetail.DicBean>();
									if (viewTag == ViewTypeUtil.item_view_type_ten) {
										dicList = mFormDetailChildList.get(position).dicBeanList;
									}else if (viewTag == ViewTypeUtil.item_view_type_thirteen) {
										dicList = mFormZuDetailChildList.get(position).dicBeanList;
									}
									
									for (DicBean dicBean : dicList) {
										mDataList.add(dicBean.getDicName());
									}
									CusSpinerAdapter<String> mAdapter = new CusSpinerAdapter<String>(this);
									mAdapter.refreshData(mDataList, 0);
									View contentView = LayoutInflater.from(context).inflate(R.layout.common_form_view, null);
									SpinerPopWindow mSpinerPopWindow = new SpinerPopWindow(this, R.layout.spiner_window_layout, true);
									mSpinerPopWindow.setAdatper(mAdapter);
									mSpinerPopWindow.setItemListener(new IOnItemSelectListener() {

										@Override
										public void onItemClick(int pos) {
											if (pos >= 0 && pos <= mDataList.size()) {
												String value = (String) mDataList.get(pos);
												editText.setText(value.toString());
											}
										}
									});
									mSpinerPopWindow.setWidth(editText.getWidth());
									
									int windowPos[] = PopMeasure.calculatePopWindowPos(editText, contentView);
									int xOff = 200;// 可以自己调整偏移
									windowPos[0] -= xOff;
									mSpinerPopWindow.showAtLocation(v, Gravity.TOP | Gravity.START,windowPos[0], windowPos[1]);
								}
							}else if(viewTag_child == ViewTypeUtil.item_view_type_thirteen || viewTag_child == ViewTypeUtil.item_view_type_ten){
								LinearLayout l3 =  (LinearLayout) ((LinearLayout) ((LinearLayout) l_add.getChildAt(k)).getChildAt(0)).getChildAt(1);
								for (int j3 = 0; j3 < l3.getChildCount(); j3++) {
									LinearLayout l_add3 = (LinearLayout) ((LinearLayout) l3.getChildAt(j3));
									for (int k3 = 0; k3 < l_add3.getChildCount(); k3++) {
										int viewTag_child3 = (int) l_add3.getChildAt(k3).getTag();
										if(viewTag_child3 == ViewTypeUtil.item_view_type_five){
											LinearLayout l_view = (LinearLayout) ((LinearLayout) l_add3.getChildAt(k3)).getChildAt(0);
											final EditText editText = (EditText) l_view.getChildAt(1);
											if (v.getTag(R.id.select_view_detial_id).equals(editText.getTag(R.id.select_view_detial_id))) {
												int position = (int) editText.getTag(R.id.select_view_position);
												mDataList.clear();
												List<DicBean> dicList = mFormZuDetailChildList.get(position).dicBeanList;
												for (DicBean dicBean : dicList) {
													mDataList.add(dicBean.getDicName());
												}
												CusSpinerAdapter<String> mAdapter = new CusSpinerAdapter<String>(this);
												mAdapter.refreshData(mDataList, 0);
												View contentView = LayoutInflater.from(context).inflate(R.layout.common_form_view, null);
												SpinerPopWindow mSpinerPopWindow = new SpinerPopWindow(this, R.layout.spiner_window_layout, true);
												mSpinerPopWindow.setAdatper(mAdapter);
												mSpinerPopWindow.setItemListener(new IOnItemSelectListener() {

													@Override
													public void onItemClick(int pos) {
														if (pos >= 0 && pos <= mDataList.size()) {
															String value = (String) mDataList.get(pos);
															editText.setText(value.toString());
														}
													}
												});
												mSpinerPopWindow.setWidth(editText.getWidth());
												
												int windowPos[] = PopMeasure.calculatePopWindowPos(editText, contentView);
												int xOff = 200;// 可以自己调整偏移
												windowPos[0] -= xOff;
												mSpinerPopWindow.showAtLocation(v, Gravity.TOP | Gravity.START,windowPos[0], windowPos[1]);
											}
										}
									}
								}
							}
						}
					}
				}
			}
		} else if (v.getId() == R.id.et_select_part_value) {
			CheckPart_Monomer_Activity.actionStartForResult(context, mPartSelectValueEt.getText().toString(), "", "", monomerId, CheckPart_Monomer_Activity.class);
		} else if (v.getId() == R.id.cbx_type_nine) {// 类型9 多选框意向
			for (int i = 0; i < mMainView.getChildCount(); i++) {
				int viewTag = (int) mMainView.getChildAt(i).getTag();
				if (viewTag == ViewTypeUtil.item_view_type_nine) {
					LinearLayout l = (LinearLayout) ((LinearLayout) mMainView.getChildAt(i)).getChildAt(0);
					CheckBox checkBox = (CheckBox) l.getChildAt(1);
				} else if (viewTag == ViewTypeUtil.item_view_type_thirteen || viewTag == ViewTypeUtil.item_view_type_ten) {
					LinearLayout l = (LinearLayout) ((LinearLayout) ((LinearLayout) mMainView.getChildAt(i)).getChildAt(0)).getChildAt(1);
					for (int j = 0; j < l.getChildCount(); j++) {
						LinearLayout l_add = (LinearLayout) ((LinearLayout) l.getChildAt(j));
						for (int k = 0; k < l_add.getChildCount(); k++) {
							int viewTag_child = (int) l_add.getChildAt(k).getTag();
							if (viewTag_child == ViewTypeUtil.item_view_type_nine) {
								LinearLayout l_view = (LinearLayout) ((LinearLayout) l_add.getChildAt(k)).getChildAt(0);
								CheckBox checkBox = (CheckBox) l_view.getChildAt(1);
							}
						}
					}
				}
			}

		} else if (v.getId() == R.id.item_eleven_value_et) {
			for (int i = 0; i < mMainView.getChildCount(); i++) {
				int viewTag = (int) mMainView.getChildAt(i).getTag(); 
				if (viewTag == ViewTypeUtil.item_view_type_eleven) {
					if (!TextUtils.isEmpty(mDrawingPath)) {
						Intent intent = new Intent(CommonFormViewActivity.this, DrawingTypeActivity.class);
						intent.putExtra("drawingPath", mDrawingPath);
						intent.putExtra("tableDetailID", (String) v.getTag());
						intent.putExtra("intent_from", FLAG_UPDATE);
						if (mIntentFrom == FLAG_SHOW || mIntentFrom == FLAG_UPDATE) {// 查看,修改记录
							intent.putExtra("RecordId", mUTableID);
						}
						startActivityForResult(intent, 11);
					}
				}
			}

		} else if (v.getId() == R.id.item_eleven_value_img_photo) {// 类型11图纸标准类型拍照按钮
			mViewTypeFlag = ViewTypeUtil.item_view_type_eleven;
			if (mCreateBmpFactory == null) {
				mCreateBmpFactory = new CreateBmpFactory(CommonFormViewActivity.this);
			}
			mCreateBmpFactory.OpenCamera();
		} else if (v.getId() == R.id.item_eleven_value_img_folder) {// 类型11图纸标准类型从图库选择
			mViewTypeFlag = ViewTypeUtil.item_view_type_eleven;
			FileSelect chosen = new FileSelect(CommonFormViewActivity.this);
			chosen.setOnFileSelecteListener(new OnFileSelecteListener() {
				@Override
				public void onFileSelected(String filePath, String fileName) {
					mDrawingPath = filePath;
					mDrawingNameTv.setText(fileName);
					Intent intent1 = new Intent(CommonFormViewActivity.this, DrawingTypeActivity.class);
					intent1.putExtra("drawingPath", mDrawingPath);
					intent1.putExtra("tableDetailID", (String) mDrawingNameTv.getTag());
					intent1.putExtra("intent_from", FLAG_UPDATE);
					if (mIntentFrom == FLAG_SHOW || mIntentFrom == FLAG_UPDATE) {// 查看,修改记录
						intent1.putExtra("RecordId", mUTableID);
					}
					startActivityForResult(intent1, 11);
				}
			});
			chosen.showChosenDialog();

		} else if (v.getId() == R.id.item_twelve_value_path) {// 类型12 图片路径
			
		} else if (v.getId() == R.id.item_twelve_value_img_photo) {// 类型12拍照
			mViewTypeFlag = ViewTypeUtil.item_view_type_twelve;
			mViewTag = v.getTag();
			if (mCreateBmpFactory == null) {
				mCreateBmpFactory = new CreateBmpFactory(CommonFormViewActivity.this);
			}
			mCreateBmpFactory.OpenCamera();
		} else if (v.getId() == R.id.item_twelve_value_img_folder) {// 类型12
			mViewTypeFlag = ViewTypeUtil.item_view_type_twelve;
			mViewTag = v.getTag();
			if (mCreateBmpFactory == null) {
				mCreateBmpFactory = new CreateBmpFactory(CommonFormViewActivity.this);
			}
			mCreateBmpFactory.OpenGallery();

		} else if (v.getId() == R.id.iv_type_ten_add) {
			AddListChildView();
		}
	}

估计这么写看看都会头晕,因为这上面代码 里面除了处理正常的点击事件,还要处理列表和组  也就是 嵌套布局里面的点击事件。但是原理都是一样的。

就是循环布局找到 你点击的那个布局,那个控件...  如果是嵌套多级  就一级一级的找...

5.保存上传数据

我们这边保存处理的逻辑就是将界面上所有的数据都拼成一个JSONOject  里面套JSONArray和JSONObject ,反正就是根据自己的业务往里面套数据,这个数据组织起来稍微有一点点麻烦。

代码贴一下:

package com.jky.xmxtcommonlib.utils;

import org.json.JSONException;
import org.json.JSONObject;

import com.jky.xmxtcommonlib.R;

import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.TextView;

/**
 * Created by zlw on 2017/6/13.
 */
public class GetViewValueUtil {

	// 表格输入框取值方法
	public static JSONObject getTypeZeroValue(LinearLayout linearLayout, JSONObject jsonObject) throws JSONException {
		EditText editText = (EditText) linearLayout.getChildAt(1);
		String id = editText.getTag() + "";
		String str = editText.getText().toString().trim();
		jsonObject.put(id + "", str);
		return jsonObject;
	}

	
	// 单行输入框取值方法
	public static JSONObject getTypeOneValue(LinearLayout linearLayout, JSONObject jsonObject) throws JSONException {
		EditText editText = (EditText) linearLayout.getChildAt(1);
		String id = editText.getTag() + "";
		String str = editText.getText().toString().trim();
		jsonObject.put(id + "", str);
		return jsonObject;
	}

	// 多行输入框取值方法
	public static JSONObject getTypeTwoValue(LinearLayout linearLayout, JSONObject jsonObject) throws JSONException {
		EditText editText = (EditText) linearLayout.getChildAt(1);
		String id = editText.getTag() + "";
		String str = editText.getText().toString().trim();
		jsonObject.put(id + "", str);
		return jsonObject;
	}

	// 时间选择框取值方法
	public static JSONObject getTypeThreeValue(LinearLayout linearLayout, JSONObject jsonObject) throws JSONException {
		if(linearLayout !=null){
			EditText editText = (EditText) linearLayout.getChildAt(1);
			if(editText!=null){
				String id = editText.getTag() + "";
				String str = editText.getText().toString().trim();
				jsonObject.put(id + "", str);
			}else{
				JSONObject json= new JSONObject();
				return json;
			}
		} else {
			JSONObject json= new JSONObject();
			return json;
		}
		
		return jsonObject;
	}

	// 数值输入框取值方法
	public static JSONObject getTypeFourValue(LinearLayout linearLayout, JSONObject jsonObject) throws JSONException {
		EditText editText = (EditText) linearLayout.getChildAt(1);
		String id = editText.getTag() + "";
		String str = editText.getText().toString().trim();
		jsonObject.put(id + "", str);
		return jsonObject;
	}

	// 下拉选择框取值方法
	public static JSONObject getTypeFiveValue(LinearLayout linearLayout, JSONObject jsonObject) throws JSONException {
		EditText editText = (EditText) linearLayout.getChildAt(1);
		String id = editText.getTag(R.id.select_view_detial_id) + "";
		String str = editText.getText().toString().trim();
		jsonObject.put(id + "", str);
		return jsonObject;
	}

	// 部位选择框取值方法
	public static JSONObject getTypeSevenValue(LinearLayout linearLayout, JSONObject jsonObject) throws JSONException {
		EditText editText = (EditText) linearLayout.getChildAt(1);
		String id = editText.getTag() + "";
		String str = editText.getText().toString().trim();
		jsonObject.put(id + "", str);
		return jsonObject;
	}

	// 单选框取值方法
	public static JSONObject getTypeEightValue(LinearLayout linearLayout, JSONObject jsonObject) throws JSONException {
		RadioGroup radioGroup = (RadioGroup) linearLayout.getChildAt(1);
		for (int i = 0; i < radioGroup.getChildCount(); i++) {
			RadioButton radioButton = (RadioButton) radioGroup.getChildAt(i);
			String  str = (String) radioButton.getTag();
			String detailId = str.split(",")[0];
			String dicId = str.split(",")[1];
			if (radioButton.isChecked()) {
					jsonObject.put(detailId + "", dicId);
			}
		}
		return jsonObject;
	}

	// 多选框取值方法
	public static JSONObject getTypeNineValue(LinearLayout linearLayout, JSONObject jsonObject) throws JSONException {
		CheckBox checkBox = (CheckBox) linearLayout.getChildAt(1);
		String value = "";
		String  str = (String) checkBox.getTag();
		if (checkBox.isChecked()) {
			value = checkBox.getText().toString().trim();
			String detailId = str.split(",")[0];
			String dicId = str.split(",")[1];
			jsonObject.put(detailId,dicId);
		} else {
			String detailId = str.split(",")[0];
			jsonObject.put(detailId, "0");
		}
		
		return jsonObject;
	}
	
	// 图纸类表格类型存值
		public static JSONObject getTypeElevenValue(LinearLayout linearLayout,String path, JSONObject jsonObject) throws JSONException {
			TextView extView = (TextView) linearLayout.getChildAt(1);
			String id = extView.getTag() + "";
			String str = path.trim();
			jsonObject.put(id + "", str);
			return jsonObject;
		}
	// 照片类表格类型存值
	public static JSONObject getTypeTwelveValue(LinearLayout linearLayout,String path, JSONObject jsonObject) throws JSONException {
		TextView extView = (TextView) linearLayout.getChildAt(0);
		String id = extView.getTag() + "";
		String str = path.trim();
		jsonObject.put(id + "", str);
		return jsonObject;
	}
}
	/**
	 * 得到需要保存的json数据
	 * 
	 * @param linearLayout
	 * @param isChildFlag
	 * @param jsonKey
	 * @throws JSONException
	 */
	private void getJsonInfo(LinearLayout linearLayout) throws JSONException {
		for (int i = 0; i < linearLayout.getChildCount(); i++) {
			int viewTag = (int) linearLayout.getChildAt(i).getTag();
			if (viewTag == ViewTypeUtil.item_view_type_zero) {
				LinearLayout l = (LinearLayout) ((LinearLayout) linearLayout.getChildAt(i)).getChildAt(0);
				mJsonObject = GetViewValueUtil.getTypeZeroValue(l, mJsonObject);
			} else if (viewTag == ViewTypeUtil.item_view_type_one) {
				LinearLayout l = (LinearLayout) ((LinearLayout) linearLayout.getChildAt(i)).getChildAt(0);
				mJsonObject = GetViewValueUtil.getTypeOneValue(l, mJsonObject);
			} else if (viewTag == ViewTypeUtil.item_view_type_two) {
				LinearLayout l = (LinearLayout) ((LinearLayout) linearLayout.getChildAt(i)).getChildAt(0);
				mJsonObject = GetViewValueUtil.getTypeTwoValue(l, mJsonObject);
			} else if (viewTag == ViewTypeUtil.item_view_type_three) {
				LinearLayout l = (LinearLayout) ((LinearLayout) linearLayout.getChildAt(i)).getChildAt(0);
				mJsonObject = GetViewValueUtil.getTypeThreeValue(l, mJsonObject);
			} else if (viewTag == ViewTypeUtil.item_view_type_four) {
				LinearLayout l = (LinearLayout) ((LinearLayout) linearLayout.getChildAt(i)).getChildAt(0);
				mJsonObject = GetViewValueUtil.getTypeFourValue(l, mJsonObject);

			} else if (viewTag == ViewTypeUtil.item_view_type_five) {
				LinearLayout l = (LinearLayout) ((LinearLayout) linearLayout.getChildAt(i)).getChildAt(0);
				mJsonObject = GetViewValueUtil.getTypeFiveValue(l, mJsonObject);
			} else if (viewTag == ViewTypeUtil.item_view_type_six) {

			} else if (viewTag == ViewTypeUtil.item_view_type_seven) {
				LinearLayout l = (LinearLayout) ((LinearLayout) linearLayout.getChildAt(i)).getChildAt(0);
				mJsonObject = GetViewValueUtil.getTypeSevenValue(l, mJsonObject);
			} else if (viewTag == ViewTypeUtil.item_view_type_eight) {
				LinearLayout l = (LinearLayout) ((LinearLayout) linearLayout.getChildAt(i)).getChildAt(0);
				mJsonObject = GetViewValueUtil.getTypeEightValue(l, mJsonObject);
			} else if (viewTag == ViewTypeUtil.item_view_type_nine) {
				LinearLayout l = (LinearLayout) ((LinearLayout) linearLayout.getChildAt(i)).getChildAt(0);
				mJsonObject = GetViewValueUtil.getTypeNineValue(l, mJsonObject);
			} else if (viewTag == ViewTypeUtil.item_view_type_ten) {
				LinearLayout l = (LinearLayout) ((LinearLayout) linearLayout.getChildAt(i)).getChildAt(0);
				LinearLayout layout = (LinearLayout) l.getChildAt(1);
				getSecondJsonInfo(layout, (String) layout.getTag());
				mJsonObject.put((String) layout.getTag(), mSecondJsonArray);
			} else if (viewTag == ViewTypeUtil.item_view_type_eleven) {
				LinearLayout l = (LinearLayout) ((LinearLayout) linearLayout.getChildAt(i)).getChildAt(0);
				if (!TextUtils.isEmpty(mDrawingPath)) {
					mJsonObject = GetViewValueUtil.getTypeElevenValue(l, mDrawingPath, mJsonObject);
				}
			} else if (viewTag == ViewTypeUtil.item_view_type_twelve) {
				if(!TextUtils.isEmpty(mPhotoPath)){
					LinearLayout l = (LinearLayout) ((LinearLayout) ((LinearLayout) linearLayout.getChildAt(i)).getChildAt(0)).getChildAt(1);
					mJsonObject = GetViewValueUtil.getTypeTwelveValue(l, mPhotoPath, mJsonObject);
				}
			} else if (viewTag == ViewTypeUtil.item_view_type_thirteen) {
				LinearLayout l = (LinearLayout) ((LinearLayout) linearLayout.getChildAt(i)).getChildAt(0);
				LinearLayout layout = (LinearLayout) l.getChildAt(1);
				getSecondJsonInfo(layout, (String) layout.getTag());
				mJsonObject.put((String) layout.getTag(), mSecondJsonArray);
			} else if (viewTag == ViewTypeUtil.item_view_type_fourteen) {
				LinearLayout l = (LinearLayout) ((LinearLayout) linearLayout.getChildAt(i)).getChildAt(0);
				mJsonObject = GetViewValueUtil.getTypeThreeValue(l, mJsonObject);
			} else if (viewTag == ViewTypeUtil.item_view_type_fifteen) {
				LinearLayout l = (LinearLayout) ((LinearLayout) linearLayout.getChildAt(i)).getChildAt(0);
				mJsonObject = GetViewValueUtil.getTypeThreeValue(l, mJsonObject);
			}
		}
	}


大概意思就是这样,核心的就是这个,其实也不是很复杂。


这样就实现了动态拼接app界面,不用重复性的写大量的界面,当然一般app是没有这么恶心的功能的...但我还是贴出来,希望能帮到后面入坑的同学....


这个由于直接在项目里面写的,不容易抽剥代码。demo暂时先不整理了,大家如果有问题可以加入我的qq群讨论

开发一群:415275066 开发二群:537532956


作者:shaoyezhangliwei 发表于2017/7/27 13:58:30 原文链接
阅读:146 评论:0 查看评论

IOS 获取图像指定点的颜色(UIColor)

$
0
0

在 IOS 开发中,有时候需要获取图像中某个像素点的颜色,返回 UIColor 值。网上收集资料,参考各种方案,最后总结如下:

- (UIColor *)colorAtPixel:(CGPoint)point {
    // 如果点超出图像范围,则退出
    if (!CGRectContainsPoint(CGRectMake(0.0f, 0.0f, self.size.width, self.size.height), point)) {
        return nil;
    }

    // Create a 1x1 pixel byte array and bitmap context to draw the pixel into.
    NSInteger pointX = trunc(point.x);
    NSInteger pointY = trunc(point.y);
    CGImageRef cgImage = self.CGImage;
    NSUInteger width = self.size.width;
    NSUInteger height = self.size.height;
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    int bytesPerPixel = 4;
    int bytesPerRow = bytesPerPixel * 1;
    NSUInteger bitsPerComponent = 8;
    unsigned char pixelData[4] = { 0, 0, 0, 0 };
    CGContextRef context = CGBitmapContextCreate(pixelData,
                                                 1,
                                                 1,
                                                 bitsPerComponent,
                                                 bytesPerRow,
                                                 colorSpace,
                                                 kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
    CGColorSpaceRelease(colorSpace);
    CGContextSetBlendMode(context, kCGBlendModeCopy);

    // Draw the pixel we are interested in onto the bitmap context
    CGContextTranslateCTM(context, -pointX, pointY-(CGFloat)height);
    CGContextDrawImage(context, CGRectMake(0.0f, 0.0f, (CGFloat)width, (CGFloat)height), cgImage);
    CGContextRelease(context);

    // 把[0,255]的颜色值映射至[0,1]区间
    CGFloat red   = (CGFloat)pixelData[0] / 255.0f;
    CGFloat green = (CGFloat)pixelData[1] / 255.0f;
    CGFloat blue  = (CGFloat)pixelData[2] / 255.0f;
    CGFloat alpha = (CGFloat)pixelData[3] / 255.0f;
    return [UIColor colorWithRed:red green:green blue:blue alpha:alpha];
}

如果还需要将返回的 UIColor 值转化成 RGBA 值,则可直接修改上述方法,直接返回相应的数值即可。也可另外封装一个 UIColor 转换成 RGB 值的方法:

- (CGFloat)RGBValueFromUIColor:(UIColor *)color {

    //获得RGB值描述
    NSString *RGBValue = [NSString stringWithFormat:@"%@",color];
    //将RGB值描述分隔成字符串
    NSArray *RGBArr = [RGBValue componentsSeparatedByString:@" "];
    //获取红色值
    CGFloat r = [[RGBArr objectAtIndex:1] floatValue] * 255;
    //获取绿色值
    CGFloat g = [[RGBArr objectAtIndex:2] floatValue] * 255;
    //获取蓝色值
    CGFloat b = [[RGBArr objectAtIndex:3] floatValue] * 255;

    NSLog(@"----------%f--%f--%f----------",r,g,b);
    }

就是这么简单!

下面对内部一些需要了解的方法作简要介绍:

CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); 创建一个通用的 RGB 色彩空间。

注意点:使用 RGB 模式的颜色空间(在Quartz 2D 中凡是使用带有 Create 或者 Copy 关键字方法创建的对象,在使用后一定要使用对应的方法释放(如,本实例中:CGColorSpaceRelease(colorSpace);))。

CGBitmapContextCreate 函数原型:

CGContextRef CGBitmapContextCreate (
   void *data,
   size_t width,
   size_t height,
   size_t bitsPerComponent,
   size_t bytesPerRow,
   CGColorSpaceRef colorspace,
   CGBitmapInfo bitmapInfo
);

data:指向要渲染的绘制内存的地址。这个内存块的大小至少是(bytesPerRow*height)个字节;
width:bitmap的宽度,单位为像素;
height:bitmap的高度,单位为像素;
bitsPerComponent:内存中像素的每个组件的位数。例如,对于32位像素格式和 RGB 颜色空间,应该将这个值设为8;
bytesPerRow:bitmap的每一行在内存所占的比特数;
colorspace:bitmap上下文使用的颜色空间;
bitmapInfo:指定 bitmap 是否包含 alpha 通道,像素中 alpha 通道的相对位置,像素组件是整形还是浮点型等信息的字符串。

当调用这个函数的时候,Quartz 创建一个位图绘制环境,也就是位图上下文。当你向上下文中绘制信息时,Quartz 把你要绘制的信息作为位图数据绘制到指定的内存块。一个新的位图上下文的像素格式由三个参数决定:每个组件的位数,颜色空间,alpha 选项。alpha 值决定了绘制像素的透明性。

CGContextSetBlendMode :设置混合模式,具体细节不赘述,给个参考资料:http://blog.csdn.net/cpskiss/article/details/7056256

CGContextTranslateCTM : 平移坐标系统。该方法相当于把原来位于 (0, 0) 位置的坐标原点平移到 (tx, ty) 点。在平移后的坐标系统上绘制图形时,所有坐标点的 X 坐标都相当于增加了 tx,所有点的 Y 坐标都相当于增加了 ty。对应的还有缩放坐标系统和旋转坐标系统,此处不展开说,后面有机会再分享。

作者:huangfei711 发表于2017/7/27 14:51:09 原文链接
阅读:67 评论:0 查看评论

Android 双开沙箱 VirtualApp 源码分析(四)启动插件 Service

$
0
0

上一章:Android 双开沙箱 VirtualApp 源码分析(三)App 启动

原生 Service 创建过程

首先有必要了解一下原生 framework 对 Service 的创建,因为在 VA 中启动 Service 和 Activity 有很大的区别。

首先入口 ContextWrapper.startService():

@Override
    public ComponentName startService(Intent service) {
        return mBase.startService(service);
    }

mBase 是 ContextImpl,所以调用到 ContextImpl.startService():

 @Override
    public ComponentName startService(Intent service) {
        warnIfCallingFromSystemProcess();
        return startServiceCommon(service, mUser);
    }
    private ComponentName startServiceCommon(Intent service, UserHandle user) {
        try {
            validateServiceIntent(service);
            service.prepareToLeaveProcess(this);
            ComponentName cn = ActivityManagerNative.getDefault().startService(
                mMainThread.getApplicationThread(), service, service.resolveTypeIfNeeded(
                            getContentResolver()), getOpPackageName(), user.getIdentifier());
            if (cn != null) {
                if (cn.getPackageName().equals("!")) {
                    throw new SecurityException(
                            "Not allowed to start service " + service
                            + " without permission " + cn.getClassName());
                } else if (cn.getPackageName().equals("!!")) {
                    throw new SecurityException(
                            "Unable to start service " + service
                            + ": " + cn.getClassName());
                }
            }
            return cn;
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

Client 的流程最后到 ActivityManagerNative.startService():

public int startActivity(IApplicationThread caller, String callingPackage, Intent intent,
            String resolvedType, IBinder resultTo, String resultWho, int requestCode,
            int startFlags, ProfilerInfo profilerInfo, Bundle options) throws RemoteException {
        Parcel data = Parcel.obtain();
        Parcel reply = Parcel.obtain();
        data.writeInterfaceToken(IActivityManager.descriptor);
        data.writeStrongBinder(caller != null ? caller.asBinder() : null);
        data.writeString(callingPackage);
        intent.writeToParcel(data, 0);
        data.writeString(resolvedType);
        data.writeStrongBinder(resultTo);
        data.writeString(resultWho);
        data.writeInt(requestCode);
        data.writeInt(startFlags);
        if (profilerInfo != null) {
            data.writeInt(1);
            profilerInfo.writeToParcel(data, Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
        } else {
            data.writeInt(0);
        }
        if (options != null) {
            data.writeInt(1);
            options.writeToParcel(data, 0);
        } else {
            data.writeInt(0);
        }
        // 远程调用 AMS
        mRemote.transact(START_ACTIVITY_TRANSACTION, data, reply, 0);
        reply.readException();
        int result = reply.readInt();
        reply.recycle();
        data.recycle();
        return result;
    }

不出所料,逻辑再一次转移到远程的 AMS 中,然后我们先忽略 AMS 中的一堆逻辑,最后 AMS 调用到这:

    private final void realStartServiceLocked(ServiceRecord r,
            ProcessRecord app, boolean execInFg) throws RemoteException {
            app.thread.scheduleCreateService(r, r.serviceInfo,
                    mAm.compatibilityInfoForPackageLocked(r.serviceInfo.applicationInfo),
                    app.repProcState);
            sendServiceArgsLocked(r, execInFg, true);
    }

这里的 ProcessRecoder.thread 是 AMS 持有的 Client App 的 IBinder 句柄,通过他可以远程调用到 Client App 的 ApplicationThread 中的 scheduleCreateService 方法:

public final void scheduleCreateService(IBinder token,
                ServiceInfo info, CompatibilityInfo compatInfo, int processState) {
            updateProcessState(processState, false);
            CreateServiceData s = new CreateServiceData();
            s.token = token;
            s.info = info;
            s.compatInfo = compatInfo;

            sendMessage(H.CREATE_SERVICE, s);
        }

这里大家可能发现了,Intent 在 AMS 绕了一圈又回来了,事实上 AMS 在其中好像没有发挥什么作用,其实在外部环境 AMS 还是很重要的,但是在 VA 中,AMS 在 Service 调度中其实没有发挥什么作用。原因有以下几点:

  1. 首先 VA 内部的插件 Service 没有比较暴露给外部 App 调用,所以让 AMS 知晓 Service 的意义不大。
  2. 其次和 Activity 必须有个 StubActivity 让 AMS 持有不一样,Service 生命周期和功能都极其简单,并且没有界面,没有交互,换句话说 Service 和其他 Framework Service(例如 WMS) 没有任何关系,所以其实并不需要 AMS 这一步存在。

    那么综上所诉,为了启动插件 Service 我们其实可以绕过 AMS,直接调用 ApplicationThread 中的 scheduleCreateService 方法,Service 的会话储存交给 VAMS 就行。

startService 的实现

和 startActivity 一样,首先是 Hook:

static class StartService extends MethodProxy {

        @Override
        public String getMethodName() {
            return "startService";
        }

        @Override
        public Object call(Object who, Method method, Object... args) throws Throwable {
            IInterface appThread = (IInterface) args[0];
            Intent service = (Intent) args[1];
            String resolvedType = (String) args[2];
            if (service.getComponent() != null
                    && getHostPkg().equals(service.getComponent().getPackageName())) {
                // for server process
                return method.invoke(who, args);
            }
            int userId = VUserHandle.myUserId();
            // 如果是内部请求,获取原来的 Service
            if (service.getBooleanExtra("_VA_|_from_inner_", false)) {
                userId = service.getIntExtra("_VA_|_user_id_", userId);
                service = service.getParcelableExtra("_VA_|_intent_");
            } else {
                if (isServerProcess()) {
                    userId = service.getIntExtra("_VA_|_user_id_", VUserHandle.USER_NULL);
                }
            }
            service.setDataAndType(service.getData(), resolvedType);
            ServiceInfo serviceInfo = VirtualCore.get().resolveServiceInfo(service, VUserHandle.myUserId());

            if (serviceInfo != null) {
                // 远程调用 VAMS.startService()
                return VActivityManager.get().startService(appThread, service, resolvedType, userId);
            }
            return method.invoke(who, args);
        }

        @Override
        public boolean isEnable() {
            return isAppProcess() || isServerProcess();
        }
    }

和 startActivity 一样将真正业务交给 VAMS.startService():

 private ComponentName startServiceCommon(Intent service,
                                             boolean scheduleServiceArgs, int userId) {
        ServiceInfo serviceInfo = resolveServiceInfo(service, userId);
        if (serviceInfo == null) {
            return null;
        }
        ProcessRecord targetApp = startProcessIfNeedLocked(ComponentUtils.getProcessName(serviceInfo),
                userId,
                serviceInfo.packageName);

        if (targetApp == null) {
            VLog.e(TAG, "Unable to start new Process for : " + ComponentUtils.toComponentName(serviceInfo));
            return null;
        }
        IInterface appThread = targetApp.appThread;
        ServiceRecord r = findRecordLocked(userId, serviceInfo);
        boolean needCreateService = false;
        if (r == null) {
            r = new ServiceRecord();
            r.name = new ComponentName(serviceInfo.packageName, serviceInfo.name);
            r.startId = 0;
            r.activeSince = SystemClock.elapsedRealtime();
            r.process = targetApp;
            r.serviceInfo = serviceInfo;
            needCreateService = true;
        } else {
            if (r.process == null) {
                r.process = targetApp;
                needCreateService = true;
            }
        }

        // 如果 service 尚未创建
        if (needCreateService) {
            try {
                // 调用 ApplicationThread.scheduleCreateService 直接创建 Service
                IApplicationThreadCompat.scheduleCreateService(appThread, r, r.serviceInfo, 0);
            } catch (RemoteException e) {
                e.printStackTrace();
            }

            // Note: If the service has been called for not AUTO_CREATE binding, the corresponding
            // ServiceRecord is already in mHistory, so we use Set to replace List to avoid add
            // ServiceRecord twice
            // 将 ServiceRecorder 推入 history
            addRecord(r);

            // 等待 bindService,如果是通过 bindService 自动创建的 Service,在创建 Service 完成后会进入 bindService 流程
            requestServiceBindingsLocked(r);
        }

        r.lastActivityTime = SystemClock.uptimeMillis();
        if (scheduleServiceArgs) {
            r.startId++;
            boolean taskRemoved = serviceInfo.applicationInfo != null
                    && serviceInfo.applicationInfo.targetSdkVersion < Build.VERSION_CODES.ECLAIR;
            try {
                IApplicationThreadCompat.scheduleServiceArgs(appThread, r, taskRemoved, r.startId, 0, service);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
        return ComponentUtils.toComponentName(serviceInfo);
    }

这里主要做了以下几个工作:

  1. 和 Activity 创建的时候一样,调用 startProcessIfNeedLocked 检查 Application 是否初始化,没有则开始初始化 Application 流程。
  2. 准备 ServiceRecord 和 ServiceInfo。
  3. 如果 service 还没有创建,则直接调用 ApplicationThread.scheduleCreateService 创建 Service,可以看出这里直接跳过了 AMS。
  4. 将 ServiceRecord 记录到 Service 列表,等待 bindService,如果是通过 bindService 自动创建的 Service,在创建 Service 完成后会进入 bindService 流程。

同样的 bindService 也是直接调用系统的 ApplicationThread.scheduleBindService

好了,由于 Service 的特点,startService 看上去比 startActivity 简单多了。接下来要分析的是 BroacastReceiver。

作者:ganyao939543405 发表于2017/7/27 17:23:27 原文链接
阅读:42 评论:0 查看评论

随手一写就是一个侧滑关闭activity

$
0
0

刚忙完一段时间,今天刚清闲会,就把以前写的东西整理整理。于是冥冥中发现有些东西完全可以共享出来,毕竟那么常见,而且简单实用。

实现原因

其实侧滑关闭activity在网上也有大量的文章去介绍他,我也有去看,要么是代码实在太多看不下去,要么就是跑了项目没有反应的。唯一的方法还是自己随手鲁一个~,侧滑这个东西在android中是比较少见的,ios是最常见不过了,因为毕竟他们没有物理返回键。还有UIScrollView那些。然而我们用的最多的QQ也只是有个功能,并没有真正的滑动效果。至于微信的,我记得N久以前滑出了一个bug。也没什么印象了。估计也是极小的概率事件。于是,当初我就强行的鲁了一个。下面我们一步步分析实现的思路以及代码。

百行代码解决侧滑关闭

首先来看下我们一些简单的定义:

private Activity activity;
    private Scroller scroller;
    //上次ACTION_MOVE时的X坐标
    private int last_X;
    //屏幕宽度
    private int width;
    //可滑动的最小X坐标,小于该坐标的滑动不处理
    private int min_X;
    // 页面边缘的阴影图
    private Drawable left_shodow;
    //页面边缘阴影的宽度默认值
    private static final int SHADOW_WIDTH = 16;
    // 页面边缘阴影的宽度
    private int shadow_width;
    // Activity finish标识符
    private boolean isFinish;

这边我已经注释过了,就不做过多就写了。接下来,我们看下我们的一些初始化已经外部调用方法:

  private void initView(Activity activity) {
        this.activity = activity;
        scroller = new Scroller(activity);
        left_shodow = getResources().getDrawable(R.drawable.left_shadow);
        int density = (int) activity.getResources().getDisplayMetrics().density;
        shadow_width = SHADOW_WIDTH * density;
        // 这里你一定要设置成透明背景,不然会影响你看到底层布局
        setBackgroundColor(Color.argb(0, 0, 0, 0));
    }

    public void bindActivity(Activity activity) {
        ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
        View child = decorView.getChildAt(0);
        decorView.removeView(child);
        addView(child);
        decorView.addView(this);

我们主要看下bindactivity这个方法。这个是我们用来绑定一个activity的。这个activity你们可以基于baseactivity实现一个backactivity。为什么要这么做,因为你每个activity都要写这么一句话,我感觉就是浪费时间,一个基类直接解决。这个activity我们可以这么写:

public abstract class SWBackActivity extends Activity {

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        SWBackLayout layout = new SWBackLayout(this);
        layout.bindActivity(this);
    }

    protected abstract void afterInject();

    protected abstract void afterInitView();

}

那么接下来我们看下,如果对手势的处理让他侧滑关闭呢?


    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                last_X = (int) event.getX();
                width = getWidth();
                min_X = width / 10;
                break;
            case MotionEvent.ACTION_MOVE:
                int rightMovedX = last_X - (int) event.getX();
                if (getScrollX() + rightMovedX >= 0) {// 左侧即将滑出屏幕
                    scrollTo(0, 0);
                } else if ((int) event.getX() > min_X) {// 手指处于屏幕边缘时不处理滑动
                    scrollBy(rightMovedX, 0);
                }
                last_X = (int) event.getX();
                break;
            case MotionEvent.ACTION_UP:
                if (-getScrollX() < width / 3) {
                    scrollBack();
                    isFinish = false;
                } else {
                    scrollClose();
                    isFinish = true;
                }
                break;
        }
        return true;
    }

    private void scrollBack() {
        int startX = getScrollX();
        int dx = -getScrollX();
        scroller.startScroll(startX, 0, dx, 0, 300);
        invalidate();
    }

    private void scrollClose() {
        int startX = getScrollX();
        int dx = -getScrollX() - width;
        scroller.startScroll(startX, 0, dx, 0, 300);
        invalidate();
    }

    public void computeScroll() {
        if (scroller.computeScrollOffset()) {
            scrollTo(scroller.getCurrX(), 0);
            postInvalidate();
        } else if (isFinish) {
            activity.finish();
        }
        super.computeScroll();
    }

    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        drawShadow(canvas);
    }

    private void drawShadow(Canvas canvas) {
        // 保存画布当前的状态
        canvas.save();
        // 设置drawable的大小范围
        left_shodow.setBounds(0, 0, shadow_width, getHeight());
        // 让画布平移一定距离
        canvas.translate(-shadow_width, 0);
        // 绘制Drawable
        left_shodow.draw(canvas);
        // 恢复画布的状态
        canvas.restore();
    }

首先我们在ACTION_DOWN记录按下点的X坐标

然后在ACTION_MOVE中判断,如果我们getScrollX() + rightMovedX是否是大于0的,如果大于0,表示Activity处于滑动状态,并且是向左滑动,同时我们进行了判断,手指处于屏幕边缘时不可以滑动。

最后在ACTION_UP中判断如果手指滑动的距离大于布局宽度的1/3,表示将Activity滑出界面,否则滑动到起始位置,我们利用Scroller类的startScroll()方法设置好开始位置,滑动距离和时间,然后调用postInvalidate()刷新界面,之后就到computeScroll()方法中,我们利用scrollTo()方法对该布局的父布局进行滚动,滚动结束之后,我们判断界面是否滑出界面,如果是那就划出页面让activity finish掉。否则,布局就归位。

使用方法

其实使用方法很简单,直接继承SWBackActivity就可以了。那么我们最后来看下效果图:
这里写图片描述

作者:sw950729 发表于2017/7/27 17:40:45 原文链接
阅读:3 评论:0 查看评论

RecyclerView Item 布局宽高无效问题探究

$
0
0

封面

前言

这个问题很早之前就碰到过,后来通过google找到了解决办法,也就没有去管它了,直到最近有朋友问到这个问题,感觉很熟悉却又说不出具体原因,因此,就想通过源码分析一下。顺便做个总结,避免以后出现类似的问题。

问题复现

为什么发现了这个问题呢?是当时要写一个列表,列表本来很简单,一行显示一个文本,实现起来也很容易,一个RecyclerView就搞定。

Activity以及Adapter代码如下:

private void initView() {
        mRecyclerView = (RecyclerView) findViewById(R.id.rv_inflate_test);
        RVAdapter adapter = new RVAdapter();
        adapter.setData(mockData());
        LinearLayoutManager manager = new LinearLayoutManager(this);
        manager.setOrientation(LinearLayoutManager.VERTICAL);
        mRecyclerView.addItemDecoration(new DividerItemDecoration(this,DividerItemDecoration.VERTICAL));
        mRecyclerView.setLayoutManager(manager);
        mRecyclerView.setAdapter(adapter);
        adapter.notifyDataSetChanged();
    }


    private List<String> mockData(){
        List<String> datas = new ArrayList<>();
        for(int i=0;i<100;i++){
            datas.add("这是第"+i+ "个item ");
        }

        return datas;
    }


    public static class RVAdapter extends RecyclerView.Adapter{
        private List<String> mData;

        public void setData(List<String> data) {
            mData = data;
        }

        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            return new InflateViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.inflate_test_item,null));
        }

        @Override
        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
             InflateViewHolder viewHolder = (InflateViewHolder) holder;
             ((InflateViewHolder) holder).mTextView.setText(mData.get(position));
        }

        @Override
        public int getItemCount() {
            return mData == null ? 0:mData.size();
        }

        public static class InflateViewHolder extends RecyclerView.ViewHolder{
            private TextView mTextView;
            public InflateViewHolder(View itemView) {
                super(itemView);
                mTextView = (TextView) itemView.findViewById(R.id.text_item);
            }
        }
    }

然后RecyclerView的item布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="wrap_content">
   <TextView
       android:id="@+id/text_item"
       android:layout_width="match_parent"
       android:layout_height="50dp"
       android:textSize="18sp"
       android:textColor="@android:color/white"
       android:background="#AA47BC"
       android:gravity="center"
       />
</LinearLayout>

代码很简单,就是一个RecyclerView 显示一个简单的列表,一行显示一个文本。写完代码运行看一下效果:

运行效果一看,这是什么鬼?右边空出来这么大一块?一看就觉得是item的布局写错了,难道item的宽写成wrap_content? 那就去改一下嘛。进入item布局一看:

不对啊,明明布局的宽写的是match_parent,为什么运行的结果就是包裹内容的呢?然后就想着既然LinearLayout作为根布局宽失效了,那就换其他几种布局方式试一下呢?

根布局换为FrameLayout,其他不变:

运行效果如下:

效果和LinearLayout一样,还是不行,那再换成RelativeLayout试一下:

看一下运行效果:

换成RelativeLayout后,运行的效果,好像就是我们想要的了,曾经一度以后只要将跟布局换成RelativeLayout,就没有宽高失效的问题了。为了验证这个问题,我改变了高度再来测试,如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="200dp"
              android:layout_height="200dp"
              android:background="@android:color/holo_red_light"
    >
   <TextView
       android:id="@+id/text_item"
       android:layout_width="match_parent"
       android:layout_height="50dp"
       android:textSize="18sp"
       android:textColor="@android:color/white"
       android:background="#AA47BC"
       android:gravity="center"
       />

将布局的宽和高固定一个确定的值200dp,然后再来看一下运行效果。

如上,并没有什么卵用,宽和高都失效了。然后又在固定宽高的情况下将布局换为原来的LinearLayout和FrameLayout,效果和前面一样,包裹内容

因此,不管用什么布局作为根布局都会出现宽高失效的问题,那就得另找原因。到底是什么原因呢?想到以前写了这么多的列表,也没有出现宽高失效的问题啊?于是就去找以前的代码来对比一下:

通过对比,发现宽高失效与不失效的区别在与Adapter中创建ViewHolder是加载布局的方式不同:

LayoutInflater.from(parent.getContext()).inflate(R.layout.inflate_test_item,null)

以上这种加载方式Item宽高失效。

LayoutInflater.from(parent.getContext()).inflate(R.layout.inflate_test_item,parent,false)

以上这种方式加载布局item不会出现宽高失效。,效果如下(宽和高都为200dp):

问题我们算是定位到了,就是加载布局的方式不一样,那么这两种加载布局的写法到底有什么区别呢?这个我们就需要去深入了解inflate这个方法了

inflate 加载布局几种写法的区别

上面我们定位到了RecyclerView item 布局宽高失效的原因在于使用inflate 加载布局时的问题,那么我们就看一下inflate这个方法:

从上图可以看到 inflate 方法有四个重载方法,有两个方法第一个参数接收的是一个布局文件id,另外两个接收的是XmlPullParse,看源码就知道,接收布局文件的inflate方法里面调用的是接收XmlPullParse的方法。

因此,我们一般只调用接收布局文件ID的inflate方法。两个重载方法的区别在于有无第三个参数attachToRoot, 而从源码里里面可以看到,两个参数的方法最终调用的是三个参数的inflate方法:


第三个参数的值是根据第二个参数的值来判断的。

因此我们只需要分析一下三个参数的inflate方法,看一下这个方法的定义:

 /**
     * Inflate a new view hierarchy from the specified xml resource. Throws
     * {@link InflateException} if there is an error.
     * 
     * @param resource ID for an XML layout resource to load (e.g.,
     *        <code>R.layout.main_page</code>)
     * @param root Optional view to be the parent of the generated hierarchy (if
     *        <em>attachToRoot</em> is true), or else simply an object that
     *        provides a set of LayoutParams values for root of the returned
     *        hierarchy (if <em>attachToRoot</em> is false.)
     * @param attachToRoot Whether the inflated hierarchy should be attached to
     *        the root parameter? If false, root is only used to create the
     *        correct subclass of LayoutParams for the root view in the XML.
     * @return The root View of the inflated hierarchy. If root was supplied and
     *         attachToRoot is true, this is root; otherwise it is the root of
     *         the inflated XML file.
     */
    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)

解释:从指定的xml资源文件加载一个新的View,如果发生错误会抛出InflateException异常。
参数解释:
resource:加载的布局文件资源id,如:R.layout.main_page。
root:如果attachToRoot(也就是第三个参数)为true, 那么root就是为新加载的View指定的父View。否则,root只是一个为返回View层级的根布局提供LayoutParams值的简单对象。
attachToRoot: 新加载的布局是否添加到root,如果为false,root参数仅仅用于为xml根布局创建正确的LayoutParams子类(列如:根布局为LinearLayout,则用LinearLayout.LayoutParam)。

了解了这几个参数的意义后,我们来看一下前面提到的两种写法

第一种:root 为null

View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.inflate_test_item,null)

这可能是我们用得比较多的一种方式,直接提供一个布局,返回一个View,根据上面的几个参数解释就知道,这种方式,没有指定新加载的View添加到哪个父容器,也没有root提供LayoutParams布局信息。这个时候,如果调用view.getLayoutParams() 返回的值为null。通过上面的测试,我们知道这种方式会导致RecyclerView Item 布局宽高失效。具体原因稍后再分析。

第二种:root不为null,attachToRoot为false

View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.inflate_test_item,parent,false)

这种方式加载,root不为null,但是attachToRoot 为 false,因此,加载的View不会添加到root,但是会用root生成的LayoutParams信息。这种方式就是上面我们说的 RecyclerView Item 宽高不会失效的加载方式。

那么为什么第一种加载方式RecyclerView Item 布局宽高会失效?而第二种加载方式宽高不会失效呢?我们接下来从原来来分析一下。

源码分析宽高失效原因

1,首先我们来分析一下inflate 方法的源码:

          ....
          //前面省略
          //result是最终返回的View
          View result = root;

            try {
               ...
               // 省略部分代码
               final String name = parser.getName();

                if (DEBUG) {
                    System.out.println("**************************");
                    System.out.println("Creating root view: "
                            + name);
                    System.out.println("**************************");
                }

                if (TAG_MERGE.equals(name)) {
                    if (root == null || !attachToRoot) {
                        throw new InflateException("<merge /> can be used only with a valid "
                                + "ViewGroup root and attachToRoot=true");
                    }

                    rInflate(parser, root, inflaterContext, attrs, false);
                } else {

                   // 重点就在这个else代码块里了
                    //解释1:首先创建了xml布局文件的根View,temp View 

                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                    ViewGroup.LayoutParams params = null;
                   // 解释2:判断root是否为null,不为null,就通过root生成LayoutParams
                    if (root != null) {
                        if (DEBUG) {
                            System.out.println("Creating params from root: " +
                                    root);
                        }
                        // Create layout params that match root, if supplied
                        params = root.generateLayoutParams(attrs);
                        // 解释3:如果在root不为null, 并且attachToRoot为false,就为temp View(也就是通过inflate加载的根View)设置LayoutParams.
                        if (!attachToRoot) {
                            // Set the layout params for temp if we are not
                            // attaching. (If we are, we use addView, below)
                            temp.setLayoutParams(params);
                        }
                    }

                    if (DEBUG) {
                        System.out.println("-----> start inflating children");
                    }
                     //解释4:加载根布局temp View 下面的子View

                    rInflateChildren(parser, temp, attrs, true);

                    if (DEBUG) {
                        System.out.println("-----> done inflating children");
                    }

                    //解释5: 注意这一步,root不为null ,并且attachToRoot 为true时,才将从xml加载的View添加到root.
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }

                    // 解释6:最后,如果root为null,或者attachToRoot为false,那么最终inflate返回的值就是从xml加载的View(temp),否则,返回的就是root(temp已添加到root)
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }

            }

            ...
            //省略部分代码
            return result;
        }

从上面这段代码就能很清楚的说明前面提到的两种加载方式的区别了。

第一种加载方式 root为 null :源码中的代码在 解释1解释6 直接返回的就是从xml加载的temp View。

第二种加载方式 root不为null ,attachToRoot 为false: 源码中在 解释3解释5 ,为temp 设置了通过root生成的LayoutParams信息,但是没有add 添加到root 。

2,RecyclerView 部分源码分析

分析了inflate的源码,那么接下来我们就要看一下RecyclerView 的源码了,看一下是怎么加载item 到 RecyclerView 的。由于RecyclerView的代码比较多,我们就通过关键字来找,主要找holer.itemView ,加载的布局就是ViewHolder中的itemView.

通过源码我们找到了一个方法tryGetViewHolderForPositionByDeadline,其中有一段代码如下:

            //1,重点就在这里了,获取itemView 的LayoutParams
            final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
            final LayoutParams rvLayoutParams;
            if (lp == null) {
                // 2,如果itemView获取到的LayoutParams为null,就生成默认的LayoutParams
                rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
                holder.itemView.setLayoutParams(rvLayoutParams);
            } else if (!checkLayoutParams(lp)) {
                rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
                holder.itemView.setLayoutParams(rvLayoutParams);
            } else {
                rvLayoutParams = (LayoutParams) lp;
            }

            rvLayoutParams.mViewHolder = holder;
            rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound;
            return holder;

其实重点就在这个方法里面了,看一下我注释的两个地方,先获取itemView的LayoutParams,如果获取到的LayoutPrams为null 的话,那么就生成默认的LayoutParams。我们看一下生成默认LayoutParams的方法generateDefaultLayoutParams

 @Override
    protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
        if (mLayout == null) {
            throw new IllegalStateException("RecyclerView has no LayoutManager");
        }
        return mLayout.generateDefaultLayoutParams();
    }

注意,里面又调用了mLayoutgenerateDefaultLayoutParams方法,这个mLayout其实就是RecyclerView 的布局管理器LayoutManager.


可以看到generateDefaultLayoutParams是一个抽象方法,具体的实现由对应的LayoutManager实现,我们用的是LinearLayoutManager,因此我们看一下LinearLayoutManager 的实现。

 /**
     * {@inheritDoc}
     */
    @Override
    public LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT);
    }

卧槽,看到这儿大概就明白了item布局的宽高为什么会失效了,如果使用了默认生成LayoutParams这个方法,宽高都是WRAP_CONTENT。也就是说不管外面你的item根布局 宽高写的多少最终都是包裹内容。

那么前面说的两种方式哪一种用了这个方法呢?其实按照前面的分析和前面的结果来看,我们推测第一种加载方式(root为null)使用了这个方法,而第二种加载方式(root不为null,attachToRoot为false)则没有使用这个方法。因此我们断点调试看一下:

第一种加载方式:

View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.inflate_test_item,null)


通过断点调试如上图,从itemView 中获取的layoutParams为null,因此会调用generateDefaultLayoutParams方法。因此会生成一个宽高都是wrap_content的LayoutParams,最后导致不管外面的item根布局设置的宽高是多少都会失效。

第二种加载方式:

View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.inflate_test_item,parent,false)

断点调试如下图:

从上图可以看出,这种加载方式从itemView是可以获取LayoutParams的,为RecyclerView的LayoutParams,因此就不会生成默认的LayoutParams,布局设置的宽高也就不会失效。

总结

本文了解了infalte 加载布局的几种写法,也解释了每个参数的意义。最后通过源码解释了两种加载布局的方式在RecyclerView 中为什么一种宽高会失效,而另一种则不会失效。因此在使用RecyclerView写列表的时候,我们应该使用item布局不会失效的这种方式:

View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.inflate_test_item,parent,false)

可能有的同学会问,如果加载布局时第三个参数设置为true呢?结果会一样吗?你会发现,一运行就会崩溃

为什么呢?因为相当于 addView 了两次.RecyclerView中不应该这样使用。

好了,以上就是全部内容,如有问题,欢迎指正。

更多精彩文章,尽在公众号【Android技术杂货铺】

作者:zwluoyuxi 发表于2017/7/28 10:29:28 原文链接
阅读:5 评论:0 查看评论

Android 双开沙箱 VirtualApp 源码分析(五)BroadcastReceiver

$
0
0

上一章:Android 双开沙箱 VirtualApp 源码分析(四)启动插件 Service

方案猜测

同 Activity 一样,Client App 在 Menifest 中注册的静态广播外部 AMS 是无法知晓的,经过前几章的分析,相信大家已经是老司机了,我们可以先尝试提出自己的观点。
1. 和 Activity 一样使用 Stub 组件占坑?仔细想一想是无法实现的,因为你无法预先确定 Client App 中广播的 Intent Filter。
2. 使用动态注册即 context.registerBroadcastReceiver 代替静态注册,这确实是个好主意,但是重点在于注册的时机。我们需要在 VAService 启动时就预先把 VA 中所有安装的 Client App 的静态 Receiver 提前注册好,事实上外部 AMS 也是这么做的。不然的话,没有打开的 App 就无法收到广播了。

VA 静态广播注册

从前面我们知道,VAService 的启动时机实在 BinderProvider.onCreate():

@Override
    public boolean onCreate() {
        .....................
        VAppManagerService.get().scanApps();
        .....................
        return true;
    }

看到 VAppManagerService.get().scanApps()————>PersistenceLayer.read()——————->PackagePersistenceLayer.readPersistenceData()——————>VAppManagerService.loadPackage()———->VAppManagerService.loadPackageInnerLocked()————–>BroadcastSystem.startUp();

// 静态 Receiver 的注册
    public void startApp(VPackage p) {
        PackageSetting setting = (PackageSetting) p.mExtras;
        // 遍历 Client App 的 Receiver
        for (VPackage.ActivityComponent receiver : p.receivers) {
            ActivityInfo info = receiver.info;
            // 得到对应 Client App 在 VAService 中的记录列表
            List<BroadcastReceiver> receivers = mReceivers.get(p.packageName);
            if (receivers == null) {
                receivers = new ArrayList<>();
                mReceivers.put(p.packageName, receivers);
            }
            // 注册显式意图
            String componentAction = String.format("_VA_%s_%s", info.packageName, info.name);
            IntentFilter componentFilter = new IntentFilter(componentAction);
            BroadcastReceiver r = new StaticBroadcastReceiver(setting.appId, info, componentFilter);
            mContext.registerReceiver(r, componentFilter, null, mScheduler);
            // 推入记录
            receivers.add(r);
            // 遍历注册隐式意图
            for (VPackage.ActivityIntentInfo ci : receiver.intents) {
                IntentFilter cloneFilter = new IntentFilter(ci.filter);
                SpecialComponentList.protectIntentFilter(cloneFilter);
                r = new StaticBroadcastReceiver(setting.appId, info, cloneFilter);
                mContext.registerReceiver(r, cloneFilter, null, mScheduler);
                // 推入记录
                receivers.add(r);
            }
        }
    }

这里对每个 Client App 静态 Receiver 的信息使用统一的代理 StaticBroadcastReceiver 注册。
1. 首先注册 Receiver 的显式意图,每个显式意图被重定向成格式为 “_VA_PKGNAME_CLASSNAME”的 componentAction 这么做的理由实际是真正注册是 VAService 进程空间的 StaticBroadcastReceiver 代理 Receiver,而不是 VA Client App 进程空间,所以直接注册 VA Client App 中的真实类名是没有意义的,这样通过 VAService 代理然后再从 Intent 中取出的 “_VA_PKGNAME_CLASSNAME”到 VA Client 中找到真正的 Receiver,这个逻辑和 Activity 的处理有些相似。
2. 然后就是遍历 Intent-Filter,每个 Intent-Filter 注册一个 StaticBroadcastReceiver 代理。

这样我们的代理 Receiver 注册完毕了。

下面当代理 Receiver 收到广播时:

 @Override
        public void onReceive(Context context, Intent intent) {
            if (mApp.isBooting()) {
                return;
            }
            if ((intent.getFlags() & FLAG_RECEIVER_REGISTERED_ONLY) != 0 || isInitialStickyBroadcast()) {
                return;
            }
            String privilegePkg = intent.getStringExtra("_VA_|_privilege_pkg_");
            if (privilegePkg != null && !info.packageName.equals(privilegePkg)) {
                return;
            }
            PendingResult result = goAsync();
            if (!mAMS.handleStaticBroadcast(appId, info, intent, new PendingResultData(result))) {
                result.finish();
            }
        }

然后看到 handleStaticBroadcast

boolean handleStaticBroadcast(int appId, ActivityInfo info, Intent intent,
                                  PendingResultData result) {
        // 这里是取出真正的目标 Intent
        Intent realIntent = intent.getParcelableExtra("_VA_|_intent_");
        // 取出真正的目标 component
        ComponentName component = intent.getParcelableExtra("_VA_|_component_");
        // 用户 id
        int userId = intent.getIntExtra("_VA_|_user_id_", VUserHandle.USER_NULL);
        if (realIntent == null) {
            return false;
        }
        if (userId < 0) {
            VLog.w(TAG, "Sent a broadcast without userId " + realIntent);
            return false;
        }
        int vuid = VUserHandle.getUid(userId, appId);
        return handleUserBroadcast(vuid, info, component, realIntent, result);
    }

注意这里取出了真正的 Intent,和 Activity 类似,但是和 Activity 处理不同的是现在的逻辑还在 VAService 中:

然后 handleUserBroadcast()———–>handleStaticBroadcastAsUser()————>performScheduleReceiver():

private void performScheduleReceiver(IVClient client, int vuid, ActivityInfo info, Intent intent,
                                         PendingResultData result) {

        ComponentName componentName = ComponentUtils.toComponentName(info);
        BroadcastSystem.get().broadcastSent(vuid, info, result);
        try {
            // 远程调用 client app 的 scheduleReceiver
            client.scheduleReceiver(info.processName, componentName, intent, result);
        } catch (Throwable e) {
            if (result != null) {
                result.finish();
            }
        }
    }

client.scheduleReceiver() 这时候远程调用了 Client App 的 scheduleReceiver。这样我们回到了 Client App 进程空间:

@Override
    public void scheduleReceiver(String processName, ComponentName component, Intent intent, PendingResultData resultData) {
        ReceiverData receiverData = new ReceiverData();
        receiverData.resultData = resultData;
        receiverData.intent = intent;
        receiverData.component = component;
        receiverData.processName = processName;
        sendMessage(RECEIVER, receiverData);
    }

跟到消息队列中:

case RECEIVER: {
     handleReceiver((ReceiverData) msg.obj);
}
private void handleReceiver(ReceiverData data) {
        BroadcastReceiver.PendingResult result = data.resultData.build();
        try {
            // 依然是检测 Application 是否初始化,没有则初始化
            if (!isBound()) {
                bindApplication(data.component.getPackageName(), data.processName);
            }
            // 获取 Receiver 的 Context,这个context是一个ReceiverRestrictedContext实例,它有两个主要函数被禁掉:registerReceiver()和 bindService()。这两个函数在BroadcastReceiver.onReceive()不允许调用。每次Receiver处理一个广播,传递进来的context都是一个新的实例。
            Context context = mInitialApplication.getBaseContext();
            Context receiverContext = ContextImpl.getReceiverRestrictedContext.call(context);
            String className = data.component.getClassName();
            // 实例化目标 Receiver
            BroadcastReceiver receiver = (BroadcastReceiver) context.getClassLoader().loadClass(className).newInstance();
            mirror.android.content.BroadcastReceiver.setPendingResult.call(receiver, result);
            data.intent.setExtrasClassLoader(context.getClassLoader());
            // 手动调用 onCreate
            receiver.onReceive(receiverContext, data.intent);
            // 通知 Pending 结束
            if (mirror.android.content.BroadcastReceiver.getPendingResult.call(receiver) != null) {
                result.finish();
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(
                    "Unable to start receiver " + data.component
                            + ": " + e.toString(), e);
        }
        // 这里需要远程通知 VAService 广播已送到
        VActivityManager.get().broadcastFinish(data.resultData);
    }

这里就是最关键的地方了,简单点概括就是 new 了真正的 Receiver 然后调用 onCreate 而已。Receiver 生命周期真的非常简单。

需要注意的是,broadCast 发送有个超时机制:

void broadcastFinish(PendingResultData res) {
        synchronized (mBroadcastRecords) {
            BroadcastRecord record = mBroadcastRecords.remove(res.mToken);
            if (record == null) {
                VLog.e(TAG, "Unable to find the BroadcastRecord by token: " + res.mToken);
            }
        }
        mTimeoutHandler.removeMessages(0, res.mToken);
        res.finish();
    }
private final class TimeoutHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            IBinder token = (IBinder) msg.obj;
            BroadcastRecord r = mBroadcastRecords.remove(token);
            if (r != null) {
                VLog.w(TAG, "Broadcast timeout, cancel to dispatch it.");
                r.pendingResult.finish();
            }
        }
    }

这里如果广播超时则会通知 PendingResult 结束,告诉发送方广播结束了。

发送广播的处理

其实上一部分已经讲了很多发送广播的处理了。
这里 Hook 了 broacastIntent 方法:

static class BroadcastIntent extends MethodProxy {

        @Override
        public String getMethodName() {
            return "broadcastIntent";
        }

        @Override
        public Object call(Object who, Method method, Object... args) throws Throwable {
            Intent intent = (Intent) args[1];
            String type = (String) args[2];
            intent.setDataAndType(intent.getData(), type);
            if (VirtualCore.get().getComponentDelegate() != null) {
                VirtualCore.get().getComponentDelegate().onSendBroadcast(intent);
            }
            Intent newIntent = handleIntent(intent);
            if (newIntent != null) {
                args[1] = newIntent;
            } else {
                return 0;
            }

            if (args[7] instanceof String || args[7] instanceof String[]) {
                // clear the permission
                args[7] = null;
            }
            return method.invoke(who, args);
        }


        private Intent handleIntent(final Intent intent) {
            final String action = intent.getAction();
            if ("android.intent.action.CREATE_SHORTCUT".equals(action)
                    || "com.android.launcher.action.INSTALL_SHORTCUT".equals(action)) {

                return VASettings.ENABLE_INNER_SHORTCUT ? handleInstallShortcutIntent(intent) : null;

            } else if ("com.android.launcher.action.UNINSTALL_SHORTCUT".equals(action)) {

                handleUninstallShortcutIntent(intent);

            } else {
                return ComponentUtils.redirectBroadcastIntent(intent, VUserHandle.myUserId());
            }
            return intent;
        }

        private Intent handleInstallShortcutIntent(Intent intent) {
            Intent shortcut = intent.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT);
            if (shortcut != null) {
                ComponentName component = shortcut.resolveActivity(VirtualCore.getPM());
                if (component != null) {
                    String pkg = component.getPackageName();
                    Intent newShortcutIntent = new Intent();
                    newShortcutIntent.setClassName(getHostPkg(), Constants.SHORTCUT_PROXY_ACTIVITY_NAME);
                    newShortcutIntent.addCategory(Intent.CATEGORY_DEFAULT);
                    newShortcutIntent.putExtra("_VA_|_intent_", shortcut);
                    newShortcutIntent.putExtra("_VA_|_uri_", shortcut.toUri(0));
                    newShortcutIntent.putExtra("_VA_|_user_id_", VUserHandle.myUserId());
                    intent.removeExtra(Intent.EXTRA_SHORTCUT_INTENT);
                    intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, newShortcutIntent);

                    Intent.ShortcutIconResource icon = intent.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE);
                    if (icon != null && !TextUtils.equals(icon.packageName, getHostPkg())) {
                        try {
                            Resources resources = VirtualCore.get().getResources(pkg);
                            int resId = resources.getIdentifier(icon.resourceName, "drawable", pkg);
                            if (resId > 0) {
                                //noinspection deprecation
                                Drawable iconDrawable = resources.getDrawable(resId);
                                Bitmap newIcon = BitmapUtils.drawableToBitmap(iconDrawable);
                                if (newIcon != null) {
                                    intent.removeExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE);
                                    intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, newIcon);
                                }
                            }
                        } catch (Throwable e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
            return intent;
        }
  1. 这里拦截了创建快捷图标的 Intent,这是一个发给 Launcher 的隐式广播,VA 把这个请求拦截下来因为如果不拦截这个快捷方式就会指向外部的 App,并且如果外部 App 没有安装,此广播也不会发生作用。VA 把这个广播换成了自己的逻辑。
  2. 注意 ComponentUtils.redirectBroadcastIntent(), 类似 Activity 用代理 Intent 包裹真正的 Intent:
public static Intent redirectBroadcastIntent(Intent intent, int userId) {
        Intent newIntent = intent.cloneFilter();
        newIntent.setComponent(null);
        newIntent.setPackage(null);
        ComponentName component = intent.getComponent();
        String pkg = intent.getPackage();
        if (component != null) {
            newIntent.putExtra("_VA_|_user_id_", userId);
            // 这里显式意图被重定位成 _VA_PKGNAME_CLASSNAME 的格式,与前面注册的时候对应
            newIntent.setAction(String.format("_VA_%s_%s", component.getPackageName(), component.getClassName()));
            newIntent.putExtra("_VA_|_component_", component);
            newIntent.putExtra("_VA_|_intent_", new Intent(intent));
        } else if (pkg != null) {
            newIntent.putExtra("_VA_|_user_id_", userId);
            newIntent.putExtra("_VA_|_creator_", pkg);
            newIntent.putExtra("_VA_|_intent_", new Intent(intent));
            String protectedAction = SpecialComponentList.protectAction(intent.getAction());
            if (protectedAction != null) {
                newIntent.setAction(protectedAction);
            }
        } else {
            newIntent.putExtra("_VA_|_user_id_", userId);
            newIntent.putExtra("_VA_|_intent_", new Intent(intent));
            String protectedAction = SpecialComponentList.protectAction(intent.getAction());
            if (protectedAction != null) {
                newIntent.setAction(protectedAction);
            }
        }
        return newIntent;
    }

Ok, BroadcastReceiver 完毕,下一张分析最后一个组件 ContentProvider。

作者:ganyao939543405 发表于2017/7/28 11:53:27 原文链接
阅读:82 评论:0 查看评论

Protobuf-swift 集成小结 韩俊强的博客

$
0
0

第一步、配置环境(跟着github 上面配置就行了)

这里写图片描述

第二步、把项目从gitHub拉下来(以下都是根据这个github的内容配置的):https://github.com/alexeyxo/protobuf-swift (使用终端操作)

以下两个方法供使用:

$ git clone git@github.com:alexeyxo/protobuf-swift.git 

$ git clone https://github.com/alexeyxo/protobuf-swift.git

第三步、执行 ./scripts/build.sh 文件 (使用终端操作)

使用终端cd到protobuf-swift目录下,然后直接在终端输入以下命令执行(貌似这是一个脚本,反正正常执行会生成一大坨文件)

$ ./scripts/build.sh

第四步、集成protobuf-swift (手动拖进项目, 或者pod管理)

1.直接拖拽进你的工程中,然后添加编译文件:target--> build phases -> Link binary with libraries (注意: 拖进去的/ProtocolBuffers.xcodeproj文件, 在你的工程里不会存在真实的文件夹, 这里拖进去只是一个引用)

2.然后记得 pod 一下 或者把Source文件夹拖进工程:protobuf-swift/Source(和 使用第三方框架一样原理)

$ pod 'ProtocolBuffers-Swift'

$ pod install

第五步、用你自己定义的 .proto 文件生成一个 .swift 文件,.proto 文件使用来写protobuf代码的 (使用终端操作)

1.创建(touch)一个 .proto 文件 ,然后 protobuf 代码就全部写在这个 .ptoto 文件里面

$ touch  Test.proto 

例如代码:

syntax = "proto2";

message UserInfo {
required string name = 1;
required int64 level = 2;
}

message TextMessage {
required UserInfo user = 1;
required string text = 2;
}

message GiftMessage {
required UserInfo user = 1;
required string giftname = 2;
required string giftURL = 3;
required string giftCount = 4;
}

2.cd到你的 .proto 文件位置, 然后 使用终端 编译成 swift文件

$ protoc  Test.proto --swift_out="./"

3.生成的 swift文件就可以直接拖进你的项目工程中了


更多惊喜:手机加iOS开发者交流群:①446310206 ②446310206

作者:qq_31810357 发表于2017/7/28 13:49:05 原文链接
阅读:14 评论:0 查看评论

24种设计模式之责任链设计模式

$
0
0

前言

设计模式重要性不言而喻。掌握了设计模式写法就好比学会了一套通用武林绝学,这篇属于责任链设计模式,闲话不多说

引入例子:

中国古代对妇女制定了“三从四德”的道德规范,“三从”是指“未嫁从父、既嫁从夫、夫死从子”,
也就是说一个女性,在没有结婚的时候要听从于父亲,结了婚后听从于丈夫,丈夫死了还要听儿子的,举
个例子来说,一个女的要出去逛街,同样这样的一个请求,在她没有出嫁前她必须征得父亲的同意,出嫁
之后必须获得丈夫的许可,那丈夫死了怎么办?一般都是男的比女的死的早,还要问问儿子是否允许自己
出去逛街,估计你下边马上要问要是没有儿子怎么办?请示小叔子、侄子等等,在父系社会中,妇女只占
从属地位,现在想想中国的妇女还是比较悲惨的,逛个街还要请示来请示去,而且作为父亲、丈夫、儿子
只有两种选择:要不承担起责任来告诉她允许或不允许逛街,要不就让她请示下一个人,这是整个社会体
系的约束,应用到我们项目中就是业务规则,那我们来看怎么把“三从”通过我们的程序来实现,需求很
简单:通过程序描述一下古代妇女的“三从”制度,好我们先看类图:

1.设计模式类图结构

这里写图片描述

看不懂类图?看下面的代码

1.Iwomen

public interface IWomen {

    // 获得个人请示,你要干什么?出去逛街?约会?还是看电影
    public String getRequestType();// 请求类型

    // 获得个人状况
    public int getType(); // 类型

}

2.Women

public class Women implements IWomen {

    /*
     * 通过一个int类型的参数来描述妇女的个人状况 1---未出嫁 2---出嫁 3---夫死
     */

    public int type;
    public String requestTypeString;

    public Women(int type, String requestTypeString) {
        super();
        this.type = type;
        this.requestTypeString = requestTypeString;
    }

    @Override
    public String getRequestType() {

        return requestTypeString;
    }

    @Override
    public int getType() {

        return type;
    }

}

3.Ihandler

public interface Ihandler {

    //一个女性(女儿,妻子或者是母亲)要求逛街,你要处理这个请求
    public void HandleMessage(IWomen women); //处理消息类型

}

4.son,father,husband

public class Son implements Ihandler {

    @Override
    public void HandleMessage(IWomen women) {
        System.out.println("母亲的请示是: "+women.getRequestType());
        System.out.println("儿子的答复是:同意");

    }

}
public class Husband implements Ihandler {

    @Override
    public void HandleMessage(IWomen women) {
        System.out.println("妻子的请示是: "+women.getRequestType());
        System.out.println("丈夫的答复是:同意");
    }

}
public class Father implements Ihandler {

    @Override
    public void HandleMessage(IWomen women) {
        System.out.println("女儿的请示是: "+women.getRequestType());
        System.out.println("父亲的答复是:同意");
    }

}

public class Client {

    public static void main(String[] args) {

        // 随机挑选几个女性
        Random rand = new Random();
        ArrayList<IWomen> arrayList = new ArrayList();
        for (int i = 0; i < 5; i++) {
            arrayList.add(new Women(rand.nextInt(4), "我要出去逛街"));
        }

        // 定义三个请示对象
        Ihandler father = new Father();
        Ihandler husband = new Husband();
        Ihandler son = new Son();

        for (IWomen iWomen : arrayList) {

            switch (iWomen.getType()) {
            case 1:
                father.HandleMessage(iWomen);
                break;
            case 2:
                husband.HandleMessage(iWomen);
                break;
            case 3:
                son.HandleMessage(iWomen);
                break;

            default:
                break;
            }

        }

    }

}

运行结果:

妻子的请示是: 我要出去逛街
丈夫的答复是:同意
女儿的请示是: 我要出去逛街
父亲的答复是:同意
女儿的请示是: 我要出去逛街
父亲的答复是:同意
妻子的请示是: 我要出去逛街
丈夫的答复是:同意

这个能看懂吧,不知道啥意思?

责任链关注三个点: 
1.要处理的内容 (询问是否可以出去逛街) 
2.处理顺序 (询问顺序) 
3.处理结果。(能不能去逛街)

“三从四德”的旧社会规范已经完整的表现出来了,你看谁向谁请示都定义出来了,但是你是不是发
现这个程序写的有点不舒服?有点别扭?有点想重构它的感觉?那就对了!这段代码有以下几个问题:
失去面向对象的意义。对女儿提出的请示,应该在父亲类中做出决定,父亲这个类应该是知道女儿的
请求应该自己处理,而不是在 Client 类中进行组装出来,也就是说原本应该是父亲这个类做的事情抛给了
其他类进行处理;
迪米特法则相违背。我们在 Client 类中写了 if…eles 的判断条件,你看这个条件体内都是一个接口
IHandler 的三个实现类,谁能处理那个请求,怎么处理,直接在实现类中定义好不就结了吗?你的类我知
道的越少越好,别让我猜测你类中的逻辑,想想看,把这段 if…else 移动到三个实现类中该怎么做?
耦合过重。这个什么意思呢,我们要根据 Women 的 type 来决定使用 IHandler 的那个实现类来处理请
求,我问你,如果 IHanlder 的实现类继续扩展怎么办?修改 Client 类?与开闭原则违背喽!
异常情况没有考虑。妻子只能向丈夫请示吗?如果妻子向自己的父亲请示了,父亲应该做何处理?我
们的程序上可没有体现出来。
既然有这么多的问题,那我们要想办法来解决这些问题,我们可以抽象成这样一个结构,女性的请求
先发送到父亲类,父亲类一看是自己要处理的,就回应处理,如果女儿已经出嫁了,那就要把这个请求转
发到女婿来处理,那女婿一旦去天国报道了,那就由儿子来处理这个请求,类似于这样请求:

这里写图片描述

父亲、丈夫、儿子每个节点有两个选择:要么承担责任,做出回复;要么把请求转发到后序环节。结
构分析的已经很清楚了,那我们看怎么来实现这个功能,先看类图:

这里写图片描述

看到这里就说明第一部分已经明白前面写法属于出力费力不讨好写法,下面重构后的写法才符合设计原则面向对象,单一职责。

1.handler 类

负责 链式调用传递逻辑处理,当然属于核心类

public abstract class Handler {

    protected int level; // 能处理的级别
    protected Handler nextHanlder;

    public Handler(int level) {
        this.level = level;
    }

    // 一个女性(女儿,妻子或者是母亲)要求逛街,你要处理这个请求
    public final void HandleMessage(IWomen women) {
        // 处理消息类型

        if (women.getType() == level) {
            this.Respond(women);
        } else {
            if (nextHanlder != null) {
                nextHanlder.HandleMessage(women);
            } else {
                System.out.println("-----------没地方请示了, 不做处理!---------\n");
            }
        }

    }

    public abstract void Respond(IWomen women);

    /**
     * 
     * 如果你属于你处理的返回,你应该让她找下一个环节的人,比如 女儿出嫁了,还向父亲请示是否可以逛街,那父亲就应该告诉女儿,应该找丈夫请示
     */
    public void setNext(Handler _handler) {
        this.nextHanlder = _handler;
    }

}

2.son father,husband

分别告诉Handler 自己能干什么,其他不用操心。

public class Son extends Handler {

    public Son() {
        super(3);
    }

    @Override
    public void Respond(IWomen women) {
        System.out.println("母亲的请示是: " + women.getRequestType());
        System.out.println("儿子的答复是:同意");
    }

}
public class Husband extends Handler {

 //丈夫只处理妻子的请求
    public Husband() {
        super(2);
    }

    @Override
    public void Respond(IWomen women) {
        System.out.println("--------妻子向丈夫请示-------");
        System.out.println(women.getRequestType());
        System.out.println("丈夫的答复是:同意\n");
    }

}
public class Father extends Handler {

   //父亲只处理女儿的请求
    public Father() {
        super(1);
    }

    @Override
    public void Respond(IWomen women) {
        System.out.println("女儿的请示是: " + women.getRequestType());
        System.out.println("父亲的答复是:同意");
    }

}

3.client

调用方式会有变化


/**
 * 采用古代典故三从四德进行比较,很生动
 * 
 * @author weichyang
 * 
 *         责任链两个职责:
 * 
 *         1.我能做的事情(向上去声明,我能做的事情) 2.有序责任链,链式调用序列需要提前指定,无序责任链,根据列表角标顺序取出来进行链式调用
 *         3.责任链链式调用方,处理方,待处理的问题方。三个职责确认不可。
 * 
 */

public class Client {

    public static void main(String[] args) {

        // 随机挑选几个女性
        Random rand = new Random();
        ArrayList<IWomen> arrayList = new ArrayList();
        for (int i = 0; i < 5; i++) {
            int ii = rand.nextInt(3) + 1;
            arrayList.add(new Women(ii, "我要出去逛街"));
        }

        // 定义三个请示对象
        Handler father = new Father();
        Handler husband = new Husband();
        Handler son = new Son();

        // 组成有序调用链
        father.setNext(husband);
        husband.setNext(son);

        for (IWomen iWomen : arrayList) {
            father.HandleMessage(iWomen);
        }

    }

}

4.women 和 iwomen

没有变化。

运行结果:

女儿的请示是: 我要出去逛街
父亲的答复是:同意
--------妻子向丈夫请示-------
我要出去逛街
丈夫的答复是:同意

--------妻子向丈夫请示-------
我要出去逛街
丈夫的答复是:同意

母亲的请示是: 我要出去逛街
儿子的答复是:同意
母亲的请示是: 我要出去逛街
儿子的答复是:同意

结果也正确,业务调用类 Client 也不用去做判断到底是需要谁去处理,而且 Handler 抽象类的子类以
后可以继续增加下去,只是我们这个传递链增加而已,调用类可以不用了解变化过程,甚至是谁在处理这
个请求都不用知道。
以上讲解的就是责任链模式,你看 Father、Husband、Son 这三个类的处理女性的请求时是不是在传递
呀,每个环节只有两个选项:要么承担责任做出回应,要么向下传递请求。

下一篇 :分析okhttp的链式调用方式

引用:

24中设计模式介绍和六大设计原则.pdf

作者:o279642707 发表于2017/7/27 16:17:54 原文链接
阅读:70 评论:0 查看评论

自定义ViewPagerIndicator

$
0
0

1. 简介

学习Android,自定义View不可避免,之前一直忽视这块内容,现在开始学,应该不算太晚。从常见的ViewPagerIndicator开始,当然,万能的Github上包罗万象,好用的indicator也是不胜枚举,旨在学习自定义View的一般操作过程。


2. 大致思路

做一个简单的ViewPagerIndicator,只支持平均大小的TextView,支持点,矩形和三角形。
1. 使用LinearLayout作为父类;
2. 定义indicator的颜色,高度和半径,当然也可以定义其他的属性;
3. 使用到Path,Paint,Canvas等图形绘制的内容;
4. 使用Kotlin


3. 实现

以圆形indicator为例

3.1 定义自定义属性

styles.xml

    <attr name="y_indicator_color" format="color"/>
    <attr name="y_indicator_radius" format="dimension"/>

    <declare-styleable name="YVPDotIndicator">
        <attr name="y_indicator_color"/>
        <attr name="y_indicator_radius"/>
    </declare-styleable>

3.2 代码实现

通过设置ViewPager,然后从Adapter的getPageTitle方法获取TextView的显示内容,然后添加标签,然后绘制圆形指示器,通过ViewPager的滑动回调方法,设置圆形指示器的位置。

class YVPDotIndicator : LinearLayout {
    private var mStartPos: Float = 0.0F//indicator开始位置
    private var mWidthOffset: Int = 0//初始offset

    private var mPaint: Paint? = null

    private var mIndicatorColor = Color.parseColor("#FFFFFF")//indicator颜色
    private var mIndicatorRadius = 2//圆形indicator半径

    private var mVp: ViewPager? = null
    private var pageListener = InterPageChangeListener()

    private var mTabCount: Int? = 0
    private var mTabWidth: Float? = 0.0F
    private val defaultLayoutParams = LinearLayout.LayoutParams(0, LayoutParams.MATCH_PARENT, 1.0f)

    constructor(context: Context, attrs: AttributeSet) : this(context, attrs, 0)
    constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle) {
        setWillNotDraw(false)

        val dm = resources.displayMetrics

        val a = context.theme.obtainStyledAttributes(attrs, R.styleable.YVPDotIndicator, defStyle, 0)
        mIndicatorRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, mIndicatorRadius.toFloat(), dm).toInt()

        mIndicatorColor = a.getColor(R.styleable.YVPDotIndicator_y_indicator_color, Color.parseColor("#FFFFFF"))
        mIndicatorRadius = a.getDimensionPixelSize(R.styleable.YVPDotIndicator_y_indicator_radius, 2)
        a.recycle()

        initPaint()
    }

    /**
     * 设置ViewPager
     */
    fun setViewPager(vp: ViewPager) {
        mVp = vp
        if (vp.adapter == null) {
            throw IllegalArgumentException()
        }
        notifyDataSetChanged()
        mVp?.addOnPageChangeListener(pageListener)
    }

    fun notifyDataSetChanged() {
        this.removeAllViews()
        mTabCount = mVp?.adapter?.count
        for (i in 0..mTabCount?.let { it - 1 } as Int) {
            addTextTab(i, mVp?.adapter?.getPageTitle(i).toString())
        }
    }

    fun addTextTab(position: Int, title: String) {
        var tab = TextView(context)
        tab.text = title
        tab.gravity = Gravity.CENTER
        tab.setSingleLine()

        tab.isFocusable = true
        tab.setOnClickListener { mVp?.currentItem = position }

        this.addView(tab, position, defaultLayoutParams)
    }

    /**
     * 初始化画笔
     */
    private fun initPaint() {
        mPaint = Paint()
        mPaint?.color = mIndicatorColor
        mPaint?.isAntiAlias = true
        mPaint?.style = Paint.Style.FILL
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        mTabWidth = (w / childCount).toFloat()
        mStartPos = mTabWidth?.let { it/2 } as Float
    }

    override fun dispatchDraw(canvas: Canvas?) {
        canvas?.save()
        canvas?.translate(0.0F, height.toFloat())
        canvas?.drawCircle(mStartPos + mWidthOffset, -mIndicatorRadius.toFloat(), mIndicatorRadius.toFloat(), mPaint)
        canvas?.restore()
        super.dispatchDraw(canvas)
    }

    inner class InterPageChangeListener: ViewPager.OnPageChangeListener {
        override fun onPageScrollStateChanged(state: Int) {

        }

        override fun onPageSelected(position: Int) {

        }

        override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
            val tabWidth = screenWidth / childCount
            mWidthOffset = (tabWidth * position + tabWidth * positionOffset).toInt()
            invalidate()
        }
    }

    /**
     * 获取屏幕宽度

     * @return
     */
    private val screenWidth: Int
        get() {
            val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
            val displayMetrics = DisplayMetrics()
            windowManager.defaultDisplay.getMetrics(displayMetrics)
            return displayMetrics.widthPixels
        }
}

4. 其他Indicator

类似的矩形指示器和三角形指示器,均可按照上面的方式实现。


5. 具体效果

这里写图片描述


6. 示例源码

请移步Github———-YVPIndicator

作者:poorkick 发表于2017/7/28 13:48:44 原文链接
阅读:73 评论:0 查看评论

Android--文件夹管理器

$
0
0

直接上代码:

import android.content.Context;
import android.content.CursorLoader;
import android.database.Cursor;
import android.media.MediaMetadataRetriever;
import android.net.Uri;
import android.os.Environment;
import android.provider.MediaStore;
import android.text.format.DateFormat;
import android.text.format.Formatter;
import android.webkit.MimeTypeMap;

import com.calintat.explorer.R;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Date;
import java.util.Locale;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

public class FileUtils {

    public static File copyFile(File src, File path) throws Exception {

        try {

            if (src.isDirectory()) {

                if (src.getPath().equals(path.getPath())) throw new Exception();

                File directory = createDirectory(path, src.getName());

                for (File file : src.listFiles()) copyFile(file, directory);

                return directory;
            }
            else {

                File file = new File(path, src.getName());

                FileChannel channel = new FileInputStream(src).getChannel();

                channel.transferTo(0, channel.size(), new FileOutputStream(file).getChannel());

                return file;
            }
        }
        catch (Exception e) {

            throw new Exception(String.format("Error copying %s", src.getName()));
        }
    }

    //----------------------------------------------------------------------------------------------

    public static File createDirectory(File path, String name) throws Exception {

        File directory = new File(path, name);

        if (directory.mkdirs()) return directory;

        if (directory.exists()) throw new Exception(String.format("%s already exists", name));

        throw new Exception(String.format("Error creating %s", name));
    }

    public static File deleteFile(File file) throws Exception {

        if (file.isDirectory()) {

            for (File child : file.listFiles()) {

                deleteFile(child);
            }
        }

        if (file.delete()) return file;

        throw new Exception(String.format("Error deleting %s", file.getName()));
    }

    public static File renameFile(File file, String name) throws Exception {

        String extension = getExtension(file.getName());

        if (!extension.isEmpty()) name += "." + extension;

        File newFile = new File(file.getParent(), name);

        if (file.renameTo(newFile)) return newFile;

        throw new Exception(String.format("Error renaming %s", file.getName()));
    }

    public static File unzip(File zip) throws Exception {

        File directory = createDirectory(zip.getParentFile(), removeExtension(zip.getName()));

        FileInputStream fileInputStream = new FileInputStream(zip);

        BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);

        try (ZipInputStream zipInputStream = new ZipInputStream(bufferedInputStream)) {

            ZipEntry zipEntry;

            while ((zipEntry = zipInputStream.getNextEntry()) != null) {

                byte[] buffer = new byte[1024];

                File file = new File(directory, zipEntry.getName());

                if (zipEntry.isDirectory()) {

                    if (!file.mkdirs()) throw new Exception("Error uncompressing");
                }
                else {

                    int count;

                    try (FileOutputStream fileOutputStream = new FileOutputStream(file)) {

                        while ((count = zipInputStream.read(buffer)) != -1) {

                            fileOutputStream.write(buffer, 0, count);
                        }
                    }
                }
            }
        }

        return directory;
    }


    public static File getInternalStorage() {

        //returns the path to the internal storage

        return Environment.getExternalStorageDirectory();
    }

    //----------------------------------------------------------------------------------------------

    public static File getExternalStorage() {

        //returns the path to the external storage or null if it doesn't exist

        String path = System.getenv("SECONDARY_STORAGE");

        return path != null ? new File(path) : null;
    }

    public static File getPublicDirectory(String type) {

        //returns the path to the public directory of the given type

        return Environment.getExternalStoragePublicDirectory(type);
    }

    public static String getAlbum(File file) {

        try {

            MediaMetadataRetriever retriever = new MediaMetadataRetriever();

            retriever.setDataSource(file.getPath());

            return retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM);
        }
        catch (Exception e) {

            return null;
        }
    }

    //----------------------------------------------------------------------------------------------

    public static String getArtist(File file) {

        try {

            MediaMetadataRetriever retriever = new MediaMetadataRetriever();

            retriever.setDataSource(file.getPath());

            return retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST);
        }
        catch (Exception e) {

            return null;
        }
    }

    public static String getDuration(File file) {

        try {

            MediaMetadataRetriever retriever = new MediaMetadataRetriever();

            retriever.setDataSource(file.getPath());

            String duration = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);

            long milliseconds = Long.parseLong(duration);

            long s = milliseconds / 1000 % 60;

            long m = milliseconds / 1000 / 60 % 60;

            long h = milliseconds / 1000 / 60 / 60 % 24;

            if (h == 0) return String.format(Locale.getDefault(), "%02d:%02d", m, s);

            return String.format(Locale.getDefault(), "%02d:%02d:%02d", h, m, s);
        }
        catch (Exception e) {

            return null;
        }
    }

    public static String getLastModified(File file) {

        //returns the last modified date of the given file as a formatted string

        return DateFormat.format("dd MMM yyy", new Date(file.lastModified())).toString();
    }

    public static String getMimeType(File file) {

        //returns the mime type for the given file or null iff there is none

        return MimeTypeMap.getSingleton().getMimeTypeFromExtension(getExtension(file.getName()));
    }

    public static String getName(File file) {

        //returns the name of the file hiding extensions of known file types

        switch (FileType.getFileType(file)) {

            case DIRECTORY:
                return file.getName();

            case MISC_FILE:
                return file.getName();

            default:
                return removeExtension(file.getName());
        }
    }

    public static String getPath(File file) {

        //returns the path of the given file or null if the file is null

        return file != null ? file.getPath() : null;
    }

    public static String getSize(Context context, File file) {

        if (file.isDirectory()) {

            File[] children = getChildren(file);

            if (children == null) return null;

            return String.format("%s items", children.length);
        }
        else {

            return Formatter.formatShortFileSize(context, file.length());
        }
    }

    public static String getStorageUsage(Context context) {

        File internal = getInternalStorage();

        File external = getExternalStorage();

        long f = internal.getFreeSpace();

        long t = internal.getTotalSpace();

        if (external != null) {

            f += external.getFreeSpace();

            t += external.getTotalSpace();
        }

        String use = Formatter.formatShortFileSize(context, t - f);

        String tot = Formatter.formatShortFileSize(context, t);

        return String.format("%s used of %s", use, tot);
    }

    public static String getTitle(File file) {

        try {

            MediaMetadataRetriever retriever = new MediaMetadataRetriever();

            retriever.setDataSource(file.getPath());

            return retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);
        }
        catch (Exception e) {

            return null;
        }
    }

    private static String getExtension(String filename) {

        //returns the file extension or an empty string iff there is no extension

        return filename.contains(".") ? filename.substring(filename.lastIndexOf(".") + 1) : "";
    }

    //----------------------------------------------------------------------------------------------

    public static String removeExtension(String filename) {

        int index = filename.lastIndexOf(".");

        return index != -1 ? filename.substring(0, index) : filename;
    }

    public static int compareDate(File file1, File file2) {

        long lastModified1 = file1.lastModified();

        long lastModified2 = file2.lastModified();

        return Long.compare(lastModified2, lastModified1);
    }

    //----------------------------------------------------------------------------------------------

    public static int compareName(File file1, File file2) {

        String name1 = file1.getName();

        String name2 = file2.getName();

        return name1.compareToIgnoreCase(name2);
    }

    public static int compareSize(File file1, File file2) {

        long length1 = file1.length();

        long length2 = file2.length();

        return Long.compare(length2, length1);
    }

    public static int getColorResource(File file) {

        switch (FileType.getFileType(file)) {

            case DIRECTORY:
                return R.color.directory;

            case MISC_FILE:
                return R.color.misc_file;

            case AUDIO:
                return R.color.audio;

            case IMAGE:
                return R.color.image;

            case VIDEO:
                return R.color.video;

            case DOC:
                return R.color.doc;

            case PPT:
                return R.color.ppt;

            case XLS:
                return R.color.xls;

            case PDF:
                return R.color.pdf;

            case TXT:
                return R.color.txt;

            case ZIP:
                return R.color.zip;

            default:
                return 0;
        }
    }

    //----------------------------------------------------------------------------------------------

    public static int getImageResource(File file) {

        switch (FileType.getFileType(file)) {

            case DIRECTORY:
                return R.drawable.ic_directory;

            case MISC_FILE:
                return R.drawable.ic_misc_file;

            case AUDIO:
                return R.drawable.ic_audio;

            case IMAGE:
                return R.drawable.ic_image;

            case VIDEO:
                return R.drawable.ic_video;

            case DOC:
                return R.drawable.ic_doc;

            case PPT:
                return R.drawable.ic_ppt;

            case XLS:
                return R.drawable.ic_xls;

            case PDF:
                return R.drawable.ic_pdf;

            case TXT:
                return R.drawable.ic_txt;

            case ZIP:
                return R.drawable.ic_zip;

            default:
                return 0;
        }
    }

    public static boolean isStorage(File dir) {

        return dir == null || dir.equals(getInternalStorage()) || dir.equals(getExternalStorage());
    }

    //----------------------------------------------------------------------------------------------

    public static File[] getChildren(File directory) {

        if (!directory.canRead()) return null;

        return directory.listFiles(pathname -> pathname.exists() && !pathname.isHidden());
    }

    //----------------------------------------------------------------------------------------------

    public static ArrayList<File> getAudioLibrary(Context context) {

        ArrayList<File> list = new ArrayList<>();

        Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;

        String data[] = new String[]{MediaStore.Audio.Media.DATA};

        String selection = MediaStore.Audio.Media.IS_MUSIC;

        Cursor cursor = new CursorLoader(context, uri, data, selection, null, null).loadInBackground();

        if (cursor != null) {

            while (cursor.moveToNext()) {

                File file = new File(cursor.getString(cursor.getColumnIndex(data[0])));

                if (file.exists()) list.add(file);
            }

            cursor.close();
        }

        return list;
    }

    //----------------------------------------------------------------------------------------------

    public static ArrayList<File> getImageLibrary(Context context) {

        ArrayList<File> list = new ArrayList<>();

        Uri uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;

        String data[] = new String[]{MediaStore.Images.Media.DATA};

        Cursor cursor = new CursorLoader(context, uri, data, null, null, null).loadInBackground();

        if (cursor != null) {

            while (cursor.moveToNext()) {

                File file = new File(cursor.getString(cursor.getColumnIndex(data[0])));

                if (file.exists()) list.add(file);
            }

            cursor.close();
        }

        return list;
    }

    public static ArrayList<File> getVideoLibrary(Context context) {

        ArrayList<File> list = new ArrayList<>();

        Uri uri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;

        String data[] = new String[]{MediaStore.Video.Media.DATA};

        Cursor cursor = new CursorLoader(context, uri, data, null, null, null).loadInBackground();

        if (cursor != null) {

            while (cursor.moveToNext()) {

                File file = new File(cursor.getString(cursor.getColumnIndex(data[0])));

                if (file.exists()) list.add(file);
            }

            cursor.close();
        }

        return list;
    }

    public static ArrayList<File> searchFilesName(Context context, String name) {

        ArrayList<File> list = new ArrayList<>();

        Uri uri = MediaStore.Files.getContentUri("external");

        String data[] = new String[]{MediaStore.Files.FileColumns.DATA};

        Cursor cursor = new CursorLoader(context, uri, data, null, null, null).loadInBackground();

        if (cursor != null) {

            while (cursor.moveToNext()) {

                File file = new File(cursor.getString(cursor.getColumnIndex(data[0])));

                if (file.exists() && file.getName().startsWith(name)) list.add(file);
            }

            cursor.close();
        }

        return list;
    }

    public enum FileType {

        DIRECTORY, MISC_FILE, AUDIO, IMAGE, VIDEO, DOC, PPT, XLS, PDF, TXT, ZIP;

        public static FileType getFileType(File file) {

            if (file.isDirectory())
                return FileType.DIRECTORY;

            String mime = FileUtils.getMimeType(file);

            if (mime == null)
                return FileType.MISC_FILE;

            if (mime.startsWith("audio"))
                return FileType.AUDIO;

            if (mime.startsWith("image"))
                return FileType.IMAGE;

            if (mime.startsWith("video"))
                return FileType.VIDEO;

            if (mime.startsWith("application/ogg"))
                return FileType.AUDIO;

            if (mime.startsWith("application/msword"))
                return FileType.DOC;

            if (mime.startsWith("application/vnd.ms-word"))
                return FileType.DOC;

            if (mime.startsWith("application/vnd.ms-powerpoint"))
                return FileType.PPT;

            if (mime.startsWith("application/vnd.ms-excel"))
                return FileType.XLS;

            if (mime.startsWith("application/vnd.openxmlformats-officedocument.wordprocessingml"))
                return FileType.DOC;

            if (mime.startsWith("application/vnd.openxmlformats-officedocument.presentationml"))
                return FileType.PPT;

            if (mime.startsWith("application/vnd.openxmlformats-officedocument.spreadsheetml"))
                return FileType.XLS;

            if (mime.startsWith("application/pdf"))
                return FileType.PDF;

            if (mime.startsWith("text"))
                return FileType.TXT;

            if (mime.startsWith("application/zip"))
                return FileType.ZIP;

            return FileType.MISC_FILE;
        }
    }
}


作者:chaoyu168 发表于2017/7/28 14:40:36 原文链接
阅读:59 评论:0 查看评论

Android 双开沙箱 VirtualApp 源码分析(六)ContentProvider

$
0
0

上一章:Android 双开沙箱 VirtualApp 源码分析(五)BroadcastReceiver

Provider 注册

回顾前面,Activity 启动的时候会检查 Application 是否初始化,会调用 bindApplication,里面执行了安装 Provider 的方法:

private void installContentProviders(Context app, List<ProviderInfo> providers) {
        long origId = Binder.clearCallingIdentity();
        Object mainThread = VirtualCore.mainThread();
        try {
            for (ProviderInfo cpi : providers) {
                if (cpi.enabled) {
                    ActivityThread.installProvider(mainThread, app, cpi, null);
                }
            }
        } finally {
            Binder.restoreCallingIdentity(origId);
        }
    }

这里很简单,调用 ActivityThread.installProvider() 注册就这么完成了。

但是,仔细一想事情没那么简单,按照这个逻辑 ContentProvider 是在 Application 启动的时候注册的,那么如果 Application 没有启动,那么自然就没有注册了,这样的话其他 App 怎么找到 Provider 呢?

Activity 是因为有在 VA 的 Menifest 里注册的 StubActivity,这样启动 StubActivity 自然就启动了在“:p(n)”进程。那么对应的,VA 用了 StubContentProvider 么?确实是有的。

但是请不要误会了,这个注册在 “p(n)”进程的 StubContentProvider(继承自 StubContentProvider 的 C1,C2,Cn……) 并不是 StubActivity 那样的为了给插件 Provider 占坑的 Stub 组件。

StubContentProvider 的真正目的是为了让 AMS 通过 system_process 带起 “p(n)”进程,然后 VAMS 用过远程调用 StubProvider.call() 回插件 IClient 的 IBinder 句柄给 VAMS 持有。这样 VAMS 就可以远程调用插件进程 “p(n)”中的方法了。

事实上在前面第二张 App 启动时就有说过相关内容,但是为了避免大家误解还是有必要重复一下。

public class StubContentProvider extends ContentProvider {

    @Override
    public boolean onCreate() {
        return true;
    }

    @Override
    public Bundle call(String method, String arg, Bundle extras) {
        if ("_VA_|_init_process_".equals(method)) {
            return initProcess(extras);
        }
        return null;
    }

    private Bundle initProcess(Bundle extras) {
        ConditionVariable lock = VirtualCore.get().getInitLock();
        if (lock != null) {
            lock.block();
        }
        IBinder token = BundleCompat.getBinder(extras,"_VA_|_binder_");
        int vuid = extras.getInt("_VA_|_vuid_");
        VClientImpl client = VClientImpl.get();
        client.initProcess(token, vuid);
        Bundle res = new Bundle();
        BundleCompat.putBinder(res, "_VA_|_client_", client.asBinder());
        res.putInt("_VA_|_pid_", Process.myPid());
        return res;
    }

getContentProvider

这里依然 Hook 了 getContentProvider 方法:

 static class GetContentProvider extends MethodProxy {

        @Override
        public String getMethodName() {
            return "getContentProvider";
        }

        @Override
        public Object call(Object who, Method method, Object... args) throws Throwable {
            int nameIdx = getProviderNameIndex();
            String name = (String) args[nameIdx];
            int userId = VUserHandle.myUserId();
            // 远程调用 VPMS 从 VPackage 拿到 ProviderInfo
            ProviderInfo info = VPackageManager.get().resolveContentProvider(name, 0, userId);
            // 拿不到说明目标 App 未启动
            if (info != null && info.enabled && isAppPkg(info.packageName)) {
                // 远程调用 VAMS,然后 VAMS 再通过 AMS 远程调用注册在插件进程的 StubContentProvider.call 初始化插件进程
                int targetVPid = VActivityManager.get().initProcess(info.packageName, info.processName, userId);
                if (targetVPid == -1) {
                    return null;
                }
                args[nameIdx] = VASettings.getStubAuthority(targetVPid);
                Object holder = method.invoke(who, args);
                if (holder == null) {
                    return null;
                }
                // ContentProviderHolder 有两个成员变量provider、connection,provider 是目标 Provider 的 IBinder 句柄。
                // connection 则是 callback
                if (BuildCompat.isOreo()) {
                    IInterface provider = ContentProviderHolderOreo.provider.get(holder);
                    if (provider != null) {
                        // 这里是重点,远程调用了 VAMS 的 acquireProviderClient
                        provider = VActivityManager.get().acquireProviderClient(userId, info);
                    }
                    ContentProviderHolderOreo.provider.set(holder, provider);
                    ContentProviderHolderOreo.info.set(holder, info);
                } else {
                    IInterface provider = IActivityManager.ContentProviderHolder.provider.get(holder);
                    if (provider != null) {
                        provider = VActivityManager.get().acquireProviderClient(userId, info);
                    }
                    IActivityManager.ContentProviderHolder.provider.set(holder, provider);
                    IActivityManager.ContentProviderHolder.info.set(holder, info);
                }
                return holder;
            }
            Object holder = method.invoke(who, args);
            if (holder != null) {
                if (BuildCompat.isOreo()) {
                    IInterface provider = ContentProviderHolderOreo.provider.get(holder);
                    info = ContentProviderHolderOreo.info.get(holder);
                    if (provider != null) {
                        provider = ProviderHook.createProxy(true, info.authority, provider);
                    }
                    ContentProviderHolderOreo.provider.set(holder, provider);
                } else {
                    IInterface provider = IActivityManager.ContentProviderHolder.provider.get(holder);
                    info = IActivityManager.ContentProviderHolder.info.get(holder);
                    if (provider != null) {
                        provider = ProviderHook.createProxy(true, info.authority, provider);
                    }
                    IActivityManager.ContentProviderHolder.provider.set(holder, provider);
                }
                return holder;
            }
            return null;
        }


        public int getProviderNameIndex() {
            return 1;
        }

        @Override
        public boolean isEnable() {
            return isAppProcess();
        }
    }

这里主要做了几件事情:

  1. 通过 VPMS 拿到解析好的目标 ProviderInfo,启动目标 Provider 所在的进程,怎么启动?远程调用 VAMS,然后 VAMS 再通过 AMS 远程调用注册在插件进程的 StubContentProvider.call 初始化插件进程。这时候 VAMS 就会持有目标插件进程的 IClient 句柄,以备后续调用。
  2. 准备 ContentProviderHolder 相关的事情,ContentProviderHolder 有两个成员变量provider、connection,provider 是目标 Provider 的 IBinder 句柄。
  3. 远程调用了 VAMS 的 acquireProviderClient(), 将任务抛给了远端的 VAMS。

下面就来看一下 VAMS.acquireProviderClient()

@Override
    public IBinder acquireProviderClient(int userId, ProviderInfo info) {
        ProcessRecord callerApp;
        synchronized (mPidsSelfLocked) {
            callerApp = findProcessLocked(VBinder.getCallingPid());
        }
        if (callerApp == null) {
            throw new SecurityException("Who are you?");
        }
        String processName = info.processName;
        ProcessRecord r;
        synchronized (this) {
            r = startProcessIfNeedLocked(processName, userId, info.packageName);
        }
        if (r != null && r.client.asBinder().isBinderAlive()) {
            try {
                return r.client.acquireProviderClient(info);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
        return null;
    }

可以看到调用了 r.client.acquireProviderClient(info);
r.client 就是前面 initProcess 的时候保存下来的插件进程的 IClient 句柄,那么等于远程调用到了插件的 VClientImpl.acquireProviderClient():

注意现在开始流程是在目标 Provider 的 Client App 进程中

@Override
    public IBinder acquireProviderClient(ProviderInfo info) {
        if (mTempLock != null) {
            mTempLock.block();
        }
        // 这里检查 Application 是否启动,注意注册 Provider 的逻辑也在里面
        if (!isBound()) {
            VClientImpl.get().bindApplication(info.packageName, info.processName);
        }
        // 准备 ContentProviderClient
        IInterface provider = null;
        String[] authorities = info.authority.split(";");
        String authority = authorities.length == 0 ? info.authority : authorities[0];
        ContentResolver resolver = VirtualCore.get().getContext().getContentResolver();
        ContentProviderClient client = null;
        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                client = resolver.acquireUnstableContentProviderClient(authority);
            } else {
                client = resolver.acquireContentProviderClient(authority);
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }
        if (client != null) {
            // 反射获取 provider
            provider = mirror.android.content.ContentProviderClient.mContentProvider.get(client);
            client.release();
        }
        return provider != null ? provider.asBinder() : null;
    }

这里终于看到了调用 bindApplication 的地方,如果 provider 尚未注册,那么这里将会注册 provider。

最终通过反射 android.content.ContentProviderClient.mContentProvider 获取到了目标 provider 的句柄,然后 provider 沿着目标 Client App 进程——> VAMS 进程 ———-> 回到了调用 Client App 进程中,整个获取 provider 的过程正式完成。

到此,四大组件的代理全部完成,本次分析也基本结束,下一章会写一些总结和补充内容。

作者:ganyao939543405 发表于2017/7/28 15:07:50 原文链接
阅读:152 评论:0 查看评论
Viewing all 5930 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>