关于Android休眠之后网络连接工作情况的研究

几个问题:

  1. 当Android设备休眠后,网络连接会断开吗?
  2. 如果网络连接不断开收到数据包后Android设备会被唤醒吗?

Android设备休眠后,网络连接是否断开

这个问题其实很好验证,按如下步骤做一个实验:

  • 步骤
  1. 下载一个TCP调试软件(做为一个TCP Server);
  2. android foregound service 锁屏后网络连接 安卓锁屏后断网_android

  3. 写一个Android平台的TCP Client, 用一个Timer来向Server发送心跳;
  4. 连接TcpServer, 这时候Tcp调试助手会收到来自Android手机的心跳;
  5. 关闭手机屏幕,让手机休眠;
  • 结论

熄灭屏幕后,很快你就会发现心跳没了,但是连接还在。证明了CPU休眠了但连接不会断。

收到数据包是否唤醒设备

同样做一个实验来验证收到数据包是否能唤醒设备

  • 步骤
  1. Tcp连接建立后, 让手机进入休眠;
  2. 通过Tcp调试助手发送一条消息给Android客户端;
  3. 打开手机屏幕, 看Client端是否收到消息;
  • 结论

看到Server端发送过去的消息被打印在了屏幕上。

Android Client端代码

基于Netty网络框架

MainActivity.java

  1. publicclassMainActivityextendsAppCompatActivityimplementsView.OnClickListener,
  2. TcpClient.TcpClientListener{

  3. privateEditText edtMain;
  4. privateButton btnConnect;
  5. privateTcpClient tcpClient;

  6. @Override
  7. protectedvoid onCreate(Bundle savedInstanceState){
  8. super.onCreate(savedInstanceState);
  9. setContentView(R.layout.activity_main);
  10. edtMain =(EditText) findViewById(R.id.edt_main);
  11. btnConnect =(Button) findViewById(R.id.btn_Connect);
  12. tcpClient =newTcpClient();
  13. edtMain.append("onCreate<<<<<<<<<<<<<<<<<");

  14. //Heart beat
  15. newTimer().schedule(newTimerTask(){
  16. @Override
  17. publicvoid run(){
  18. finalCharSequence time =DateFormat.format("hh:mm:ss",System.currentTimeMillis());
  19. if(tcpClient.isConnected()){
  20. tcpClient.send((time +"\r\n").toString().getBytes());
  21. }
  22. runOnUiThread(newRunnable(){
  23. @Override
  24. publicvoid run(){
  25. edtMain.append(time +"\n");
  26. }
  27. });
  28. }
  29. },0,2000);

  30. btnConnect.setOnClickListener(this);
  31. tcpClient.setTcpClentListener(this);
  32. }

  33. @Override
  34. protectedvoid onDestroy(){
  35. edtMain.append("onDestroy>>>>>>>>>>>>>>>>");
  36. super.onDestroy();
  37. }

  38. @Override
  39. publicvoid onClick(View v){
  40. tcpClient.connect("10.1.1.86",3008);
  41. }

  42. @Override
  43. publicvoid received(finalbyte[] msg){
  44. runOnUiThread(newRunnable(){
  45. @Override
  46. publicvoid run(){
  47. edtMain.append("received ============= "+newString(msg));
  48. }
  49. });
  50. }

  51. @Override
  52. publicvoid sent(byte[] sentData){

  53. }

  54. @Override
  55. publicvoid connected(){
  56. runOnUiThread(newRunnable(){
  57. @Override
  58. publicvoid run(){
  59. btnConnect.setText("Connected");
  60. }
  61. });
  62. }

  63. @Override
  64. publicvoid disConnected(int code,String msg){
  65. runOnUiThread(newRunnable(){
  66. @Override
  67. publicvoid run(){
  68. btnConnect.setText("Connect");
  69. }
  70. });
  71. }
  72. }

TcpClient.java

  1. publicclassTcpClient{
  2. //Disconnect code
  3. publicstaticfinalint ERR_CODE_NETWORK_INVALID =0;
  4. publicstaticfinalint ERR_CODE_TIMEOUT =1;
  5. publicstaticfinalint ERR_CODE_UNKNOWN =2;
  6. publicstaticfinalint ERR_CODE_DISCONNECTED =3;

  7. privateEventLoopGroup mGroup;
  8. privateBootstrap mBootstrap;
  9. privateTcpClientListener mListener;
  10. privateChannelFuture mChannelFuture;
  11. privateboolean isConnected =false;

  12. publicTcpClient(){
  13. mGroup =newNioEventLoopGroup();
  14. mBootstrap =newBootstrap();
  15. mBootstrap.group(mGroup)
  16. .channel(NioSocketChannel.class)
  17. .option(ChannelOption.TCP_NODELAY,true)
  18. .handler(newChannelInitializer<SocketChannel>(){
  19. @Override
  20. protectedvoid initChannel(SocketChannel ch)throwsException{
  21. ch.pipeline().addLast("decoder",newFrameDecoder());
  22. ch.pipeline().addLast("handler",newMsgHandler());
  23. }
  24. });
  25. }

  26. publicboolean isConnected(){
  27. return isConnected;
  28. }

  29. /**
  30. * 建立连接
  31. * @author swallow
  32. * @createTime 2016/2/16
  33. * @lastModify 2016/2/16
  34. * @param host Server host
  35. * @param port Server port
  36. * @return
  37. */
  38. publicvoid connect(finalString host,finalint port){
  39. try{
  40. mChannelFuture = mBootstrap.connect(host, port);
  41. // mChannelFuture.sync();
  42. // mChannelFuture.channel().closeFuture();
  43. }catch(Exception e){
  44. mGroup.shutdownGracefully();
  45. if(mListener !=null)
  46. mListener.disConnected(ERR_CODE_TIMEOUT, e.getMessage());
  47. e.printStackTrace();
  48. }
  49. }

  50. /**
  51. * 断开连接
  52. * @author swallow
  53. * @createTime 2016/1/29
  54. * @lastModify 2016/1/29
  55. * @param
  56. * @return
  57. */
  58. publicvoid disConnect(){
  59. if(mChannelFuture ==null)
  60. return;
  61. if(!mChannelFuture.channel().isActive())
  62. return;
  63. mChannelFuture.channel().close();
  64. mChannelFuture.channel().closeFuture();
  65. mGroup.shutdownGracefully();
  66. }

  67. /**
  68. * 消息发送
  69. * @param
  70. * @return
  71. * @author swallow
  72. * @createTime 2016/1/29
  73. * @lastModify 2016/1/29
  74. */
  75. publicvoid send(finalbyte[] data){
  76. finalByteBuf byteBuf =Unpooled.copiedBuffer(data);
  77. mChannelFuture.channel()
  78. .writeAndFlush(byteBuf)
  79. .addListener(
  80. newChannelFutureListener(){
  81. @Override
  82. publicvoid operationComplete(ChannelFuture future)throwsException{
  83. if(mListener !=null)
  84. mListener.sent(data);
  85. }
  86. }
  87. );
  88. }

  89. /**
  90. * 设置TcpClient监听
  91. * @author swallow
  92. * @createTime 2016/1/29
  93. * @lastModify 2016/1/29
  94. * @param
  95. * @return
  96. */
  97. publicvoid setTcpClentListener(TcpClientListener listener){
  98. this.mListener = listener;
  99. }

  100. /**
  101. * @className: FrameDecoder
  102. * @classDescription: Decode to frames
  103. * @author: swallow
  104. * @createTime: 2015/11/23
  105. */
  106. privateclassFrameDecoderextendsByteToMessageDecoder{
  107. @Override
  108. protectedvoid decode(ChannelHandlerContext ctx,ByteBufin,List<Object>out)throwsException{
  109. byte[] data =newbyte[in.readableBytes()];
  110. in.readBytes(data);
  111. out.add(data);
  112. // // 不够4个字节则不予解析
  113. // if (in.readableBytes() < 4) {
  114. // return;
  115. // }
  116. // in.markReaderIndex(); // mina in.mark();
  117. //
  118. // // 获取帧长度
  119. // byte[] headers = new byte[4];
  120. // in.readBytes(headers);
  121. // int frameLen = Utils.bytesToInt(headers);
  122. // if (frameLen > in.readableBytes()) {
  123. // in.resetReaderIndex();
  124. // return;
  125. // }
  126. //
  127. // //获取一个帧
  128. // byte[] data = new byte[frameLen];
  129. // in.readBytes(data);
  130. // out.add(data);
  131. }
  132. }


  133. /**
  134. * @className: MsgHandler
  135. * @classDescription: 经过FrameDecoder的消息处理器
  136. * @author: swallow
  137. * @createTime: 2015/11/23
  138. */
  139. privateclassMsgHandlerextendsSimpleChannelInboundHandler<byte[]>{

  140. @Override
  141. protectedvoid channelRead0(ChannelHandlerContext ctx,byte[] msg)throwsException{
  142. if(mListener !=null)
  143. mListener.received(msg);
  144. }

  145. @Override
  146. publicvoid channelActive(ChannelHandlerContext ctx)throwsException{
  147. if(mListener !=null){
  148. isConnected =true;
  149. mListener.connected();
  150. }
  151. }

  152. @Override
  153. publicvoid channelInactive(ChannelHandlerContext ctx)throwsException{
  154. if(mListener !=null){
  155. isConnected =false;
  156. mListener.disConnected(ERR_CODE_DISCONNECTED,"The connection is disconnected!");
  157. }
  158. }

  159. @Override
  160. publicvoid exceptionCaught(ChannelHandlerContext ctx,Throwable cause)throwsException{
  161. super.exceptionCaught(ctx, cause);
  162. }
  163. }

  164. /**
  165. * @className: TcpClientListener
  166. * @classDescription: The client listener
  167. * @author: swallow
  168. * @createTime: 2015/11/25
  169. */
  170. publicinterfaceTcpClientListener{
  171. void received(byte[] msg);

  172. void sent(byte[] sentData);

  173. void connected();

  174. void disConnected(int code,String msg);
  175. }
  176. }

activity_main.xml

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <RelativeLayoutxmlns:android="http://schemas.android.com/apk/res/android"
  3. xmlns:tools="http://schemas.android.com/tools"
  4. android:layout_width="match_parent"
  5. android:layout_height="match_parent"
  6. android:paddingBottom="@dimen/activity_vertical_margin"
  7. android:paddingLeft="@dimen/activity_horizontal_margin"
  8. android:paddingRight="@dimen/activity_horizontal_margin"
  9. android:paddingTop="@dimen/activity_vertical_margin"
  10. tools:context="cn.ecpark.devicesleep.MainActivity">

  11. <Button
  12. android:id="@+id/btn_Connect"
  13. android:layout_width="match_parent"
  14. android:layout_height="wrap_content"
  15. android:layout_alignParentBottom="true"
  16. android:text="Connect"/>

  17. <ScrollView
  18. android:layout_width="match_parent"
  19. android:layout_height="match_parent"
  20. android:layout_above="@+id/btn_Connect">

  21. <EditText
  22. android:id="@+id/edt_main"
  23. android:layout_width="match_parent"
  24. android:layout_height="match_parent"
  25. android:editable="false"/>

  26. </ScrollView>

  27. </RelativeLayout>

原理

如果一开始就对Android手机的硬件架构有一定的了解,设计出的应用程序通常不会成为待机电池杀手,而要设计出正确的通信机制与通信协议也并不困难。但如果不去了解而盲目设计,可就没准了。

首先Android手机有两个处理器,一个叫Application Processor(AP),一个叫Baseband Processor(BP)。AP是ARM架构的处理器,用于运行Linux+Android系统;BP用于运行实时操作系统(RTOS),通讯协议栈运行于BP的RTOS之上。非通话时间,BP的能耗基本上在5mA左右,而AP只要处于非休眠状态,能耗至少在50mA以上,执行图形运算时会更高。另外LCD工作时功耗在100mA左右,WIFI也在100mA左右。一般手机待机时,AP、LCD、WIFI均进入休眠状态,这时Android中应用程序的代码也会停止执行。

Android为了确保应用程序中关键代码的正确执行,提供了Wake Lock的API,使得应用程序有权限通过代码阻止AP进入休眠状态。但如果不领会Android设计者的意图而滥用Wake Lock API,为了自身程序在后台的正常工作而长时间阻止AP进入休眠状态,就会成为待机电池杀手。比如前段时间的某应用,比如现在仍然干着这事的某应用。

首先,完全没必要担心AP休眠会导致收不到消息推送。通讯协议栈运行于BP,一旦收到数据包,BP会将AP唤醒,唤醒的时间足够AP执行代码完成对收到的数据包的处理过程。其它的如Connectivity事件触发时AP同样会被唤醒。那么唯一的问题就是程序如何执行向服务器发送心跳包的逻辑。你显然不能靠AP来做心跳计时。Android提供的Alarm Manager就是来解决这个问题的。Alarm应该是BP计时(或其它某个带石英钟的芯片,不太确定,但绝对不是AP),触发时唤醒AP执行程序代码。那么Wake Lock API有啥用呢?比如心跳包从请求到应答,比如断线重连重新登陆这些关键逻辑的执行过程,就需要Wake Lock来保护。而一旦一个关键逻辑执行成功,就应该立即释放掉Wake Lock了。两次心跳请求间隔5到10分钟,基本不会怎么耗电。除非网络不稳定,频繁断线重连,那种情况办法不多。

网上有说使用AlarmManager,因为AlarmManager 是Android 系统封装的用于管理 RTC 的模块,RTC (Real Time Clock) 是一个独立的硬件时钟,可以在 CPU 休眠时正常运行,在预设的时间到达时,通过中断唤醒 CPU。