目录

  • 一、获取基站信息的两个关键方法
    • getAllCellInfo调用流程总结
    • requestCellInfoUpdate 流程总结
    • 问题
  • 二、双卡手机适配 Android Q requestCellInfoUpdate接口
  • 三、getAllCellInfo方法源码流程
    • 应用层
    • 系统层
  • 四、requestCellInfoUpdate源码流程分析
    • 应用层:
    • 系统层

一、获取基站信息的两个关键方法

// 获取所有的基站信息列表
// 在targetSDK < Q时,主动请求基站信息刷新并返回,
// >Q 需要调用下面的方法请求刷新
public List<CellInfo> getAllCellInfo() // Android Q 新增,请求基站刷新
public void requestCellInfoUpdate(@NonNull @CallbackExecutor Executor executor, @NonNull CellInfoCallback callback)

getAllCellInfo调用流程总结

1.通过TelephoneyManager.getAllCellInfo方法获取所有基站信息。

2.该方法会调用到TelephonyManager在系统层的对应接口类PhoneInterfaceManager的getAllCellInfo。

3.PhoneInterfaceManager.getAllCellInfo中首先判断是否有位置权限,如果没有权限则根据情况抛出异常或者返回空。

4.PhoneInterfaceManager.getAllCellInfo继续判断如果当前targetSdk > ,循环Phone列表,调用每一个Phone.getAllCellInfo(),最终调用ServiceStateTracker的成员变量mLastCellInfoList,返回缓存基站信息。否则,循环当前的Phone列表,分别调用Phone.requestCellInfoUpdate方法

5.Phone.requestCellInfoUpdate方法最终的响应类为ServiceStateTracker.requestAllCellInfo

6.ServiceStateTracker.requestAllCellInfo中首先判断当前刷新时间的间隔,如果刷新时间间隔小于一定阈值,则直接返回缓存结果,不再刷新。刷新时间间隔为(亮屏 & (未连接wifi || 充电中) = 2000ms,其它10000ms)。

7.如果符合间隔,则调用和硬件对接的接口,刷新基站信息,同时通过handler等待结果回调,并设置超时时间2000ms。

8.如果超时或者刷新接口回调刷新结果,则回调基站信息,如果超时或者刷新异常,则返回空基站信息。

9.上4中,最终所有的Phone刷新结束,组装所有Phone的基站信息结果并返回。

具体源码分析见下第三部分

requestCellInfoUpdate 流程总结

1.通过TelephoneyManager.requestCellInfoUpdate方法请求刷新基站信息并等待回调结果,TelephoneyManager实现中,调用系统层的requestCellInfoUpdate会传入当前TelephonyManager对应的subId,可以理解为对应的手机卡槽中的某一张卡。

2.该方法会调用到TelephonyManager在系统层的对应接口类PhoneInterfaceManager的requestCellInfoUpdate,并最终调用到requestCellInfoUpdateInternal,该方法首先判断是否有位置权限,如果没有权限则根据情况抛出异常或者返回空。其次,通过subId获取到Phone对象,并调用Phone.requestCellInfoUpdate。

3.其余步骤同上getAllCellInfo中5~7。

4.最终根据刷新基站信息结果,如果异常,则回调onError,成功回调onCellInfo。

具体源码分析见下第四部分

问题

根据如上分析,当targetSdk > Q时,系统不会再主动刷新基站信息,需要我们调用requestCellInfoUpdate主动刷新。

但根据源码分析,requestCellInfoUpdate刷新时和 getAllCellInfo & targetSdk < Q时,刷新逻辑存在差异。requestCellInfoUpdate刷新时,因传入了subId,只会刷新subId对应的Phone对象,而getAllCellInfo刷新时,会调用所有Phone的刷新基站信息接口。

按照该源码表现,requestCellInfoUpdate仅刷新了一个卡的基站信息,对于双卡手机,差异如下:

1)requestCellInfoUpdate回调的onCellInfo方法中,基站列表信息少于getAllCellInfo所获取基站信息。

2)requestCellInfoUpdate刷新时,仅刷新了单卡的基站信息,如果调用requestCellInfoUpdate刷新基站后,立即通过getAllCellInfo获取,会发现部分基站信息的刷新为缓存时间。

二、双卡手机适配 Android Q requestCellInfoUpdate接口

根据以上分析,核心问题是如何找到第二张卡以及对应的TelephonyManager对象,具体逻辑如下:

SubscriptionManager subscriptionManager = (SubscriptionManager) mContext.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE);
// 0 对应卡槽0 所对应的系统中的subId,1 对应卡槽1
int[] subIds0 = subscriptionManager.getSubscriptionIds(0);
int[] subIds1 = subscriptionManager.getSubscriptionIds(1);
// 判断是否有效,如果有效,取返回的数组中的第一位,为subId
int subId0 = subIds0 != null && subIds0.length > 0 ? subIds0[0] : SubscriptionManager.INVALID_SUBSCRIPTION_ID;
int subId1 = subIds1 != null && subIds1.length > 0 ? subIds1[0] : SubscriptionManager.INVALID_SUBSCRIPTION_ID;// 获取系统的TelphoneManager对象 系统的telehonyM
TelephonyManager telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);// 卡槽0 对应的TelephonyManager
TelephonyManager tm0 = telephonyManager.createForSubscriptionId(subId0);
// 卡槽1 对应的TelephonyManager
TelephonyManager tm1 = telephonyManager.createForSubscriptionId(subId1);
//.... 调用对应的requestCellInfoUpdate即可

根据如上逻辑,即可达到刷新双卡基站信息的逻辑,通过卡槽获取subId,根据subId创建对应的TelephoneManager。

在这里存在一个疑问,通过context获取系统的TelephonyManager和哪个卡槽对应?这个是不一定的,取决于系统的设置。可以通过subscriptionManager.getDefaultSubscriptionId获取当前默认的subId,和卡槽对应的subId比对可知。

三、getAllCellInfo方法源码流程

源码总结:具体细分见下:

应用层

应用层调用getAllCellInfo接口,具体逻辑实现

  public List<CellInfo> getAllCellInfo() {try {// Binder机制,会调用到系统进程ITelephony telephony = getITelephony();if (telephony == null)return null;// 通过跨进程访问系统的Phone相关服务return telephony.getAllCellInfo(getOpPackageName(), getAttributionTag());} catch (RemoteException ex) {} catch (NullPointerException ex) {}return null;}

获取系统的基站信息,需要和系统的进程通信,那么根据跨进程相关的aidl逻辑,系统进程肯定存在一个响应的接口,该接口为packages/services/Telephony/src/com/android/phone/PhoneInterfaceManager.java

系统层

PhoneInterfaceManager.java

    @Overridepublic List<CellInfo> getAllCellInfo(String callingPackage, String callingFeatureId) {mApp.getSystemService(AppOpsManager.class).checkPackage(Binder.getCallingUid(), callingPackage);// 检查权限,是否允许获取基站信息,如果不允许,抛出异常或者返回空的基站信息列表LocationAccessPolicy.LocationPermissionResult locationResult =LocationAccessPolicy.checkLocationPermission(mApp,new LocationAccessPolicy.LocationPermissionQuery.Builder().setCallingPackage(callingPackage).setCallingFeatureId(callingFeatureId).setCallingPid(Binder.getCallingPid()).setCallingUid(Binder.getCallingUid()).setMethod("getAllCellInfo").setMinSdkVersionForCoarse(Build.VERSION_CODES.BASE).setMinSdkVersionForFine(Build.VERSION_CODES.Q).build());switch (locationResult) {case DENIED_HARD:throw new SecurityException("Not allowed to access cell info");case DENIED_SOFT:return new ArrayList<>();}// 判断当前targetSdk版本,如果>=Q,则会获取缓存位置final int targetSdk = TelephonyPermissions.getTargetSdk(mApp, callingPackage);if (targetSdk >= android.os.Build.VERSION_CODES.Q) {// 返回缓存的基站信息,最终返回ServiceStateTracker.java的成员变量mLastCellInfoList;return getCachedCellInfo();}if (DBG_LOC) log("getAllCellInfo: is active user");// 如果当前targetSdk版本 < Q,则刷新基站信息,返回基站信息WorkSource workSource = getWorkSource(Binder.getCallingUid());final long identity = Binder.clearCallingIdentity();try {List<CellInfo> cellInfos = new ArrayList<CellInfo>();for (Phone phone : PhoneFactory.getPhones()) {final List<CellInfo> info = (List<CellInfo>) sendRequest(CMD_GET_ALL_CELL_INFO, null, phone, workSource);if (info != null) cellInfos.addAll(info);}return cellInfos;} finally {Binder.restoreCallingIdentity(identity);}}

检查权限,如果权限不通过,则返回空

    /** Check if location permissions have been granted */public static LocationPermissionResult checkLocationPermission(Context context, LocationPermissionQuery query) {// Always allow the phone process, system server, and network stack to access location.// This avoid breaking legacy code that rely on public-facing APIs to access cell location,// and it doesn't create an info leak risk because the cell location is stored in the phone// process anyway, and the system server already has location access.if (query.callingUid == Process.PHONE_UID || query.callingUid == Process.SYSTEM_UID|| query.callingUid == Process.NETWORK_STACK_UID|| query.callingUid == Process.ROOT_UID) {return LocationPermissionResult.ALLOWED;}// Check the system-wide requirements. If the location main switch is off or// the app's profile isn't in foreground, return a soft denial.if (!checkSystemLocationAccess(context, query.callingUid, query.callingPid)) {return LocationPermissionResult.DENIED_SOFT;}// Do the check for fine, then for coarse.if (query.minSdkVersionForFine < Integer.MAX_VALUE) {LocationPermissionResult resultForFine = checkAppLocationPermissionHelper(context, query, Manifest.permission.ACCESS_FINE_LOCATION);if (resultForFine != null) {return resultForFine;}}if (query.minSdkVersionForCoarse < Integer.MAX_VALUE) {LocationPermissionResult resultForCoarse = checkAppLocationPermissionHelper(context, query, Manifest.permission.ACCESS_COARSE_LOCATION);if (resultForCoarse != null) {return resultForCoarse;}}// At this point, we're out of location checks to do. If the app bypassed all the previous// ones due to the SDK backwards compatibility schemes, allow it access.return LocationPermissionResult.ALLOWED;}

请求基站刷新,并获取基站结果

  private @Nullable Object sendRequest(int command, Object argument, Integer subId, Phone phone,WorkSource workSource, long timeoutInMs) {// timeoutInMs 超时时间判断if (Looper.myLooper() == mMainThreadHandler.getLooper()) {throw new RuntimeException("This method will deadlock if called from the main thread.");}MainThreadRequest request = null;if (subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID && phone != null) {throw new IllegalArgumentException("subId and phone cannot both be specified!");} else if (phone != null) {request = new MainThreadRequest(argument, phone, workSource);} else {request = new MainThreadRequest(argument, subId, workSource);}Message msg = mMainThreadHandler.obtainMessage(command, request);msg.sendToTarget();synchronized (request) {if (timeoutInMs >= 0) {// Wait for at least timeoutInMs before returning null request resultlong now = SystemClock.elapsedRealtime();long deadline = now + timeoutInMs;while (request.result == null && now < deadline) {try {request.wait(deadline - now);} catch (InterruptedException e) {// Do nothing, go back and check if request is completed or timeout} finally {now = SystemClock.elapsedRealtime();}}} else {// Wait for the request to complete// 等待结果返回while (request.result == null) {try {request.wait();} catch (InterruptedException e) {// Do nothing, go back and wait until the request is complete}}}}if (request.result == null) {Log.wtf(LOG_TAG,"sendRequest: Blocking command timed out. Something has gone terribly wrong.");}return request.result;}

通过handler响应事件处理处理

case CMD_GET_ALL_CELL_INFO:request = (MainThreadRequest) msg.obj;onCompleted = obtainMessage(EVENT_GET_ALL_CELL_INFO_DONE, request);// 刷新基站信息request.phone.requestCellInfoUpdate(request.workSource, onCompleted);break;
case EVENT_GET_ALL_CELL_INFO_DONE:ar = (AsyncResult) msg.obj;request = (MainThreadRequest) ar.userObj;// 如果结果返回,则停止线程等待,并返回结果request.result = (ar.exception == null && ar.result != null)? ar.result : new ArrayList<CellInfo>();synchronized (request) {request.notifyAll();}break;

最终响应的类:ServiceStateTracker.java

https://cs.android.com/android/platform/superproject/+/master:frameworks/opt/telephony/src/java/com/android/internal/telephony/ServiceStateTracker.java;l=1271;drc=master?hl=zh-cn

   public void requestAllCellInfo(WorkSource workSource, Message rspMsg) {// .....synchronized (mPendingCellInfoRequests) {// 如果当前正在请求刷新基站信息,则保存回调信息,等待刷新结束后通知if (mIsPendingCellInfoRequest) {if (rspMsg != null) mPendingCellInfoRequests.add(rspMsg);return;}// 判断是否可以刷新,刷新时间间隔需 >  mCellInfoMinIntervalMs// 该间隔时间:亮屏 & (未连接wifi || 充电中) = 2000ms,其它10000msfinal long curTime = SystemClock.elapsedRealtime();if ((curTime - mLastCellInfoReqTime) < mCellInfoMinIntervalMs) {if (rspMsg != null) {// 如果小于这个时间,回调缓存的结果AsyncResult.forMessage(rspMsg, mLastCellInfoList, null);rspMsg.sendToTarget();}return;}// 添加当前回调对象,等待刷新结束后通知if (rspMsg != null) mPendingCellInfoRequests.add(rspMsg);// 最后刷新的时间mLastCellInfoReqTime = curTime;// 刷新中的标记mIsPendingCellInfoRequest = true;// 请求基站刷新,刷新成功后会回调EVENT_GET_CELL_INFO_LISTMessage msg = obtainMessage(EVENT_GET_CELL_INFO_LIST);mCi.getCellInfoList(msg, workSource);// 超时判断,如果超时,则强制返回结果,2000mssendMessageDelayed(obtainMessage(EVENT_GET_CELL_INFO_LIST), CELL_INFO_LIST_QUERY_TIMEOUT);}}

刷新结束后,基站信息回调:

            case EVENT_GET_CELL_INFO_LIST: // fallthroughcase EVENT_UNSOL_CELL_INFO_LIST: {List<CellInfo> cellInfo = null;Throwable ex = null;if (msg.obj != null) {ar = (AsyncResult) msg.obj;if (ar.exception != null) {log("EVENT_GET_CELL_INFO_LIST: error ret null, e=" + ar.exception);// 存在异常,标记异常ex = ar.exception;} else if (ar.result == null) {loge("Invalid CellInfo result");} else {// 保存最新的基站 cellInfo = (List<CellInfo>) ar.result;updateOperatorNameForCellInfo(cellInfo);mLastCellInfoList = cellInfo;mPhone.notifyCellInfo(cellInfo);if (VDBG) {log("CELL_INFO_LIST: size=" + cellInfo.size() + " list=" + cellInfo);}}} else {synchronized (mPendingCellInfoRequests) {// If we receive an empty message, it's probably a timeout; if there is no// pending request, drop it.if (!mIsPendingCellInfoRequest) break;// 判断是否是超时final long curTime = SystemClock.elapsedRealtime();if ((curTime - mLastCellInfoReqTime) <  CELL_INFO_LIST_QUERY_TIMEOUT) {break;}// We've received a legitimate timeout, so something has gone terribly// wrong.loge("Timeout waiting for CellInfo; (everybody panic)!");// 如果是超时,则将lastCellInfo置为空mLastCellInfoList = null;// Since the timeout is applicable, fall through and update all synchronous// callers with the failure.}}synchronized (mPendingCellInfoRequests) {// If we have pending requests, then service them. Note that in case of a// timeout, we send null responses back to the callers.if (mIsPendingCellInfoRequest) {// 将最新的基站信息回调给上层业务mIsPendingCellInfoRequest = false;for (Message m : mPendingCellInfoRequests) {AsyncResult.forMessage(m, cellInfo, ex);m.sendToTarget();}mPendingCellInfoRequests.clear();}}break;}

四、requestCellInfoUpdate源码流程分析

应用层:

应用层调用requestCellInfoUpdate接口,具体逻辑实现

public void requestCellInfoUpdate(@NonNull @CallbackExecutor Executor executor, @NonNull CellInfoCallback callback) {try {ITelephony telephony = getITelephony();if (telephony == null) return;telephony.requestCellInfoUpdate(getSubId(), // 获取当前的subIdnew ICellInfoCallback.Stub() { // 监听回调@Overridepublic void onCellInfo(List<CellInfo> cellInfo) {final long identity = Binder.clearCallingIdentity();try {executor.execute(() -> callback.onCellInfo(cellInfo));} finally {Binder.restoreCallingIdentity(identity);}}@Overridepublic void onError(int errorCode, String exceptionName, String message) {final long identity = Binder.clearCallingIdentity();try {executor.execute(() -> callback.onError(errorCode,createThrowableByClassName(exceptionName, message)));} finally {Binder.restoreCallingIdentity(identity);}}}, getOpPackageName() , getAttributionTag());} catch (RemoteException ex) {}}

系统层

PhoneInterfaceManager.java

    private void requestCellInfoUpdateInternal(int subId, ICellInfoCallback cb,String callingPackage, String callingFeatureId, WorkSource workSource) {mApp.getSystemService(AppOpsManager.class).checkPackage(Binder.getCallingUid(), callingPackage);// 位置权限判断LocationAccessPolicy.LocationPermissionResult locationResult =LocationAccessPolicy.checkLocationPermission(mApp,new LocationAccessPolicy.LocationPermissionQuery.Builder().setCallingPackage(callingPackage).setCallingFeatureId(callingFeatureId).setCallingPid(Binder.getCallingPid()).setCallingUid(Binder.getCallingUid()).setMethod("requestCellInfoUpdate").setMinSdkVersionForCoarse(Build.VERSION_CODES.BASE).setMinSdkVersionForFine(Build.VERSION_CODES.BASE).build());switch (locationResult) {case DENIED_HARD:if (TelephonyPermissions.getTargetSdk(mApp, callingPackage) < Build.VERSION_CODES.Q) {// Safetynet logging for b/154934934EventLog.writeEvent(0x534e4554, "154934934", Binder.getCallingUid());}throw new SecurityException("Not allowed to access cell info");case DENIED_SOFT:if (TelephonyPermissions.getTargetSdk(mApp, callingPackage) < Build.VERSION_CODES.Q) {// Safetynet logging for b/154934934EventLog.writeEvent(0x534e4554, "154934934", Binder.getCallingUid());}try {cb.onCellInfo(new ArrayList<CellInfo>());} catch (RemoteException re) {// Drop without consequences}return;}// 获取subId对应的Phone对象final Phone phone = getPhoneFromSubId(subId);if (phone == null) throw new IllegalArgumentException("Invalid Subscription Id: " + subId);// 发送message,请求基站刷新sendRequestAsync(CMD_REQUEST_CELL_INFO_UPDATE, cb, phone, workSource);}

Handler处理逻辑为

case CMD_REQUEST_CELL_INFO_UPDATE:request = (MainThreadRequest) msg.obj;// 请求基站刷新request.phone.requestCellInfoUpdate(request.workSource,obtainMessage(EVENT_REQUEST_CELL_INFO_UPDATE_DONE, request));break;
case EVENT_REQUEST_CELL_INFO_UPDATE_DONE:// 回调刷新结果ar = (AsyncResult) msg.obj;request = (MainThreadRequest) ar.userObj;ICellInfoCallback cb = (ICellInfoCallback) request.argument;try {if (ar.exception != null) {Log.e(LOG_TAG, "Exception retrieving CellInfo=" + ar.exception);cb.onError(TelephonyManager.CellInfoCallback.ERROR_MODEM_ERROR,ar.exception.getClass().getName(),ar.exception.toString());} else if (ar.result == null) {Log.w(LOG_TAG, "Timeout Waiting for CellInfo!");cb.onError(TelephonyManager.CellInfoCallback.ERROR_TIMEOUT, null, null);} else {// use the result as returnedcb.onCellInfo((List<CellInfo>) ar.result);}} catch (RemoteException re) {Log.w(LOG_TAG, "Discarded CellInfo due to Callback RemoteException");}break;

刷新基站信息逻辑与第三部分getAllCellInfo逻辑一致,不再分析。

Android Q 基站刷新接口源码分析 适配双卡手机基站刷新逻辑相关推荐

  1. Android Q 10.1 KeyMaster源码分析(二) - 各家方案的实现

    写在之前 这两篇文章是我2021年3月初看KeyMaster的笔记,本来打算等分析完KeyMaster和KeyStore以后再一起做成一系列贴出来,后来KeyStore的分析中断了,这一系列的文章就变 ...

  2. 【Android 事件分发】ItemTouchHelper 源码分析 ( OnItemTouchListener 事件监听器源码分析 )

    Android 事件分发 系列文章目录 [Android 事件分发]事件分发源码分析 ( 驱动层通过中断传递事件 | WindowManagerService 向 View 层传递事件 ) [Andr ...

  3. 【Android 事件分发】ItemTouchHelper 源码分析 ( OnItemTouchListener 事件监听器源码分析 二 )

    Android 事件分发 系列文章目录 [Android 事件分发]事件分发源码分析 ( 驱动层通过中断传递事件 | WindowManagerService 向 View 层传递事件 ) [Andr ...

  4. Android shortcut的使用及源码分析

    Android shortcut的使用及源码分析 最近遇到了一个切换国家码后部分应用的shortcut未更新的问题,就学习了shortcut的相关知识,在这里分享一下我了解的知识,希望能对大家有帮助. ...

  5. Android之rild进程启动源码分析

    Android 电话系统框架介绍 在android系统中rild运行在AP上,AP上的应用通过rild发送AT指令给BP,BP接收到信息后又通过rild传送给AP.AP与BP之间有两种通信方式: 1. ...

  6. 【Android 电量优化】JobScheduler 源码分析 ( JobServiceContext 源码分析 | 闭环操作总结 | 用户提交任务 | 广播接收者接受相关广播触发任务执行 )★

    文章目录 一.JobServiceContext 引入 二.JobServiceContext 源码分析 三.用户在应用层如何使用 JobScheduler 四.用户提交任务 五.广播接收者监听广播触 ...

  7. Android网络库之Okio源码分析

    今天来扒一扒Square公司的IO流的库Okio,现在越来越多Android项目都在使用Square公司的网络开源全家桶,即 Okio + OkHttp + Retrofit.这三个库的层级是从下网上 ...

  8. 【Android 插件化】VirtualApp 源码分析 ( 启动应用源码分析 | HomePresenterImpl 启动应用方法 | VirtualCore 启动插件应用最终方法 )

    文章目录 一.启动应用源码分析 1.HomeActivity 启动应用点击方法 2.HomePresenterImpl 启动应用方法 3.VirtualCore 启动插件应用最终方法 一.启动应用源码 ...

  9. 【Android 插件化】VirtualApp 源码分析 ( 添加应用源码分析 | LaunchpadAdapter 适配器 | 适配器添加元素 | PackageAppData 元素 )

    文章目录 一.添加应用源码分析 1.LaunchpadAdapter 适配器 2.适配器添加元素 3.PackageAppData 元素 一.添加应用源码分析 1.LaunchpadAdapter 适 ...

最新文章

  1. Python----面向对象---主动触发异常-raise
  2. Java获取正在执行的函数名
  3. Appium 命令行安装教程
  4. “大话架构”阿里架构师分享的Java程序员需要突破的技术要点
  5. JAVA--网络编程
  6. mysql约束教程,MySQL 约束
  7. 相机成像原理_【科研进展】动态虚拟相机:探索三维视觉成像新方法
  8. nginx-正则表达式-重定向
  9. 基于Android的人事管理系统开发与设计源码(二)
  10. jquery.autocomplete 使用解析
  11. 设计模式(五) 注解方式实现AOP
  12. GAN与自动编码器:深度生成模型的比较
  13. Logistic模型原理详解以及Python项目实现
  14. SPOJ COT 10628 Count on a tree
  15. ios键盘done中文_IOS_总结IOS中隐藏软键盘的三种方式,一、使用软键盘的 Done 键隐藏 - phpStudy...
  16. [ffmpeg][issues] bit_equal_to_one out of range: 0, but must be in [1,1]
  17. 阿里云盘 网页版地址 阿里云盘pc版 阿里云盘下载
  18. 初来乍到:新用户冷启的算法技巧
  19. IDEA中Dubugger设置
  20. 音乐外链生成 html,音乐外链生成工具V2.1 支持14个音乐网站外链提取转换

热门文章

  1. 魔幻蓝诗@画板3.0
  2. 首届“设计·无尽谈”论坛完满收官 持续打造当代设计共同体
  3. 华为现在研究出鸿蒙系统吗,中兴弃用华为鸿蒙系统,情有可原?网友群嘲中兴并非空穴来风?...
  4. 教育部推荐全国高校毕业生网上签约,君子签助力就业网签加速度
  5. 分类模型——Softmax回归
  6. Best Practices for Speeding Up Your Web Site
  7. 《都挺好》一剧,除了气愤的苏大强,生活中还有多少苏明哲!
  8. Bugku 多种方法解决
  9. 主键约束(primary key,简称PK)
  10. Java 9 缩小字符串( Compact String)