默认
打赏 发表评论 19
想开发IM:买成品怕坑?租第3方怕贵?找开源自已撸?尽量别走弯路了... 找站长给点建议
应用保活终极总结(一):Android6.0以下的双进程守护保活实践
阅读(189004) | 评论(19 收藏14 淘帖1 2
微信扫一扫关注!

原作者:“裂缝中的阳光dg”,本文由即时通讯网重新修订发布,感谢原作者的无私分享。


1、前言


最新推荐:Android保活从入门到放弃:乖乖引导用户加白名单吧(附7大机型加白示例)》(此文发布于2020年06月13日)。

在Android 4.4及以后的系统中,应用能否常驻内存,一直以来都是相当头疼的事情,尤其移动端IM、消息推送这类应用,为了保证“全时在线”的概念,真是费尽了心思。

虽然APP常驻内存对于用户来说比较”恶心”,但是在诸如IM和消息推送这类场景来说,APP的常驻内存却尤其重要,而且很多时候用户也会要求APP能够保证长久运行。

因为Android机型太多太杂,以及各厂商定制ROOM的差异,Android应用保活没有一劳永逸和万能的方法,本文探讨的是Android应用在Android 6.0以下系统中的典型应用场景下的保活实践Android 6.0及以上系统的防杀和复活方法,详见本系列文章的下两篇《应用保活终极总结(二):Android6.0及以上的保活实践(进程防杀篇)》、《Android应用保活终极总结(三):Android6.0及以上的保活实践(被杀复活篇)》,内容仅供参考,希望给您带来启发

本文中的进程防杀方法适用Android 6.0以下系统(即低于Android 6.0版本的手机)

特别说明:本文中的Demo源码打包完整下载请至文末,直接从附件下载。

2、系列文章


本文是系列文章中的第1篇,本系列文章的大纲如下:


3、参考资料


Android进程保活详解:一篇文章解决你的所有疑问
微信团队原创分享:Android版微信后台保活实战分享(进程保活篇)
Android P正式版即将到来:后台应用保活、消息推送的真正噩梦
全面盘点当前Android后台保活方案的真实运行效果(截止2019年前)
融云技术分享:融云安卓端IM产品的网络链路保活技术实践
2020年了,Android后台保活还有戏吗?看我如何优雅的实现!
史上最强Android保活思路:深入剖析腾讯TIM的进程永生技术
Android进程永生技术终极揭密:进程被杀底层原理、APP对抗被杀技巧》(* 推荐
>> 更多同类文章 ……

4、常见的Android应用保活方法


1)监听广播方式:
通过监听一些全局的静态广播,比如开机广播、解锁屏广播、网络状态广播等,来启动应用的后台服务。目前,在高版本的Android系统中已经失效,因为高版本的Android系统规定应用必须在系统开机后运行一次才能监听这些系统广播,一般而言,应用被系统杀死后,基本无法接收系统广播。

2)提高Service的优先级:
以前提高Service优先级方法很多,比如onStartCommand返回START_STICKY使系统内存足够的时候Service能够自动启动、弹出通知、配置service的优先级等,这些方式只能在一定程度上缓解service被立马回收,但只要用户一键清理或者系统回收照样无效。

3)全局定时器:
还有一种方法就是在设置一种全局定时器,定时检测启动后台服务,但这种方法目前也已经无效,因为应用只要被系统杀死,全局定时器最后也只成了摆设。

4)应用中的双service拉起:
经过测试,只要当前应用被杀,任何后台service都无法运行,也无法自行启动。

5)应用中的双进程拉起:
这种方式就是传说中的使用NDK在底层fork出一个子进程,来实现与父进程之间的互拉。在Android4.x还是非常有效的,但是高版本的Android系统的系统回收策略已经改成进程组的形式了,如果系统要回收一个应用,必然会杀死同属于一个进程组的所有进程,因此最后导致双进程无法拉起。

5、剖析Android的Service


Service的特征类似于Activity,其区别是它没有交互界面,且可长时间运行在后台,即使它所属的应用已经退出,Service仍然可以继续在后台运行。Service无法自行启动,访问者启动它的方式分为两种,即startService(绑定式)和bindService (非绑定式),相关介绍如下。

startService:
即非绑定式。访问者使用这种方式启动service后,被启动的service将不受访问者控制,也无法与访问者进行数据通信,它会无限地运行下去,必须调用stopSelf()方法或者其他组件(包括访问者)调用stopService()方法来停止。它的生命周期:onCreate->onStartCommand()->……>onDestory(),其中,onCreate用于初始化工作,多次调用startService启动同一个服务,onCreate方法只会被调用一次,onStartCommand会被调用多次,onDestory在销毁时也只会被调用一次。

bindService:
即绑定式。访问者使用这种方式启动service后,被启动的service受访问者的控制,访问者将通过一个IBinder接口的对象与被绑定的service进行通信,并且可以通过unbindService()方法随时关闭service。一个service可以同时被多个访问者绑定,只有当多个访问者都主动解除绑定关系之后,系统才会销毁service。它的生命周期:onCreate->onBind->....>onUnbind->onDestory,其中,onBind用于返回一个通信对象(IBinder)给访问者,访问者可以通过该IBinder对象调用service的相关方法。当多个访问者绑定同一个service时,onCreate只会被调用一次,onBind、unOnbind方法会被调用与访问者数目相关的次数,onDestory在销毁时只会被调用一次。

应用保活终极总结(一):Android6.0以下的双进程守护保活实践_1.png

有一点需要注意的是:如果一个service被startService启动,然后其他组件再执行bindService绑定该service,service的onCreate不会再被回调,而是直接回调onBind方法;当该组件unBindService时,service的onUnbind方法被回调,但是service不会被销毁,直到自己调用stopSelf方法或者其他组件调用stopService方法时才会被销毁。

6、理解AIDL和远程Service调用


接上一节。bindService绑定方式启动service分为两种:本地service和远程service

本地service是指绑定启动的service实现在它所属的应用进程中,其他组件访问者与本地service之间的通信是通过IBinder接口对象实现的;远程service是指绑定启动的service实现在其他应用进程中,也就是另一个APP中,他们之间的通信则是通过IBinder接口的代理对象实现的,而这个代理对象必须通过AIDL方式来构造。

AIDL(Android Interface DefinitionLanguage,即接口描述语言)使用用于约束两个进程之间通讯的规则,可以实现Android终端上两个不同应用(进程)之间的通信(IPC)。

AIDL实现的步骤非常简单,阐述如下:

  • 1)在A应用创建IPerson.aidl接口文件(注意AIDL语言的编写规则,这里不详解);
  • 2)将创建的IPerson.aidl接口文件拷贝到B应用中。

packagecom.person.aidl  
interfaceIPerson{  
       String getName();  
       int getAge();  

然后:我们只需要保存.aidl文件,编译器就会自动生成所需的IPerson.java文件,保存在gen目录下,该文件包含一个内部类IPerson.Stub,它实际上继承于Binder对象,即充当通信所需的IBinder代理对象。

这里有几点需要注意,将会影响两进程之间是否能够通讯成功:

  • 1)接口名与aidl文件名相同;
  • 2)应用A、应用B中的.aidl文件必须相同,并且它们所属的包名也必须要一样。

7、先来看看单进程守护如何实现保活


我们先分析一下绑定方式启动Service流程。

以某Android应用(以下简称应用X)中的守护Service启动保活助手A的守护Service为例:

  • 1)当应用X中的Service通过bindService绑定保活助手A的Service时,保活助手A会回调onBind方法返回一个IPerson.Stub的代理对象给应用X;
  • 2)当应用XService中的onServiceConnected被回调时,说明应用X绑定保活助手A的Service成功;
  • 3)当解绑时,应用X中的onServiceDisconnected和保活助手A中的onUnbind被调用。

有了前面的基础,接下来就分析如何在不同的应用进程中创建守护service,通过检测彼此的绑定情况来唤醒彼此:

  • 1)当应用X绑定保活助手A时(浅绿色字体),如果保活助手A被系统杀死,应用X的onServiceDisConnected被回调,我们可以在该方法中执行bindService方法再次尝试绑定唤醒保活助手A;
  • 2)当保活助手A绑定应用X时(橙色字体),如果应用X被系统杀死,保活助手A的onServiceDisconnected被回调,我们可以在该方法中执行bindService方法再次尝试绑定唤醒应用X。

至此,经过这种双重绑定守护来到达应用保活的目的。

逻辑图解如下:
应用保活终极总结(一):Android6.0以下的双进程守护保活实践_2.png

Demo源码工程结构:
应用保活终极总结(一):Android6.0以下的双进程守护保活实践_3.png

MainActivity.java:
/** 
 *@decrible  
 * 
 * Create by jiangdongguo on 2016-12-6 上午9:34:10 
 */  
public class MainActivity extends Activity {  
  
    @Override  
    protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setContentView(R.layout.activity_main);  
        //启动保活后台服务  
        Intent intent = new Intent(MainActivity.this,LcbAliveService.class);  
        startService(intent);  
    }  
}  

LcbAliveService.java:
/** 
 *@decrible 应用X保活后台服务,绑定启动保活助手A的服务 
 * 
 * Create by jiangdongguo on 2016-12-6 上午9:41:36 
 */  
public class LcbAliveService extends Service {  
    private final String A_PackageName = "com.alive.coreone";  
    private final String A_ServicePath = "com.alive.coreone.AssistantAService";  
    private ICat mBinderFromA;  
      
    private ServiceConnection conn = new ServiceConnection() {  
          
        @Override  
        public void onServiceDisconnected(ComponentName name) {  
            bindAliveA();  
        }  
          
        @Override  
        public void onServiceConnected(ComponentName name, IBinder service) {  
            mBinderFromA = ICat.Stub.asInterface(service);  
            if (mBinderFromA != null) {  
                try {  
                    Log.d("Debug",  
                            "收到保活助手A的数据:name="  
                                    + mBinderFromA.getName() + ";age="  
                                    + mBinderFromA.getAge() + "----");  
                } catch (RemoteException e) {  
                    e.printStackTrace();  
                }  
            }  
        }  
    };  
      
    private ICat.Stub mBinderToA = new ICat.Stub() {      
        @Override  
        public String getName() throws RemoteException {  
            return "我是应用X";  
        }  
          
        @Override  
        public int getAge() throws RemoteException {  
            return 3;  
        }  
    };  
      
    @Override  
    public IBinder onBind(Intent intent) {  
        return mBinderToA;  
    }  
      
    @Override  
    public boolean onUnbind(Intent intent) {  
        return super.onUnbind(intent);  
    }  
  
    @Override  
    public void onCreate() {  
        super.onCreate();  
        if(!isApkInstalled(A_PackageName)){  
            Log.d("Debug","----保活助手A未安装----");  
            stopSelf();  
            return;  
        }  
        bindAliveA();  
    }  
      
    @Override  
    public int onStartCommand(Intent intent, int flags, int startId) {  
        return START_STICKY;  
    }  
      
    @Override  
    public void onDestroy() {  
        super.onDestroy();  
    }  
  
    private void bindAliveA() {  
        Intent serverIntent = new Intent();  
        serverIntent.setClassName(A_PackageName, A_ServicePath);  
        bindService(serverIntent, conn, Context.BIND_AUTO_CREATE);  
    }  
      
    private boolean isApkInstalled(String packageName){  
        PackageManager mPackageManager = getPackageManager();  
        //获得所有已经安装的包信息  
        List<;PackageInfo> infos = mPackageManager.getInstalledPackages(0);  
        for(int i=0;i<infos.size();i++){  
            if(infos.get(i).packageName.equalsIgnoreCase(packageName)){  
                return true;  
            }  
        }  
        return false;  
    }  
}

AssistantAService.java:
/** 
 *@decrible 保活助手A守护后台服务 
 * 
 * Create by jiangdongguo on 2016-11-23 上午10:57:59 
 */  
public class AssistantAService extends Service {  
    private final String Lcb_PackageName = "com.luchibao";  
    private final String Lcb_ServicePath = "com.luchibao.LcbAliveService";  
    private ICat mBinderFromLcb;  
      
    private ServiceConnection conn = new ServiceConnection() {  
  
        @Override  
        public void onServiceDisconnected(ComponentName name) {  
            bindLuChiBao();  
        }  
  
        @Override  
        public void onServiceConnected(ComponentName name, IBinder service) {  
            mBinderFromLcb = ICat.Stub.asInterface(service);  
            if (mBinderFromLcb != null) {  
                try {  
                    Log.d("Debug",  
                            "收到应用X Service返回的数据:name="  
                                    + mBinderFromLcb.getName() + ";age="  
                                    + mBinderFromLcb.getAge() );  
                } catch (RemoteException e) {  
                    e.printStackTrace();  
                }  
            }  
        }  
    };  
      
    private ICat.Stub mBinderToLcb = new ICat.Stub() {    
        @Override  
        public String getName() throws RemoteException {  
            return "我是保活助手A";  
        }  
          
        @Override  
        public int getAge() throws RemoteException {  
            return 2;  
        }  
    };  
      
    @Override  
    public IBinder onBind(Intent intent) {  
        return mBinderToLcb;  
    }  
      
    @Override  
    public boolean onUnbind(Intent intent) {  
        return super.onUnbind(intent);  
    }  
      
    @Override  
    public void onCreate() {  
        super.onCreate();  
        //提升Service的优先级  
        Notification notification = new Notification();  
        notification.flags = Notification.FLAG_ONGOING_EVENT;  
        notification.flags |= Notification.FLAG_NO_CLEAR;  
        notification.flags |= Notification.FLAG_FOREGROUND_SERVICE;  
        startForeground(1, notification);  
          
        Log.d("Debug","****保活助手1onCreate:绑定启动应用X****");  
        bindLuChiBao();  
    }  
      
    @Override  
    public int onStartCommand(Intent intent, int flags, int startId) {  
        return START_STICKY;  
    }  
      
    @Override  
    public void onDestroy() {  
        super.onDestroy();  
    }  
      
    private void bindLuChiBao() {  
        Intent clientIntent = new Intent();  
        clientIntent.setClassName(Lcb_PackageName, Lcb_ServicePath);  
        bindService(clientIntent, conn, Context.BIND_AUTO_CREATE);  
    }  
}  

到这里也许你会问:为什么只提高保活助手A的service优先级,而不同时提高应用X的优先级?

这是因为自己在测试的过程中发现:当上述两个进程长时间运行在后台时还是有可能被系统杀死,以致无法实现保活的目的。当时猜想,系统在回收进程时很可能是按顺序回收的,当这两个进程顺序比较接近,或者说内存中可能就只有这两个进程,那么系统在回收的时候一次性将其干掉了。为了缓解这种情况,我采取了一种高低优先级的方式来尽量保证系统不会同一时间回收两个进程,只要有了这个时间差,两个进程就能够实现互相启动保活的目的。

单进程守护保活演示如下,测试手机为华为荣耀4X全网通(Android6.0),其一键清理、强制停止效果分别如下:
应用保活终极总结(一):Android6.0以下的双进程守护保活实践_4.gif        应用保活终极总结(一):Android6.0以下的双进程守护保活实践_5.gif

8、新型双进程守护的保活实践


从程度上来说,单进程守护方式差不多可以满足应用长时间保活的要求,虽说让人感觉有点怪异,但实现起来也不是很难,效果明显。

但是,随着进一步的测试,我连续两次强杀应用X,应用X就无法启动了,这是因为当应用X“体积”较大,启动前需要加载诸如大量的静态变量或者Application类中的变量等,导致启动较慢,当我第一次强杀应用X时,保活助手A是执行绑定启动应用X保活服务的,但我继续第二次强杀应用X时,保活助手A可能还未与应用X绑定,最终导致保活助手A无法检查应用X的绑定状态而失效。

双进程守护:针对单进程守护出现的问题,当应用X“体积”较大时,我们可以采用双进程守护,即实现两个保活助手,它们彼此双向绑定来对应用X进行守护。我们采用“环”的形式来进行互拉,无论谁被杀死,只要系统杀掉剩余的任何一个进程,最后活着的进程都能够将其他被杀进程拉起来。当然,这里还有个小技巧,为了防止两个保活助手进程同时被系统杀死,我这里采取高低优先级的方式来解决。

逻辑图解如下:
应用保活终极总结(一):Android6.0以下的双进程守护保活实践_6.png

MainActivity.java:
代码同上一节,本节不再重复贴出。

LcbAliveService.java:
代码同上一节,本节不再重复贴出。

AssistantAService.java:
/** 
 *@decrible 保活助手A守护后台服务,绑定保活助手B 
 * 
 * Create by jiangdongguo on 2016-11-23 上午10:57:59 
 */  
public class AssistantAService extends Service {  
    private final String B_PackageName = "com.alive.coretwo";  
    private final String B_ServicePath = "com.alive.coretwo.AssistantBService";  
    private ICat mBinderFromB;  
      
    private ServiceConnection conn = new ServiceConnection() {  
  
        @Override  
        public void onServiceDisconnected(ComponentName name) {  
            bindAliveB();  
        }  
  
        @Override  
        public void onServiceConnected(ComponentName name, IBinder service) {  
            mBinderFromB = ICat.Stub.asInterface(service);  
            if (mBinderFromB != null) {  
                try {  
                    Log.d("Debug",  
                            "收到保活助手B Service返回的数据:name="  
                                    + mBinderFromB.getName() + ";age="  
                                    + mBinderFromB.getAge() );  
                } catch (RemoteException e) {  
                    e.printStackTrace();  
                }  
            }  
        }  
    };  
      
    private ICat.Stub mBinderToB = new ICat.Stub() {      
        @Override  
        public String getName() throws RemoteException {  
            return "我是保活助手A";  
        }  
          
        @Override  
        public int getAge() throws RemoteException {  
            return 2;  
        }  
    };  
      
    @Override  
    public IBinder onBind(Intent intent) {  
        return mBinderToB;  
    }  
      
    @Override  
    public boolean onUnbind(Intent intent) {  
        return super.onUnbind(intent);  
    }  
      
    @Override  
    public void onCreate() {  
        super.onCreate();  
        //提升Service的优先级  
        Notification notification = new Notification();  
        notification.flags = Notification.FLAG_ONGOING_EVENT;  
        notification.flags |= Notification.FLAG_NO_CLEAR;  
        notification.flags |= Notification.FLAG_FOREGROUND_SERVICE;  
        startForeground(1, notification);  
          
        Log.d("Debug","****保活助手AonCreate:绑定启动保活助手B****");  
        bindAliveB();  
    }  
      
    @Override  
    public int onStartCommand(Intent intent, int flags, int startId) {  
        return START_STICKY;  
    }  
      
    @Override  
    public void onDestroy() {  
        super.onDestroy();  
    }  
    private void bindAliveB() {  
        Intent clientIntent = new Intent();  
        clientIntent.setClassName(B_PackageName,B_ServicePath);  
        bindService(clientIntent, conn, Context.BIND_AUTO_CREATE);  
    }  
}

AssistantBService.java:
/** 
 *@decrible 保活助手APK后台服务 
 * 
 * Create by jiangdongguo on 2016-11-23 上午10:57:59 
 */  
public class AssistantBService extends Service {  
    private final String Lcb_PackageName = "com.luchibao";  
    private final String Lcb_ServicePath = "com.luchibao.LcbAliveService";  
    private final String A_PackageName = "com.alive.coreone";  
    private final String A_ServicePath = "com.alive.coreone.AssistantAService";  
      
    private ICat mBinderFomAOrLcb;  
      
    private ServiceConnection conn = new ServiceConnection() {  
          
        @Override  
        public void onServiceDisconnected(ComponentName name) {  
            bindAliveA();  
            bindLuChiBao();  
        }  
          
        @Override  
        public void onServiceConnected(ComponentName name, IBinder service) {  
            mBinderFomAOrLcb = ICat.Stub.asInterface(service);  
        }  
    };  
      
      
    private ICat.Stub mBinderToA = new ICat.Stub() {      
        @Override  
        public String getName() throws RemoteException {  
            return "我是保活助手B";  
        }  
          
        @Override  
        public int getAge() throws RemoteException {  
            return 1;  
        }  
    };  
      
    @Override  
    public IBinder onBind(Intent intent) {  
        return mBinderToA;  
    }  
      
    @Override  
    public boolean onUnbind(Intent intent) {  
        return super.onUnbind(intent);  
    }  
      
    @Override  
    public void onCreate() {  
        super.onCreate();  
        bindAliveA();  
        bindLuChiBao();  
    }  
      
    @Override  
    public int onStartCommand(Intent intent, int flags, int startId) {  
        return START_STICKY;  
    }  
      
    @Override  
    public void onDestroy() {  
        super.onDestroy();  
    }  
      
    private void bindLuChiBao() {  
        Intent clientIntent = new Intent();  
        clientIntent.setClassName(Lcb_PackageName,Lcb_ServicePath);  
        bindService(clientIntent, conn, Context.BIND_AUTO_CREATE);  
    }  
      
    private void bindAliveA() {  
        Intent clientIntent = new Intent();  
        clientIntent.setClassName(A_PackageName,A_ServicePath);  
        bindService(clientIntent, conn, Context.BIND_AUTO_CREATE);  
    }  
}

最后:就是每个应用的AndroidManifest.xml配置,也许你会发现当部署在部分机型时,无法启动保活助手,并且弹出一个“关联启动”权限警告,这是由于部分机型在启动一个外部应用时需要对其进行授权。为了避免这个麻烦,我们可以在AndroidManifest.xml的<manifest ../>标签内添加android:sharedUserId,只要保证每个关联启动的android:sharedUserId的值是一致的,然后都使用同一个密钥进行签名即可无需“关联启动”权限

保活助手A的AndroidManifest.xml参考代码如下:
<?xml version="1.0" encoding="utf-8"?>  
<manifest xmlns:android="http://schemas.android.com/apk/res/android"  
    package="com.alive.coreone"  
    android:sharedUserId="com.alive.app"  
    android:versionCode="1"  
    android:versionName="1.0" >  
  
    <uses-sdk  
        android:minSdkVersion="8"  
        android:targetSdkVersion="21" />  
  
    <application  
        android:allowBackup="true"  
        android:icon="@drawable/ic_logo_new_72"  
        android:label="@string/app_name" >  
        <service  
            android:name="com.alive.coreone.AssistantAService"  
            android:enabled="true"  
            android:exported="true"  
            android:process=":remote" >  
            <intent-filter android:priority="1000" >  
            </intent-filter>  
        </service>  
    </application>  
  
</manifest>

Demo运行效果:
应用保活终极总结(一):Android6.0以下的双进程守护保活实践_7.gif

9、本文小结


虽说上述的这种多进程守护的应用保活方式比较有效,实现起来也比较简单,面对一些特殊的应用场合还是不错的。但是,它的局限性也非常明显,我们不可能面对广大用户使用一款应用需要安装两个或者三个APP,有点不现实,但仅供参考!

10、源码下载


Android6.0以下系统-单进程守护Demo源码(52im.net).zip (4.03 MB , 下载次数: 70 , 售价: 3 金币)
Android6.0以下系统-双进程守护Demo源码(52im.net).zip (6.28 MB , 下载次数: 137 , 售价: 3 金币)

11、下篇预告


Android系统在最近几个版本的演化中,大力打击这种保活“黑科技”,因而新系统中的保活方法又有了变化。本系列文章的下2篇将着重分享Android6.0及以上系统防杀和复活方法,敬请期待!

(原文链接:点此进入,有改动)

即时通讯网 - 即时通讯开发者社区! 来源: - 即时通讯开发者社区!

标签:进程保活

评分

2

查看评分

上一篇:IM全文检索技术专题(一):微信移动端的全文检索优化之路下一篇:应用保活终极总结(二):Android6.0及以上的保活实践(进程防杀篇)

本帖已收录至以下技术专辑

推荐方案
评论 19
非常值得尝试,666
谢谢分享
签名:
引用:Lbk 发表于 2017-10-25 16:03
看你一下实现;感觉实现的有问题;这个保活在5.0一下是否可用,没测试过;谁会在一个项目中要求安装3个APP ...

主要提供的是一种思路,不一定能套用。但不管怎么说,Andrid的保活是越来越困难了
很有用
签名: 签个到都要吐槽的吗
神秘人  发表于 7 年前
学习了
签名: 努力......
引用:裂缝中的阳光╄→守候一生 发表于 2017-10-30 16:42
为了尊重作者的劳动成果,请标明你的转载出处:
双进程:http://blog.csdn.net/andrexpert/article/detail ...

在文章一开始的地方已经致谢了
引用:liu1348789134 发表于 2017-11-22 14:59
在文章一开始的地方已经致谢了

这光屁股头像是你自已的吗。。。
感谢分享
签名: 记录
看着挺好的,果然还是最后一句话精辟,可以看看,然而不能好好用  哈哈
不错
啦啦啦啦啦啦啦啦啦啦啦
签名: zz
学习了
看了三天的保活,这绝对是最详细最全的,给力。
引用:猛虎下山 发表于 2018-11-20 16:43
看了三天的保活,这绝对是最详细最全的,给力。

现在最新版本已经是android 9了,自已搞保活越来越难了:http://www.52im.net/thread-1832-1-1.html
学习下保活
学习了
学习了,感谢分享
打赏楼主 ×
使用微信打赏! 使用支付宝打赏!

返回顶部