懒人国度


  • 首页

  • 归档

  • 标签

Android Log 打印

发表于 2018-06-14   |     |   阅读次数

Android Log 打印

很多开发者喜欢在应用里封装一个公共的 log 工具类,使用统一的 log tag 输出日志,大一统的方式,看似方便,其实还是有不少问题的。

  • 统一封装的 log 工具类一般内部有 DEBUG 判断,外层调用打印 log 前不会有 DEBUG 判断,会增加每次 new String,增加 gc
  • 统一 log tag 后,查看 log 时,会只搜索当前 tag,任何一个应用都是依附在系统上的,这样的搜索方式忽略了系统给你打印的 log,造成有些问题貌似毫无线索,其实系统已经打印出提示了
  • 没有 DEBUG 判断,自然无法在 release 版本的时候,通过 proguard 优化掉这些 log 了,给应用安全留下隐患

对于 log 的一些思考,尝试以下方式,不合理之处,轻拍。

  • 所有的 log tag 使用统一的前缀,前缀 2-3 个字符
  • 应用内不同的模块功能使用不同的 tag,不一定拘泥于不同的类使用不同的 tag
  • 打印 log 前都增加 DEBUG 判断

使用方式:

  • 分析 bug 的时候,分析可能模块的问题,找到对应模块的 tag,如果忘记 tag 或者不是很方便快速找到对应的 tag,则搜索前缀。
  • 对于不是一个模块的问题,需要联合起来看的问题,搜索前缀找到对应的 pid 后,查找整个 pid 下所有的 log。
  • 对于需要查看跨启动的 bug,会相对麻烦点,只搜索前缀会有较多无用信息,pid 又不是同一个,可以使用 notepad ++ 的正则搜索功能,搜索多个 pid,当然这种情况比较少,有相应的策略应对就好

树莓派配置记录备份

发表于 2018-02-12   |     |   阅读次数

树莓派版本2016-09-23-raspbian-jessie.zip

一、将树莓派根分区扩展到整张sd卡

image
1、选择Expand Filesystem进行扩展
2、顺便修改下Password

二、开启root用户

1
sudo passwd

后输入两次root用户密码

三、给树莓派设置固定dns

1
2
3
4
#执行 
sudo nano /etc/resolv.conf.head
#添加内容如下:
nameserver 114.114.114.114

四、给树莓派指定ip,网关

1
2
3
4
5
6
7
8
sudo nano /etc/dhcpcd.conf

# 指定接口 eth0
interface eth0
# 指定静态IP,/24表示子网掩码为 255.255.255.0
static ip_address=192.168.1.105/24
# 路由器/网关IP地址
static routers=192.168.1.1

重启,验证下树莓派是否能够正常上网

五、将树莓派作为家里的网关

打开 NAT 网关,执行

1
sudo nano /etc/sysctl.conf

修改指定行为:

1
net.ipv4.ip_forward=1

然后在家里路由器的DHCP服务器里的网关设置为树莓派ip(当然要先给树莓派一个固定ip了)

六、使用树莓派屏蔽家里的广告

1
curl -s "https://raw.githubusercontent.com/jacobsalmela/pi-hole/master/automated%20install/basic-install.sh" | bash

一个命令搞定,然后在路由器的DHCP服务器指定到树莓派ip

七、git server搭建

1、树莓派包含git可以直接使用
2、使用ssh协议(ssh://)
3、使用win下的id_rsa.pub 上传到树莓派中的/home/pi/.ssh/authorized_keys中,一行一个(win下的是有分成两行);另外需要修改authorized_keys这个文件的用户组为pi。

1
chown pi:pi authorized_keys

八、samba 搭建

1
2
sudo apt-get install samba
sudo apt-get install samba-common-bin

1、删除home share dir
2、增加下列配置

1
2
3
4
5
6
[Play]
comment =Pi Play
path = /media/pi/play/
read only = no
public = yes
guest ok = no

3、重启samba

1
sudo /etc/init.d/samba restart

九、硬盘休眠

挂载了两个硬盘,一个硬盘不常用,设置休眠省电、静音。

查看硬盘设备名,一般就是/dev/sda,dev/sdb之类的

1
sudo blkid

确认为/dev/sda

1
sudo nano /etc/hdparm.conf

spindown_time值乘以 5 得到总的时间(单位秒). 例如想配置成空闲10分钟就休眠,spindown_time = 10 * 60 / 5 = 120
增加

1
2
3
4
5
/dev/sda {
write_cache = on
spindown_time = 120
poweron_standby = on
}

Android 耗电量统计

发表于 2017-01-10   |     |   阅读次数

将电池比喻为蓄水池,则系统统计的(DrainType)就是不同类型的排水。

wakeLock 耗电计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public WakelockPowerCalculator(PowerProfile profile) {
mPowerWakelock = profile.getAveragePower(PowerProfile.POWER_CPU_AWAKE);
}


@Override
public void calculateApp(BatterySipper app, BatteryStats.Uid u, long rawRealtimeUs,
long rawUptimeUs, int statsType) {
long wakeLockTimeUs = 0;
final ArrayMap<String, ? extends BatteryStats.Uid.Wakelock> wakelockStats =
u.getWakelockStats();
final int wakelockStatsCount = wakelockStats.size();
for (int i = 0; i < wakelockStatsCount; i++) {
final BatteryStats.Uid.Wakelock wakelock = wakelockStats.valueAt(i);

// Only care about partial wake locks since full wake locks
// are canceled when the user turns the screen off.
BatteryStats.Timer timer = wakelock.getWakeTime(BatteryStats.WAKE_TYPE_PARTIAL);
if (timer != null) {
wakeLockTimeUs += timer.getTotalTimeLocked(rawRealtimeUs, statsType);
}
}
app.wakeLockTimeMs = wakeLockTimeUs / 1000; // convert to millis
mTotalAppWakelockTimeMs += app.wakeLockTimeMs;

// Add cost of holding a wake lock.
app.wakeLockPowerMah = (app.wakeLockTimeMs * mPowerWakelock) / (1000*60*60);
if (DEBUG && app.wakeLockPowerMah != 0) {
Log.d(TAG, "UID " + u.getUid() + ": wake " + app.wakeLockTimeMs
+ " power=" + BatteryStatsHelper.makemAh(app.wakeLockPowerMah));
}
}

wakelock 阻止cpu进入休眠,所以使用了cpu_awake的值进行计算。
首先获得Wakelock的数量,然后逐个遍历得到每个Wakelock对象,得到该对象后,得到BatteryStats.WAKE_TYPE_PARTIAL的唤醒时间,然后累加,其实wakelock有4种,为什么只取partial的时间,具体代码google也没解释的很清楚,只是用一句注释打发了我们。得到总时间后,就可以与构造方法中的单位时间waklock消耗电量相乘得到Wakelock消耗的总电量。

Q:为啥app下有wifi耗电,还要单独一个DrainType统计wifi耗电,是否是重复统计?所有的app和硬件耗电相加为啥是100%?
A:系统会先计算app耗电,将归属在对应uid下面的耗电先计算出来,后再将剩余电量计算出来并归属于对应硬件。以wifi为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public WifiPowerCalculator(PowerProfile profile) {
mIdleCurrentMa = profile.getAveragePower(PowerProfile.POWER_WIFI_CONTROLLER_IDLE);
mTxCurrentMa = profile.getAveragePower(PowerProfile.POWER_WIFI_CONTROLLER_TX);
mRxCurrentMa = profile.getAveragePower(PowerProfile.POWER_WIFI_CONTROLLER_RX);
}

@Override
public void calculateApp(BatterySipper app, BatteryStats.Uid u, long rawRealtimeUs,
long rawUptimeUs, int statsType) {
//获取对应uid下的counter
final BatteryStats.ControllerActivityCounter counter = u.getWifiControllerActivity();
if (counter == null) {
return;
}
//对应uid下的time
final long idleTime = counter.getIdleTimeCounter().getCountLocked(statsType);
final long txTime = counter.getTxTimeCounters()[0].getCountLocked(statsType);
final long rxTime = counter.getRxTimeCounter().getCountLocked(statsType);
app.wifiRunningTimeMs = idleTime + rxTime + txTime;
//已经计算的应用的wifi总耗时
mTotalAppRunningTime += app.wifiRunningTimeMs;

//计算对应uid的app耗电
app.wifiPowerMah =
((idleTime * mIdleCurrentMa) + (txTime * mTxCurrentMa) + (rxTime * mRxCurrentMa))
/ (1000*60*60);
//已经计算的应用的wifi总耗电
mTotalAppPowerDrain += app.wifiPowerMah;

app.wifiRxPackets = u.getNetworkActivityPackets(BatteryStats.NETWORK_WIFI_RX_DATA,
statsType);
app.wifiTxPackets = u.getNetworkActivityPackets(BatteryStats.NETWORK_WIFI_TX_DATA,
statsType);
app.wifiRxBytes = u.getNetworkActivityBytes(BatteryStats.NETWORK_WIFI_RX_DATA,
statsType);
app.wifiTxBytes = u.getNetworkActivityBytes(BatteryStats.NETWORK_WIFI_TX_DATA,
statsType);

if (DEBUG && app.wifiPowerMah != 0) {
Log.d(TAG, "UID " + u.getUid() + ": idle=" + idleTime + "ms rx=" + rxTime + "ms tx=" +
txTime + "ms power=" + BatteryStatsHelper.makemAh(app.wifiPowerMah));
}
}

//这个方法是在所有app耗电计算完成后调用的
@Override
public void calculateRemaining(BatterySipper app, BatteryStats stats, long rawRealtimeUs,
long rawUptimeUs, int statsType) {
//获取总counter
final BatteryStats.ControllerActivityCounter counter = stats.getWifiControllerActivity();

final long idleTimeMs = counter.getIdleTimeCounter().getCountLocked(statsType);
final long txTimeMs = counter.getTxTimeCounters()[0].getCountLocked(statsType);
final long rxTimeMs = counter.getRxTimeCounter().getCountLocked(statsType);
//计算剩余的time
app.wifiRunningTimeMs = Math.max(0,
(idleTimeMs + rxTimeMs + txTimeMs) - mTotalAppRunningTime);

double powerDrainMah = counter.getPowerCounter().getCountLocked(statsType)
/ (double)(1000*60*60);
if (powerDrainMah == 0) {
// Some controllers do not report power drain, so we can calculate it here.
powerDrainMah = ((idleTimeMs * mIdleCurrentMa) + (txTimeMs * mTxCurrentMa)
+ (rxTimeMs * mRxCurrentMa)) / (1000*60*60);
}
//计算剩余的耗电
app.wifiPowerMah = Math.max(0, powerDrainMah - mTotalAppPowerDrain);

if (DEBUG) {
Log.d(TAG, "left over WiFi power: " + BatteryStatsHelper.makemAh(app.wifiPowerMah));
}
}

IDLE计算

不知道IDLE计算啥原理

1
2
3
4
5
6
7
8
mTypeBatteryUptimeUs = mStats.computeBatteryUptime(rawUptimeUs, mStatsType);
mTypeBatteryRealtimeUs = mStats.computeBatteryRealtime(rawRealtimeUs, mStatsType)

final double suspendPowerMaMs = (mTypeBatteryRealtimeUs / 1000) *
mPowerProfile.getAveragePower(PowerProfile.POWER_CPU_IDLE);
final double idlePowerMaMs = (mTypeBatteryUptimeUs / 1000) *
mPowerProfile.getAveragePower(PowerProfile.POWER_CPU_AWAKE);
final double totalPowerMah = (suspendPowerMaMs + idlePowerMaMs) / (60 * 60 * 1000);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Returns the total, last, or current battery uptime in microseconds.
* 上次或当前电池正常运行时间。
* @param curTime the elapsed realtime in microseconds.
* @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT.
*/
public abstract long computeBatteryUptime(long curTime, int which);

/**
* Returns the total, last, or current battery realtime in microseconds.
* 最后或当前电池实时时间
* @param curTime the current elapsed realtime in microseconds.
* @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT.
*/
public abstract long computeBatteryRealtime(long curTime, int which);

CPU 计算

CPU 部分没有remaining

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public void calculateApp(BatterySipper app, BatteryStats.Uid u, long rawRealtimeUs,
long rawUptimeUs, int statsType) {
app.cpuTimeMs = (u.getUserCpuTimeUs(statsType) + u.getSystemCpuTimeUs(statsType)) / 1000;

// Aggregate total time spent on each cluster.
long totalTime = 0;
final int numClusters = mProfile.getNumCpuClusters();
for (int cluster = 0; cluster < numClusters; cluster++) {
final int speedsForCluster = mProfile.getNumSpeedStepsInCpuCluster(cluster);
for (int speed = 0; speed < speedsForCluster; speed++) {
totalTime += u.getTimeAtCpuSpeed(cluster, speed, statsType);
}
}
totalTime = Math.max(totalTime, 1);

double cpuPowerMaMs = 0;
for (int cluster = 0; cluster < numClusters; cluster++) {
final int speedsForCluster = mProfile.getNumSpeedStepsInCpuCluster(cluster);
for (int speed = 0; speed < speedsForCluster; speed++) {
//获取对应cluster,对应speed下所使用时间的比例
final double ratio = (double) u.getTimeAtCpuSpeed(cluster, speed, statsType) /
totalTime;
final double cpuSpeedStepPower = ratio * app.cpuTimeMs *
mProfile.getAveragePowerForCpu(cluster, speed);
if (DEBUG && ratio != 0) {
Log.d(TAG, "UID " + u.getUid() + ": CPU cluster #" + cluster + " step #"
+ speed + " ratio=" + BatteryStatsHelper.makemAh(ratio) + " power="
+ BatteryStatsHelper.makemAh(cpuSpeedStepPower / (60 * 60 * 1000)));
}
cpuPowerMaMs += cpuSpeedStepPower;
}
}
app.cpuPowerMah = cpuPowerMaMs / (60 * 60 * 1000);

OVERCOUNTED,UNACCOUNTED 计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
mMinDrainedPower = (mStats.getLowDischargeAmountSinceCharge()
* mPowerProfile.getBatteryCapacity()) / 100;
mMaxDrainedPower = (mStats.getHighDischargeAmountSinceCharge()
* mPowerProfile.getBatteryCapacity()) / 100;

mTotalPower = mComputedPower;
if (mStats.getLowDischargeAmountSinceCharge() > 1) {
if (mMinDrainedPower > mComputedPower) {
double amount = mMinDrainedPower - mComputedPower;
mTotalPower = mMinDrainedPower;
BatterySipper bs = new BatterySipper(DrainType.UNACCOUNTED, null, amount);


} else if (mMaxDrainedPower < mComputedPower) {
double amount = mComputedPower - mMaxDrainedPower;

// Insert the BatterySipper in its sorted position.
BatterySipper bs = new BatterySipper(DrainType.OVERCOUNTED, null, amount);

}
}
//其中getLowDischargeAmountSinceCharge
@Override
public int getLowDischargeAmountSinceCharge() {
synchronized(this) {
int val = mLowDischargeAmountSinceCharge;
if (mOnBattery && mDischargeCurrentLevel < mDischargeUnplugLevel) {
val += mDischargeUnplugLevel-mDischargeCurrentLevel-1;
}
return val;
}
}
//mLowDischargeAmountSinceCharge在setOnBatteryLocked 会改变这个值。mLowDischargeAmountSinceCharge 和mHighDischargeAmountSinceCharge 这两个变量中,这两个变量差1,是一个误差,受电池level精度影响
void setOnBatteryLocked(final long mSecRealtime, final long mSecUptime, final boolean onBattery,
final int oldStatus, final int level, final int chargeUAh) {
.......
if (onBattery) {
....
} else {
mLastChargingStateLevel = level;
mOnBattery = mOnBatteryInternal = false;
pullPendingStateUpdatesLocked();
mHistoryCur.batteryLevel = (byte)level;
mHistoryCur.states |= HistoryItem.STATE_BATTERY_PLUGGED_FLAG;
if (DEBUG_HISTORY) Slog.v(TAG, "Battery plugged to: "
+ Integer.toHexString(mHistoryCur.states));
addHistoryRecordLocked(mSecRealtime, mSecUptime);
mDischargeCurrentLevel = mDischargePlugLevel = level;
if (level < mDischargeUnplugLevel) {//level代表当前电量,mDischargeUnplugLevel代表上一次拔去usb线的电量
mLowDischargeAmountSinceCharge += mDischargeUnplugLevel-level-1;//累计消耗的电量
mHighDischargeAmountSinceCharge += mDischargeUnplugLevel-level;
}
updateDischargeScreenLevelsLocked(screenOn, screenOn);
updateTimeBasesLocked(false, !screenOn, uptime, realtime);
mChargeStepTracker.init();
mLastChargeStepLevel = level;
mMaxChargeStepLevel = level;
mInitStepMode = mCurStepMode;
mModStepMode = 0;
}
//再看下getLowDischargeAmountSinceCharge
@Override
public int getLowDischargeAmountSinceCharge() {
synchronized(this) {
int val = mLowDischargeAmountSinceCharge;
//表示现在正在用电状态,mDischargeCurrentLevel 变量代表用电的时候的电量,会时时更新,mDischargeUnplugLevel代表上一次拔去usb线的一个电量
if (mOnBattery && mDischargeCurrentLevel < mDischargeUnplugLevel) {
val += mDischargeUnplugLevel-mDischargeCurrentLevel-1;//也就是加上最近的一次消耗电量
}
return val;
}
}
//再看下UNACCOUNTED,OVERCOUNTED计算
mTotalPower = mComputedPower;
if (mStats.getLowDischargeAmountSinceCharge() > 1) {//只统计消耗的电量大于1
if (mMinDrainedPower > mComputedPower) {//如果实际总的消耗电量(min)比统计的电量大
double amount = mMinDrainedPower - mComputedPower;
mTotalPower = mMinDrainedPower;
BatterySipper bs = new BatterySipper(DrainType.UNACCOUNTED, null, amount);


} else if (mMaxDrainedPower < mComputedPower) {//如果实际总的消耗电量(max)比统计的电量小
double amount = mComputedPower - mMaxDrainedPower;

// Insert the BatterySipper in its sorted position.
BatterySipper bs = new BatterySipper(DrainType.OVERCOUNTED, null, amount);

}
}

Gradle工程使用@hide接口

发表于 2016-09-01   |     |   阅读次数

Android 系统存在一些系统级应用与 framework 代码耦合较深,用到了一些 hide 接口,如图

image

非常影响阅读和编码。

一般我们可以通过 framework 编译出来的 class.jar 作为 Global Libraries 导入到项目中,可以解决这个问题。

image

并置顶

image

就可以识别到 hide 接口了。

但是 gradle sync 之后这个配置又会消失,每次更新 gradle.build 文件,重启 IDEA 都会导致需要重新进行这个配置,不胜其烦。

为了解决这个问题,这里提供个方法在 gradle 和 IDE 直接找到一个较好的平衡点 —- 修改 Android 官方 SDK。

这里以 Android 5.0 为例。

  1. 下载 7.0 SDK 包,然后从 \sdk\platforms\android-24 目录下取到 android.jar。
  2. 从编译环境 out/target/common/obj/JAVA_LIBRARIES/framework_intermediates/ 目录下渠道 classes-full-debug.jar
  3. 解压 android.jar,方法:首先改名为 android.zip,然后用 winrar 解压到本地文件夹。解压 classes-full-debug.jar,方法和解压 android.jar 一样。
  4. 将 classes-full-debug.zip 包里面的文件全部复制到 android.zip 对应的文件夹中,然后重新将 android.zip 文件夹打包为 android.jar。此时生成的 android.jar 是包含了所有 @hide 接口的 SDK 包。

这个时候无论你是如何 gradle sync,再次重启 IDEA 都可以找到 hide 接口了。

image

这里提供个 7.0 已经包含 hide 接口的android.jar, 点击下载

Android自带VPN服务框架学习

发表于 2016-07-15   |     |   阅读次数

Android从4.0开始(API LEVEL 15),增加了VpnService这个API,在不用root的情况下就可以使用。

一、基本原理

在介绍如何使用这些新增的API之前,先来说说其基本的原理。

Android设备上,如果已经使用了VpnService框架,建立起了一条从设备到远端的VPN链接,那么数据包在设备上大致经历了如下四个过程的转换:

image

1)应用程序使用socket,将相应的数据包发送到真实的网络设备上。一般移动设备只有无线网卡,因此是发送到真实的WiFi设备上;

2)Android系统通过iptables,使用NAT,将所有的数据包转发到TUN虚拟网络设备上去,端口是tun0;

3)VPN程序通过打开/dev/tun设备,并读取该设备上的数据,可以获得所有转发到TUN虚拟网络设备上的IP包。因为设备上的所有IP包都会被NAT转成原地址是tun0端口发送的,所以也就是说你的VPN程序可以获得进出该设备的几乎所有的数据(也有例外,不是全部,比如回环数据就无法获得);

4)VPN数据可以做一些处理,然后将处理过后的数据包,通过真实的网络设备发送出去。为了防止发送的数据包再被转到TUN虚拟网络设备上,VPN程序所使用的socket必须先被明确绑定到真实的网络设备上去。

二、代码实现

要实现Android设备上的VPN程序,一般需要分别实现一个继承自Activity类的带UI的客户程序和一个继承自VpnService类的服务程序。

申明Service

要想让你的VPN程序正常运行,首先必须要在AndroidManifest.xml申明Service

1
2
3
4
5
6
<service android:name=".service.MyVpnService"
android:permission="android.permission.BIND_VPN_SERVICE">
<intent-filter>
<action android:name="android.net.VpnService"/>
</intent-filter>
</service>

客户程序实现

客户程序一般要首先调用VpnService.prepare函数:

1
2
3
4
5
6
Intent intent = VpnService.prepare(this);  
if (intent != null) {
startActivityForResult(intent, 0);
} else {
onActivityResult(0, RESULT_OK, null);
}

目前Android只支持一条VPN连接,如果新的程序想建立一条VPN连接,必须先中断系统中当前存在的那个VPN连接。

同时,由于VPN程序的权利实在太大了,所以在正式建立之前,还要弹出一个对话框,让用户点头确认。

image

VpnService.prepare函数的目的,主要是用来检查当前系统中是不是已经存在一个VPN连接了,如果有了的话,是不是就是本程序创建的。

如果当前系统中没有VPN连接,或者存在的VPN连接不是本程序建立的,则VpnService.prepare函数会返回一个intent。这个intent就是用来触发确认对话框的,程序会接着调用startActivityForResult将对话框弹出来等用户确认。如果用户确认了,则会关闭前面已经建立的VPN连接,并重置虚拟端口。该对话框返回的时候,会调用onActivityResult函数,并告之用户的选择。

如果当前系统中有VPN连接,并且这个连接就是本程序建立的,则函数会返回null,就不需要用户再确认了。因为用户在本程序第一次建立VPN连接的时候已经确认过了,就不要再重复确认了,直接手动调用onActivityResult函数就行了。

在onActivityResult函数中,处理相对简单:

1
2
3
4
5
6
7
protected void onActivityResult(int request, int result, Intent data) {  
if (result == RESULT_OK) {
Intent intent = new Intent(this, MyVpnService.class);
...
startService(intent);
}
}

如果返回结果是OK的,也就是用户同意建立VPN连接,则将你写的,继承自VpnService类的服务启动起来就行了。

当然,你也可以通过intent传递一些别的参数。

服务程序实现

服务程序必须要继承自android.net.VpnService类:

1
public class MyVpnService extends VpnService ...

VpnService类封装了建立VPN连接所必须的所有函数,后面会逐步用到。

建立链接的第一步是要用合适的参数,创建并初始化好tun0虚拟网络端口,这可以通过在VpnService类中的一个内部类Builder来做到:

1
2
3
4
5
6
7
8
Builder builder = new Builder();  
builder.setMtu(...);
builder.addAddress(...);
builder.addRoute(...);
builder.addDnsServer(...);
builder.addSearchDomain(...);
builder.setSession(...);
builder.setConfigureIntent(...);

ParcelFileDescriptor interface = builder.establish();
可以看到,这里使用了标准的Builder设计模式。在正式建立(establish)虚拟网络接口之前,需要设置好几个参数,分别是:

1)MTU(Maximun Transmission Unit),即表示虚拟网络端口的最大传输单元,如果发送的包长度超过这个数字,则会被分包;

2)Address,即这个虚拟网络端口的IP地址;

3)Route,只有匹配上的IP包,才会被路由到虚拟端口上去。如果是0.0.0.0/0的话,则会将所有的IP包都路由到虚拟端口上去;

4)DNS Server,就是该端口的DNS服务器地址;

5)Search Domain,就是添加DNS域名的自动补齐。DNS服务器必须通过全域名进行搜索,但每次查找都输入全域名太麻烦了,可以通过配置域名的自动补齐规则予以简化;

6)Session,就是你要建立的VPN连接的名字,它将会在系统管理的与VPN连接相关的通知栏和对话框中显示出来;

image

7)Configure Intent,这个intent指向一个配置页面,用来配置VPN链接。它不是必须的,如果没设置的话,则系统弹出的VPN相关对话框中不会出现配置按钮。

最后调用Builder.establish函数,如果一切正常的话,tun0虚拟网络接口就建立完成了。并且,同时还会通过iptables命令,修改NAT表,将所有数据转发到tun0接口上。

这之后,就可以通过读写VpnService.Builder返回的ParcelFileDescriptor实例来获得设备上所有向外发送的IP数据包和返回处理过后的IP数据包到TCP/IP协议栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Packets to be sent are queued in this input stream.  
FileInputStream in = new FileInputStream(interface.getFileDescriptor());

// Packets received need to be written to this output stream.
FileOutputStream out = new FileOutputStream(interface.getFileDescriptor());

// Allocate the buffer for a single packet.
ByteBuffer packet = ByteBuffer.allocate(32767);
...
// Read packets sending to this interface
int length = in.read(packet.array());
...
// Write response packets back
out.write(packet.array(), 0, length);

ParcelFileDescriptor类有一个getFileDescriptor函数,其会返回一个文件描述符,这样就可以将对接口的读写操作转换成对文件的读写操作。

每次调用FileInputStream.read函数会读取一个IP数据包,而调用FileOutputStream.write函数会写入一个IP数据包到TCP/IP协议栈。

这其实基本上就是这个所谓的VpnService的全部了,是不是觉得有点奇怪,半点没涉及到建立VPN链接的事情。这个框架其实只是可以让某个应用程序可以方便的截获设备上所有发送出去和接收到的数据包,仅此而已。能获得这些数据包,当然可以非常方便的将它们封装起来,和远端VPN服务器建立VPN链接,但是这一块VpnService框架并没有涉及,留给你的应用程序自己解决。

还有一点要特别解释一下,一般的应用程序,在获得这些IP数据包后,会将它们再通过socket发送出去。但是,这样做会有问题,你的程序建立的socket和别的程序建立的socket其实没有区别,发送出去后,还是会被转发到tun0接口,再回到你的程序,这样就是一个死循环了。为了解决这个问题,VpnService类提供了一个叫protect的函数,在VPN程序自己建立socket之后,必须要对其进行保护:

1
protect(my_socket);

其背后的原理是将这个socket和真实的网络接口进行绑定,保证通过这个socket发送出去的数据包一定是通过真实的网络接口发送出去的,不会被转发到虚拟的tun0接口上去。

好了,Android系统默认提供的这个VPN框架就只有这么点东西。

最后,简单总结一下:

1)VPN连接对于应用程序来说是完全透明的,应用程序完全感知不到VPN的存在,也不需要为支持VPN做任何更改;

2)并不需要获得Android设备的root权限就可以建立VPN连接。你所需要的只是在你应用程序内的AndroidManifest.xml文件中申明需要一个叫做“android.permission.BIND_VPN_SERVICE”的特殊权限;

3)在正式建立VPN链接之前,Android系统会弹出一个对话框,需要用户明确的同意;

4)一旦建立起了VPN连接,Android设备上所有发送出去的IP包,都会被转发到虚拟网卡的网络接口上去(主要是通过给不同的套接字打fwmark标签和iproute2策略路由来实现的);

5)VPN程序可以通过读取这个接口上的数据,来获得所有设备上发送出去的IP包;同时,可以通过写入数据到这个接口上,将任何IP数据包插入系统的TCP/IP协议栈,最终送给接收的应用程序;

6)Android系统中同一时间只允许建立一条VPN链接。如果有程序想建立新的VPN链接,在获得用户同意后,前面已有的VPN链接会被中断;

7)这个框架虽然叫做VpnService,但其实只是让程序可以获得设备上的所有IP数据包。通过前面的简单分析,大家应该已经感觉到了,这个所谓的VPN服务,的确可以方便的用来在Android设备上建立和远端服务器之间的VPN连接,但其实它也可以被用来干很多有趣的事情,比如可以用来做防火墙,也可以用来抓设备上的所有IP包。

本文大部分内容转载自传送门

懒人国度

懒人国度

尽力去做,但是到了实在不行的时候就原谅自己

5 日志
9 标签
GitHub Weibo
© 2016 - 2018 懒人国度