1. 为什么TCP连接需要心跳?

因为运营商有一个NAT超时:因为IP v4的IP量有限,运营商分配给手机终端的IP是运营商内网的IP,手机要连接Internet,就需要通过运营商的网关做一个网络地址转换(Network Address Translation,NAT)。简单的说运营商的网关需要维护一个外网IP、端口到内网IP、端口的对应关系,以确保内网的手机可以跟Internet的服务器通讯,大部分移动无线网络运营商都在链路一段时间没有数据通讯时,会淘汰NAT表中的对应项,造成链路中断。
所以我们需要间隔一定的时间发送一个数据包来保证当前的TCP连接保持有效,这就是所谓的心跳包

2. 什么是智能心跳?

智能心跳实际上就是动态的探测到最大的NAT超时时间,然后选定合适的心跳间隔区间去发送心跳包,同时在网络状况发生变化的时候能够动态的调整心跳间隔时间;如果心跳间隔不合适,例如心跳间隔过短,那么可能导致频繁的唤醒手机发送心跳包,增加耗电,心跳间隔过长,可能导致这条TCP连接已经无效但是无法及时的检测到,只能等待下一个心跳包发送的时候才能感知到,所以会导致消息接收延迟,所以探测到一个合适的心跳间隔是非常重要的,把耗电和消息接收及时性综合折中来取得一个最佳的体验

3. 我们的二分法智能心跳策略

心跳变量

探测心跳:程序采用不确定的时间间隔去发送心跳,目的是为了得到最大NAT超时时间 稳定心跳:当探测心跳探测到了NAT超时时间那么就会选定比这个时间点稍微小一点的时间来作为稳定心跳,以后就一直以这个稳定时间去发送心跳 minHeart:最小的心跳间隔 maxHeart:最大的心跳间隔 curMinHeart:初始值为minHeart,变换过程中的最小心跳 curMaxHeart:初始值为maxHeart,变换过程中的最大心跳 step:心跳探测步长 maxSuccessCount:稳定心跳成功次数的最大值,用来动态向上探测 maxFailedCount:心跳连续失败次数最大值,用来向下探测 curHeart:当前正在使用的心跳间隔,默认270秒,这个值可以根据不同地区的心跳区间大数据采集统计然后再设置 timeout:心跳超时时间,我们当前设置为20秒,这个其实可以调整的更小,5秒~10秒,之所以设置为20秒是考虑到网络很不好的情况下可能心跳返回的比较慢,所以间隔设的大一些 heartbeatStabledSuccessCount:稳定心跳连续成功的次数 heartbeatFailedCount:心跳连续失败的次数 networkTag:网络环境标识,对于数据网络来说分为电信,联通,移动;对于wifi来说是用wifi的名称来区分的,因为每个运营商的网络环境都可能有不同的NAT超时,所以在网络环境变换的时候要重新调整心跳

流程图

上调curHeart(心跳成功的时候)

把当前的成功心跳区间保存到列表中 curMinHeart = heartbeat.curHeart; 如果当前心跳是稳定心跳,heartbeatStabledFailedCount = 0;heartbeatStabledSuccessCount++;如果当前心跳不是稳定心跳,curHeart = (curMinHeart + curMaxHeart) / 2,然后直接执行第6步 判断heartbeatStabledSuccessCount是否大于maxSuccessCount,如果大于的话就上调maxSuccessCount的上限,可以乘以2,或者递增固定值,这个可以自己决定,我们是maxSuccessCount默认为20,所以maxSuccessCount = maxSuccessCount + 20; 从成功心跳列表选择比当前稳定心跳更大一级心跳,如果有就把这个作为新的稳定心跳,如果没有:curMaxHeart = maxHeart;curHeart = (curMinHeart + curMaxHeart) / 2;然后再重新以curHeart开始向上探测心跳 判断curMaxHeart - curMinHeart < 10是否满足,如果满足并且当前心跳还不是稳定心跳:curHeart = curMinHeart;把二分法比较小的那个值作为稳定心跳,然后探测结束,进入稳定心跳,这里之所以这么做是因为二分法的一个特点,二分法的一个临界值就是curMaxHeart = curMinHeart,到最后curMinHeart和curMaxHeart很接近的时候其实(curMaxHeart+curMinHeart)== 2*curMinHeart ==2*curMaxHeart,所以会导致二分法计算出来的curHeart和curMinHeart,curMaxHeart相差就几秒,这是没什么意义的,设置一个10秒的区间来让心跳尽快进入稳定状态

下调心跳(心跳失败的时候)

heartbeatStabledSuccessCount=0; curMaxHeart = curHeart; 如果是稳定心跳失败了,heartbeatStabledFailedCount++;并且判断heartbeatStabledFailedCount>maxFailedCount,如果是则从成功心跳列表中选择比当前心跳略小一级的心跳并把这个心跳作为新的稳定心跳,要是不存在略小一级的成功心跳,那么curMinHeart = minHeart;curHeart = (curMinHeart + curMaxHeart) / 2; 如果是探测心跳失败了,curHeart = (curMinHeart + curMaxHeart) / 2; 判断curMaxHeart - curMinHeart < 10,如果满足并且当前心跳不是稳定心跳:curMinHeart = minHeart;

代码实现

public abstract class HeartbeatScheduler {protected int timeout = 20000;protected int minHeart = 60;protected int maxHeart = 300;protected int step = 30;protected volatile boolean started = false;protected volatile long heartbeatSuccessTime;protected volatile int currentHeartType;public static final String HEART_TYPE_TAG = "heart_type";public static final int UNKNOWN_HEART = 0, SHORT_HEART = 1, PROBE_HEART = 2, STABLE_HEART = 3, REDUNDANCY_HEART = 4;protected PendingIntent createPendingIntent(Context context, int requestCode, int heartType) {Intent intent = new Intent();intent.setPackage(context.getPackageName());intent.setAction(SyncAction.HEARTBEAT_REQUEST);intent.putExtra(HEART_TYPE_TAG, heartType);PendingIntent pendingIntent = PendingIntent.getBroadcast(context, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT);return pendingIntent;}protected void set(int minHeart, int maxHeart, int step) {this.minHeart = minHeart;this.maxHeart = maxHeart;this.step = step;SyncLogUtil.i("set minMax:" + minHeart + ",maxHeart:" + maxHeart + ",step:" + step);}protected boolean isStarted() {return started;}protected abstract boolean isStabled();protected void setCurrentHeartType(int currentHeartType) {this.currentHeartType = currentHeartType;SyncLogUtil.i("set current heart type:" + currentHeartType);}protected int getTimeout() {return timeout;}protected void setTimeout(int timeout) {this.timeout = timeout;}protected long getHeartbeatSuccessTime() {return heartbeatSuccessTime;}protected void setHeartbeatSuccessTime(long heartbeatSuccessTime) {this.heartbeatSuccessTime = heartbeatSuccessTime;}protected abstract void start(Context context);protected abstract void stop(Context context);protected abstract void clear(Context context);protected abstract void adjustHeart(Context context, boolean success);protected abstract void startNextHeartbeat(Context context, int heartType);protected abstract void resetScheduledHeart(Context context);protected abstract void receiveHeartbeatFailed(Context context);protected abstract void receiveHeartbeatSuccess(Context context);protected abstract int getCurHeart();
}
public class WatchHearbeatScheduler extends HeartbeatScheduler {private class Heartbeat {AtomicInteger heartbeatStabledSuccessCount = new AtomicInteger(0); // 心跳连续成功次数AtomicInteger heartbeatFailedCount = new AtomicInteger(0); // 心跳连续失败次数int successHeart;int failedHeart;int curHeart = 270;AtomicBoolean stabled = new AtomicBoolean(false);}private int curMaxHeart = maxHeart;private int curMinHeart = minHeart;private int maxFailedCount = 5;private int maxSuccessCount = 20;private volatile String networkTag;private int requestCode = 700;private Map heartbeatMap = new HashMap<>();private List successHeartList = new ArrayList<>();protected WatchHearbeatScheduler() {}@Overrideprotected void start(Context context) {started = true;networkTag = NetUtil.getNetworkTag(context);alarm(context);SyncLogUtil.i("start heartbeat,networkTag:" + networkTag);}@Overrideprotected void stop(Context context) {heartbeatSuccessTime = 0;started = false;networkTag = null;currentHeartType = UNKNOWN_HEART;for (Map.Entry entry : heartbeatMap.entrySet()) {Heartbeat heartbeat = entry.getValue();heartbeat.heartbeatStabledSuccessCount.set(0);heartbeat.heartbeatFailedCount.set(0);}cancel(context);SyncLogUtil.d("stop heartbeat...");}@Overrideprotected void setCurrentHeartType(int currentHeartType) {this.currentHeartType = currentHeartType;}@Overrideprotected void set(int minHeart, int maxHeart, int step) {super.set(minHeart, maxHeart, step);curMaxHeart = maxHeart;curMinHeart = minHeart;}@Overrideprotected boolean isStabled() {Heartbeat heartbeat = getHeartbeat();return heartbeat.stabled.get();}@TargetApi(Build.VERSION_CODES.KITKAT)public void alarm(Context context) {AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);Heartbeat heartbeat = getHeartbeat();boolean stabled = heartbeat.stabled.get();int heart;if (stabled) {heart = heartbeat.curHeart - 10;if (heart < minHeart) {heart = minHeart;}heart = heart * 1000;} else {heart = heartbeat.curHeart * 1000;}int heartType = stabled ? STABLE_HEART : PROBE_HEART;PendingIntent pendingIntent = createPendingIntent(context, requestCode, heartType);int sdk = Build.VERSION.SDK_INT;if (sdk >= Build.VERSION_CODES.KITKAT) {alarmManager.setExact(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + heart, pendingIntent);} else {alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + heart, pendingIntent);}SyncLogUtil.i("start heartbeat,curHeart [" + heartbeat.curHeart + "],heart [" + heart + "],requestCode:" + requestCode + ",stabled:" + stabled);}private void cancel(Context context) {Heartbeat heartbeat = getHeartbeat();int heartType = heartbeat.stabled.get() ? STABLE_HEART : PROBE_HEART;AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);PendingIntent pendingIntent = createPendingIntent(context, requestCode, heartType);alarmManager.cancel(pendingIntent);SyncLogUtil.d("cancel heartbeat,requestCode:" + requestCode);}@Overridepublic void startNextHeartbeat(Context context, int heartType) {alarm(context);}@Overridepublic void resetScheduledHeart(Context context) {alarm(context);}private void addSuccessHeart(Integer successHeart) {if (!successHeartList.contains(successHeart)) {if (successHeartList.size() > 10) {successHeartList.remove(0);}successHeartList.add(successHeart);SyncLogUtil.i("add successHeart:" + successHeart);}SyncLogUtil.i("successHeartList:" + successHeartList);}private void removeSuccessHeart(Integer successHeart) {successHeartList.remove(Integer.valueOf(successHeart));SyncLogUtil.i("successHeartList:" + successHeartList);}@Overrideprotected void adjustHeart(Context context, boolean success) {if (currentHeartType == REDUNDANCY_HEART) {SyncLogUtil.d("redundancy heart,do not adjustHeart...");return;}Heartbeat heartbeat = getHeartbeat();if (success) {onSuccess(heartbeat);} else {onFailed(heartbeat);}SyncLogUtil.i("after success is [" + success +  "] adjusted,heartbeat.curHeart:" + heartbeat.curHeart + ",networkTag:" + networkTag);}private void onSuccess(Heartbeat heartbeat) {heartbeat.successHeart = heartbeat.curHeart;curMinHeart = heartbeat.curHeart;addSuccessHeart(heartbeat.successHeart);heartbeat.heartbeatFailedCount.set(0);if (heartbeat.stabled.get()) {int count = heartbeat.heartbeatStabledSuccessCount.incrementAndGet();SyncLogUtil.i("heartbeatStabledSuccessCount:" + heartbeat.heartbeatStabledSuccessCount.get());if (count >= maxSuccessCount) {maxSuccessCount += 20;SyncLogUtil.i("maxSuccessCount:" + maxSuccessCount);Integer successHeart = selectMinSuccessHeart(heartbeat.curHeart);if (successHeart != null) {heartbeat.curHeart = successHeart;} else {heartbeat.stabled.set(false);curMaxHeart = maxHeart;heartbeat.curHeart = (curMinHeart + curMaxHeart) / 2;SyncLogUtil.i("curHeart = (" + curMinHeart + " + " + curMaxHeart + ") / 2 = " + heartbeat.curHeart);}}} else {heartbeat.curHeart = (curMinHeart + curMaxHeart) / 2;SyncLogUtil.i("curHeart = (" + curMinHeart + " + " + curMaxHeart + ") / 2 = " + heartbeat.curHeart);}if (heartbeat.curHeart >= maxHeart) {heartbeat.curHeart = maxHeart;heartbeat.stabled.set(true);SyncLogUtil.i("探测达到最大心跳adjust stabled:" + heartbeat.stabled.get());} else if (curMaxHeart - curMinHeart < 10) {if (!heartbeat.stabled.get()) {heartbeat.curHeart = curMinHeart;}heartbeat.stabled.set(true);SyncLogUtil.i("二分法探测尽头adjust stabled:" + heartbeat.stabled.get());}SyncLogUtil.i("curHeart:" + heartbeat.curHeart + ",curMinHeart:" + curMinHeart + ",curMaxHeart:" + curMaxHeart);}private void onFailed(Heartbeat heartbeat) {removeSuccessHeart(heartbeat.curHeart);heartbeat.failedHeart = heartbeat.curHeart;heartbeat.heartbeatStabledSuccessCount.set(0);curMaxHeart = heartbeat.curHeart;int count = heartbeat.heartbeatFailedCount.incrementAndGet();SyncLogUtil.i("heartbeatFailedCount:" + count);if (maxSuccessCount > 20) {maxSuccessCount -= 20;}if (heartbeat.stabled.get()) {if (count > maxFailedCount) {Integer successHeart = selectMaxSuccessHeart(heartbeat.curHeart);if (successHeart != null) {heartbeat.curHeart = successHeart;} else {heartbeat.stabled.set(false);curMinHeart = minHeart;heartbeat.curHeart = (curMinHeart + curMaxHeart) / 2;SyncLogUtil.i("curHeart = (" + curMaxHeart + " + " + curMinHeart + ") / 2 = " + heartbeat.curHeart);}} else {SyncLogUtil.i("continue retry heartbeat.curHeart:" + heartbeat.curHeart + ",stabled:" + heartbeat.stabled.get());}} else {if (count > maxFailedCount) {heartbeat.curHeart = (curMinHeart + curMaxHeart) / 2;SyncLogUtil.i("curHeart = (" + curMaxHeart + " + " + curMinHeart + ") / 2 = " + heartbeat.curHeart);} else {SyncLogUtil.i("continue retry heartbeat.curHeart:" + heartbeat.curHeart + ",stabled:" + heartbeat.stabled.get());}}if (curMaxHeart - curMinHeart < 10) {if (!heartbeat.stabled.get()) {curMinHeart = minHeart;}SyncLogUtil.i("二分法探测达到瓶颈" + ",curHeart:" + heartbeat.curHeart);SyncLogUtil.i("curMinHeart:" + curMinHeart + ",curMaxHeart:" + curMaxHeart);}SyncLogUtil.i("curHeart:" + heartbeat.curHeart + ",curMinHeart:" + curMinHeart + ",curMaxHeart:" + curMaxHeart);}private Integer selectMaxSuccessHeart(int curHeart) {Collections.sort(successHeartList, new Comparator() {@Overridepublic int compare(Integer lhs, Integer rhs) {return rhs.compareTo(lhs);}});SyncLogUtil.i("successHeartList:" + successHeartList);for (Integer heart : successHeartList) {if (curHeart >= heart) {continue;} else {return heart;}}return null;}private Integer selectMinSuccessHeart(int curHeart) {Collections.sort(successHeartList, new Comparator() {@Overridepublic int compare(Integer lhs, Integer rhs) {return lhs.compareTo(rhs);}});SyncLogUtil.i("successHeartList:" + successHeartList);for (Integer heart : successHeartList) {if (curHeart >= heart) {continue;} else {return heart;}}return null;}private Heartbeat getHeartbeat() {Heartbeat heartbeat = heartbeatMap.get(networkTag);if (heartbeat == null) {heartbeat = new Heartbeat();heartbeatMap.put(networkTag, heartbeat);}return heartbeat;}@Overrideprotected void receiveHeartbeatFailed(Context context) {adjustHeart(context, false);}@Overrideprotected void receiveHeartbeatSuccess(Context context) {adjustHeart(context, true);alarm(context);}@Overrideprotected void clear(Context context) {stop(context);heartbeatMap.clear();successHeartList.clear();curMinHeart = minHeart;curMaxHeart = maxHeart;SyncLogUtil.d("clear heartbeat...");}@Overrideprotected int getCurHeart() {Heartbeat heartbeat = getHeartbeat();return heartbeat.curHeart;}
}

4. 最终探测效果

以270秒作为curHeart开始探测,minHeart为60秒,maxHeart为300秒,在我们公司的wifi或者数据网络环境下:270,285,292就能够达到稳定心跳,最终稳定心跳会比292小10秒。也就是282秒作为稳定心跳,这里面大概在14分钟之内alarm了三次,如果把maxHeart上调的话探测到稳定心跳的时间会变长,不过平均alarm次数会降低,因为心跳周期在不断变长 当达到稳定心跳后,在稳定心跳成功发送20次后会再次尝试上调心跳,如果由于网络环境不稳定导致当前的心跳可能失败次数超过了5次,那么就会下调心跳,总之做到一个原则,严格控制下调条件,能不下调就尽量不下调

5. 和微信智能心跳的对比

更加省电:微信智能心跳是按照从最小还是逐渐递增的去探测的,所以在网络环境不好的条件下前期可能一直探测不上来,心跳周期一直维持在一个较小的范围,导致频繁的alarm,耗电,微信智能心跳探测过程:60秒短心跳,连续发3次后开始探测,90,120,150,180,210,240,270,这个过程中一共耗费24分钟,alarm了10次,在前14分钟之内alarm了8次,而二分法智能心跳前14分钟才唤醒3次 网络环境差的情况下不会频繁的唤醒:当网络环境很不好的情况下,心跳可能会经常失败,微信智能心跳由于是从下往上上调心跳,可能一直维持在一个间隔周期较小的心跳,会频繁alarm,二分法是从上往下下调心跳,因此心跳周期是逐渐缩小,一开始不会频繁的alarm,比较省电 探测周期短:微信智能心跳是逐渐的通过累加探测步长来上调心跳,上调的趋势比较稳定,但是如果step设置的比较小,那么会导致上调缓慢,探测到稳定心跳所需要的时间比较长(24分钟);二分法智能心跳的心跳调整波动比较大,成功了就上调一半,失败了就下调一半,所以探测到稳定心跳的时间会比较短(14分钟),但是其实这个都是相对的,如果NAT超时时间为2分钟,那么微信智能心跳一下子就能探测到了,而二分法智能心跳要调整好多次,反正是看NAT超时时间距离最初开始探测的curHeat比较接近,所以curHeart可以通过大数据搜集分析,针对各个地区给出不同的curHeart 探测期间不够稳定:微信智能心跳的探测过程很稳定,基本不会导致心跳失败,因为它是从最小的开始探测;二分法智能心跳就不一样了,以为curHeart的调整波动比较大,一开始探测一下子上调或者下调一半很容易就超出NAT超时时间,在探测前期会有比较频繁的失败心跳;当然,这个也是相对的,最终都要取决与curHeart的初始值,minHeart,maxHeart,如果这些值设置的合适,那么二分法智能心跳将会很快的探测到稳定心跳

6. Android机子上存在的问题

alarm的对齐唤醒:国内的手机厂商例如华为,魅族,小米都是自定制的android 系统,对于AlarmManager都有对齐唤醒策略,因此会导致心跳alarm的时间不准确,例如设置了270秒alarm一次,但是在这些手机上可能要推迟到300秒才能唤醒,那么问题来了,如果NAT超时时间是2分钟,而这些手机的alarm最小间隔是5分钟,那就坑了,永远无法探测到最佳心跳,你设置120秒的alarm,手机系统也给你延迟到5分钟才执行alarm,不过这种情况只有在手机休眠的时候才会对齐唤醒,在手机不休眠的时候,我侧过,alarm计时还是准确的

IM推送Android客户端SDK之智能心跳相关推荐

  1. OPPO推送服务客户端SDK接入

    目前SDK版本为V3.0.0,只支持Android 4.4或以上版本的手机系统,如无特殊说明,兼容历史版本.开发者技术支持:QQ群1(1125363958).QQ群2(1125372593). 二.S ...

  2. 谷歌android wear智能腕表 价格,谷歌Android Wear 2.0更新推送:仅三款智能手表可享受...

    经过一段时间的等待之后,谷歌的最新一代可穿戴平台Android Wear 2.0终于正式发布.此后便有多个品牌宣布将推出搭载Android Wear 2.0的新款智能手表.在近日举办的瑞士巴塞尔国际珠 ...

  3. 极光推送android 区分开发测试,给自己的项目做极光推送的步骤

    极光推送(对客户端的app如天猫进行消息推送) 第一种:利用网页进行推送(下面按这种来介绍) 第二种:利用javase代码进行消息推送 使用步骤 1\登陆极光网址 2\注册后并登陆 3.点击用户名下的 ...

  4. 小米杀不死的消息推送-- Android、java后端同时接入小米推送

    作者简介 微信公众号(高质量文章推送):陈博易 前言 小米手机选择小米推送的原因:App进程被杀死的情况下,小米手机上的小米推送服务并不会被杀死(除非用户手动在设置中关闭了该推送的权限),所以我选择给 ...

  5. 极光推送Android集成以及使用

    极光推送Android集成以及使用(本文只集成了通知,如需要自定义消息,富文本等请去查看官方文档)https://docs.jiguang.cn/ 一.概念图 二.搭建环境以及集成 1. 官网注册,并 ...

  6. Android端消息推送总结:实现原理、心跳保活、遇到的问题等

    前言 最近研究Android推送的实现, 研究了两天一夜, 有了一点收获, 写下来既为了分享, 也为了吐槽. 需要说明的是有些东西偏底层硬件和通信行业, 我对这些一窍不通, 只能说说自己的理解. 为什 ...

  7. android o测试版,一加手机可升级!谷歌已正式推送Android O测试版系统

    原标题:一加手机可升级!谷歌已正式推送Android O测试版系统 科客点评:怎么和iOS比起来,安卓新系统没什么人玩呢? 谷歌6月9日公布了第三个开发者预览版,同时也是用户测试版本的Android ...

  8. android p正式版,国内首家!一加正式推送Android P正式版

    近日,相信不少一加6的用户都收到了Android9.0也就是Android P系统的推送,如果没有记错的话,这应该是首个推送Android P稳定版的系统,而此前一加也是首个开放Android P公测 ...

  9. .net平台借助第三方推送服务在推送Android消息(极光推送) 转

    分类: .net外部工具接口(3) .net知识精华(29) 版权声明:本文为博主原创文章,未经博主允许不得转载. 最近做的.net项目(Windows Service)需要向Android手机发送推 ...

最新文章

  1. 【Python-ML】探索式数据分析EDA(Exploratory Data Analysis)
  2. 简单的WINFORM窗体,体验WINFORM带来的快感
  3. 战神笔记本电脑自带access吗_笔记本电脑卡顿不要急着换,这几个方法,让你的电脑流畅爆表...
  4. MTK 驱动(73)--- Kernel Backtrace 无法显示出具体的地址.
  5. Java基础学习总结(55)——java8新特性:stream
  6. java中的关键字transient说明
  7. 计算机算法设计与分析 N后问题
  8. oracle11g创建修改删除表
  9. python走起之第十三话
  10. 林记seo告诉你seo教程菜鸟seo一个月到底能赚多少钱
  11. golang解决数据库中null值的问题
  12. Field not found; typically this occurs with arrays which are not mapped as single value
  13. python画圆形螺旋线_Python写的弹球小游戏
  14. 如何给mac重做系统
  15. WIndows内核学习笔记:分页机制——PAE分页模式
  16. 百度爱番番实时CDP建设实践
  17. linux运维自动化脚本,linux运维自动化shell脚本小工具
  18. 常用Android代码
  19. 使用硬盘从linux服务器上复制文件
  20. mysql接受表单数据类型_PHP如何接收表单数据数组并插入MySQL数据库?其中表单数据类型包括图片文件类型,要实现图片..._慕课问答...

热门文章

  1. 远程互动 gk服务器,不要错过!GKUI APP远程控制教程
  2. 系统平台运营热门店铺模式
  3. 是否需要这么一支“特别行动队”?
  4. 三明梅列:社区服务走进“微时代”
  5. 9*9的数独(dfs)
  6. 数据中台各种架构图大全
  7. 2、通过mos管构成的逻辑门电路
  8. python子列表_关于python:创建子列表
  9. 利用powershell安装360杀毒
  10. 大学生实习就业调研报告之二 - 共性问题与企业技术管理者探讨