默认
打赏 发表评论 5
全面盘点当前Android后台保活方案的真实运行效果(截止2019年前)
阅读(12621)?|?评论(5 收藏2 淘帖1 2

本文原作者“minminaya”,作者网站:minminaya.cn,为了提升文章品质,365bet最新官网备用网址_365bet怎么提款_365bet行政费用网对内容作了幅修订和改动,感谢原作者。


1、引言


对于IM应用和消息推送服务的开发者来说,在Android机型上的后台保活是个相当头疼的问题。

老板一句:“为什么微信、QQ能收到消息,而你写的APP却不行?”,直接让人崩溃,话说老板你这APP要是整成微信、APP那么牛,直接进手机厂商白名单,还要程序员在这瞎忙活?

好了,抱怨归抱怨,活还得干,不然靠谁养活广大苦逼的程序员?

回到正题,Android程序员都知道,随着Android系统的不断完善和升级,Andriod应用的后台保活是一次比一次难(详见《Android P正式版即将到来:后台应用保活、消息推送的真正噩梦),但日子还得过,只能一次次绞尽脑汁想出各种黑科技。但不幸的是,因为Andriod系统的不断升级,各种黑科技也只能适应某些版本的Android系统,无法一劳永逸解决问题。

全面盘点当前Android后台保活方案的真实运行效果(截止2019年前)_333.jpeg
▲ Android各版本都是用“甜品”命名的

正因为Android系统版本的差异,也导致了各种保活黑科技的运行效果大相径庭,所以本文正好借此机会,盘点一下当前主流(截止2019年前)的保活黑科技在市面上各版本Android手机上的运行效果,希望能给大家提供一些客观的参考

2、先总结一下,Android端APP为何要搞保活黑科技?


* 本节内容摘录自365bet最新官网备用网址_365bet怎么提款_365bet行政费用网整理的《Android P正式版即将到来:后台应用保活、消息推送的真正噩梦》一文

其实Android端APP搞保活的目的倒不是为了干什么见不得人的坏事(但不排除动机不纯的开发者),主要是像IM365bet最新官网备用网址_365bet怎么提款_365bet行政费用应用和资讯类应用等需要搞后台消息推送、运动类应用需要在后台实时监测用户的运动数据等,因为现在越来越多的手机厂商为了省电策略考虑,基本上如果你的应用没有被加入白名单,一旦处于后台就会被系统限制甚至干掉,但使用APP的用户才不听你这些解释——反正“我”就要你的APP能如期正常运行,开发者也是不得已而为之。

以消息推送为例,当APP处于后台或关闭时,消息推送对于某些应用来说非常有用,比如:

  • 1)IM365bet最新官网备用网址_365bet怎么提款_365bet行政费用聊天应用:聊天消息通知、音视频聊天呼叫等,典型代表有:微信、QQ、易信、米聊、钉钉、Whatsup、Line;
  • 2)新闻资讯应用:最新资讯通知等,典型代表有:网易新闻客户端、腾讯新闻客户端;
  • 3)SNS社交应用:转发/关注/赞等通知,典型代表有:微博、知乎;
  • 4)邮箱客户端:新邮件通知等,典型代表有:QQ邮箱客户端、Foxmail客户端、网易邮箱大师;
  • 5)金融支付应用:收款通知、转账通知等,典型代表有:支付宝、各大银行的手机银行等;
  • ??.... ....

在上述的各种应用中,尤其对于用户接触最多、最平常的IM聊天应用或新闻资讯来说,保活和消息推送简直事关APP的“生死”,消息推送这种能力已经被越来越多的APP作为基础能力之一,因为移动互联网时代下,用户的“全时在线”能力非常诱人和强大,能随时随地即时地将各种重要信息推送给用户,无疑是非常有意义的。

题外话:实际上,对于后台消息推送能力,Android原版系统早就内置了系统级推送服务(跟iOS上的APNs服务是一个东西),它就是GCM服务(现在升级为FCM了),但众所周之的原因,谷哥的服务在国内都是用不了的(你懂的)——无奈啊!

有关GCM的介绍详见:《移动端IM实践:谷歌消息推送服务(GCM)研究(来自微信)》、《为何微信、QQ这样的IM工具不使用GCM服务推送消息?》、《求教android消息推送:GCM、XMPP、MQTT三种方案的优劣)。

全面盘点当前Android后台保活方案的真实运行效果(截止2019年前)_11.jpg
▲ 如果Android能有iOS的APNs这么强势的方案存在,那该是多美的事 ...

3、相关文章



4、常见的Android端保活黑科技方案盘点


主要黑科技方案有:

  • 1)监听广播:监听全局的静态广播,比如时间更新的广播、开机广播、解锁屏、网络状态、解锁加锁亮屏暗屏(3.1版本),高版本需要应用开机后运行一次才能监听这些系统广播,目前此方案失效。可以更换思路,做APP启动后的保活(监听广播启动保活的前台服务);
  • 2)定时器、JobScheduler:假如应用被系统杀死,那么定时器则失效,此方案失效。JobService在5.0,5.1,6.0作用很大,7.0时候有一定影响(可以在电源管理中给APP授权);
  • 3)双进程(NDK方式Fork子进程)、双Service守护:高版本已失效,5.0起系统回收策略改成进程组。双Service方案也改成了应用被杀,任何后台Service无法正常状态运行;
  • 4)提高Service优先级:只能一定程度上缓解Service被立马回收。

针对上述方案,具体的实现思路,通常是这样的:

  • 1)进程拉活:AIDL方式单进程、双进程方式保活Service(最极端的例子就是推送厂商的互相唤醒复活:极光、友盟、以及各大厂商的推送,同派系APP广播互相唤醒:比如今日头条系、阿里系);
  • 2)降低oom_adj的值:常驻通知栏(可通过启动另外一个服务关闭Notification,不对oom_adj值有影响)、使用”1像素“的Activity覆盖在getWindow()的view上(据传某不可言说的IM大厂用过这个方案,虽然他们从未正面承认过)、循环播放无声音频(黑科技,7.0下杀不掉);
  • 3)监听锁屏广播:使Activity始终保持前台;
  • 4)使用自定义锁屏界面:覆盖了系统锁屏界面;
  • 5)创建子进程:通过android:process属性来为Service创建一个进程;
  • 6)白名单:跳转到系统白名单界面让用户自己添加app进入白名单。

5、汇总一下,主要的保活黑科技方案的具体代码实现


5.1黑科技代码实现1:双进程拉活方案的代码实现


使用AIDL绑定方式新建2个Service优先级(防止服务同时被系统杀死)不一样的守护进程互相拉起对方,并在每一个守护进程的ServiceConnection的绑定回调里判断保活Service是否需要重新拉起和对守护线程进行重新绑定。

关于本方案的具体实现,365bet最新官网备用网址_365bet怎么提款_365bet行政费用网的以下文章有更详细的介绍,您也可以仔细研读:


本方案的具体代码实现,主要由以下4步构成。

1)新建一个AIDL文件:
KeepAliveConnection
interface KeepAliveConnection  {
}

2)新建一个服务类StepService,onBind()方法返回new KeepAliveConnection.Stub()对象,并在ServiceConnection的绑定回调中对守护进程服务类GuardService的启动和绑定:
/**
* 主进程 双进程通讯
*
* @author LiGuangMin
* @time Created by 2018/8/17 11:26
*/
public class StepService extends Service {

   private final static String TAG = StepService.class.getSimpleName();
   private ServiceConnection mServiceConnection = new ServiceConnection() {
       @Override
       public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
           Logger.d(TAG, "StepService:建立链接");
           boolean isServiceRunning = ServiceAliveUtils.isServiceAlice();
           if (!isServiceRunning) {
               Intent i = new Intent(StepService.this, DownloadService.class);
               startService(i);
           }
       }

       @Override
       public void onServiceDisconnected(ComponentName componentName) {
           // 断开链接
           startService(new Intent(StepService.this, GuardService.class));
           // 重新绑定
           bindService(new Intent(StepService.this, GuardService.class), mServiceConnection, Context.BIND_IMPORTANT);
       }
   };

   @Nullable
   @Override
   public IBinder onBind(Intent intent) {
       return new KeepAliveConnection.Stub() {
       };
   }

   @Override
   public int onStartCommand(Intent intent, int flags, int startId) {
       startForeground(1, new Notification());
       // 绑定建立链接
       bindService(new Intent(this, GuardService.class), mServiceConnection, Context.BIND_IMPORTANT);
       return START_STICKY;
   }
}

3)对守护进程GuardService进行和2一样的处理:
/**
* 守护进程 双进程通讯
*
* @author LiGuangMin
* @time Created by 2018/8/17 11:27
*/
public class GuardService extends Service {
   private final static String TAG = GuardService.class.getSimpleName();
   private ServiceConnection mServiceConnection = new ServiceConnection() {
       @Override
       public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
           Logger.d(TAG, "GuardService:建立链接");
           boolean isServiceRunning = ServiceAliveUtils.isServiceAlice();
           if (!isServiceRunning) {
               Intent i = new Intent(GuardService.this, DownloadService.class);
               startService(i);
           }
       }

       @Override
       public void onServiceDisconnected(ComponentName componentName) {
           // 断开链接
           startService(new Intent(GuardService.this, StepService.class));
           // 重新绑定
           bindService(new Intent(GuardService.this, StepService.class), mServiceConnection, Context.BIND_IMPORTANT);
       }
   };

   @Nullable
   @Override
   public IBinder onBind(Intent intent) {
       return new KeepAliveConnection.Stub() {
       };
   }

   @Override
   public int onStartCommand(Intent intent, int flags, int startId) {
       startForeground(1, new Notification());
       // 绑定建立链接
       bindService(new Intent(this, StepService.class), mServiceConnection, Context.BIND_IMPORTANT);
       return START_STICKY;
   }
}

4)在Activity中在启动需要保活的DownloadService服务后然后启动保活的双进程:
public class MainActivity extends AppCompatActivity {
   private TextView mShowTimeTv;
   private DownloadService.DownloadBinder mDownloadBinder;
   private ServiceConnection mServiceConnection = new ServiceConnection() {
       @Override
       public void onServiceConnected(ComponentName name, IBinder service) {
           mDownloadBinder = (DownloadService.DownloadBinder) service;
           mDownloadBinder.setOnTimeChangeListener(new DownloadService.OnTimeChangeListener() {
               @Override
               public void showTime(final String time) {
                   runOnUiThread(new Runnable() {
                       @Override
                       public void run() {
                           mShowTimeTv.setText(time);
                       }
                   });
               }
           });
       }

       @Override
       public void onServiceDisconnected(ComponentName name) {
       }
   };

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

       Intent intent = new Intent(this, DownloadService.class);
       startService(intent);
       bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
       //双守护线程,优先级不一样
       startAllServices();
   }

   @Override
   public void onContentChanged() {
       super.onContentChanged();
       mShowTimeTv = findViewById(R.id.tv_show_time);
   }

   @Override
   protected void onDestroy() {
       super.onDestroy();
       unbindService(mServiceConnection);
   }

   /**
    * 开启所有守护Service
    */
   private void startAllServices() {
       startService(new Intent(this, StepService.class));
       startService(new Intent(this, GuardService.class));
   }
}

5.2黑科技代码实现2:监听到锁屏广播后使用“1”像素Activity提升优先级


“1”像素保活这么流氓的手段,传说是某IM大厂用过的方案 ...

本方法的具体代码实现主要由以下6步组成。

1)该Activity的View只要设置为1像素然后设置在Window对象上即可。在Activity的onDestroy周期中进行保活服务的存活判断从而唤醒服务。”1像素”Activity如下:
public class SinglePixelActivity extends AppCompatActivity {
   private static final String TAG = SinglePixelActivity.class.getSimpleName();

   @Override
   protected void onCreate(@Nullable Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       Window mWindow = getWindow();
       mWindow.setGravity(Gravity.LEFT | Gravity.TOP);
       WindowManager.LayoutParams attrParams = mWindow.getAttributes();
       attrParams.x = 0;
       attrParams.y = 0;
       attrParams.height = 1;
       attrParams.width = 1;
       mWindow.setAttributes(attrParams);
       ScreenManager.getInstance(this).setSingleActivity(this);
   }

   @Override
   protected void onDestroy() {
       if (!SystemUtils.isAppAlive(this, Constant.PACKAGE_NAME)) {
           Intent intentAlive = new Intent(this, DownloadService.class);
           startService(intentAlive);
       }
       super.onDestroy();
   }
}

2)对广播进行监听,封装为一个ScreenReceiverUtil类,进行锁屏解锁的广播动态注册监听:
public class ScreenReceiverUtil {
   private Context mContext;
   private SreenBroadcastReceiver mScreenReceiver;
   private SreenStateListener mStateReceiverListener;

   public ScreenReceiverUtil(Context mContext) {
       this.mContext = mContext;
   }

   public void setScreenReceiverListener(SreenStateListener mStateReceiverListener) {
       this.mStateReceiverListener = mStateReceiverListener;
       // 动态启动广播接收器
       this.mScreenReceiver = new SreenBroadcastReceiver();
       IntentFilter filter = new IntentFilter();
       filter.addAction(Intent.ACTION_SCREEN_ON);
       filter.addAction(Intent.ACTION_SCREEN_OFF);
       filter.addAction(Intent.ACTION_USER_PRESENT);
       mContext.registerReceiver(mScreenReceiver, filter);
   }

   public void stopScreenReceiverListener() {
       mContext.unregisterReceiver(mScreenReceiver);
   }

   /**
    * 监听sreen状态对外回调接口
    */
   public interface SreenStateListener {
       void onSreenOn();

       void onSreenOff();

       void onUserPresent();
   }

   public class SreenBroadcastReceiver extends BroadcastReceiver {
       @Override
       public void onReceive(Context context, Intent intent) {
           String action = intent.getAction();
           if (mStateReceiverListener == null) {
               return;
           }
           if (Intent.ACTION_SCREEN_ON.equals(action)) { // 开屏
               mStateReceiverListener.onSreenOn();
           } else if (Intent.ACTION_SCREEN_OFF.equals(action)) { // 锁屏
               mStateReceiverListener.onSreenOff();
           } else if (Intent.ACTION_USER_PRESENT.equals(action)) { // 解锁
               mStateReceiverListener.onUserPresent();
           }
       }
   }
}

3)对1像素Activity进行防止内存泄露的处理,新建一个ScreenManager类:
public class ScreenManager {
   private static final String TAG = ScreenManager.class.getSimpleName();
   private static ScreenManager sInstance;
   private Context mContext;
   private WeakReference mActivity;

   private ScreenManager(Context mContext) {
       this.mContext = mContext;
   }

   public static ScreenManager getInstance(Context context) {
       if (sInstance == null) {
           sInstance = new ScreenManager(context);
       }
       return sInstance;
   }

   /** 获得SinglePixelActivity的引用
    * @param activity
    */
   public void setSingleActivity(Activity activity) {
       mActivity = new WeakReference<>(activity);
   }

   /**
    * 启动SinglePixelActivity
    */
   public void startActivity() {
       Intent intent = new Intent(mContext, SinglePixelActivity.class);
       intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
       mContext.startActivity(intent);
   }

   /**
    * 结束SinglePixelActivity
    */
   public void finishActivity() {
       if (mActivity != null) {
           Activity activity = mActivity.get();
           if (activity != null) {
               activity.finish();
           }
       }
   }
}

4)对1像素的Style进行特殊处理,在style文件中新建一个SingleActivityStyle: