主頁 > 移動端開發 > Android11 WiFi連接流程梳理

Android11 WiFi連接流程梳理

2021-09-11 11:06:43 移動端開發

梳理一下Android11的wifi連接流程,

一、可以看到點擊連接以后,如果config不為null,則先保存網路,再進行連接,所以即使連接失敗,此網路依然在已保存網路串列里,
packages/apps/Settings/src/com/android/settings/wifi/WifiSettings.java

void submit(WifiConfigController configController) {

    final WifiConfiguration config = configController.getConfig();
 if (configController.getMode() == WifiConfigUiBase.MODE_MODIFY) {
        if(!configController.checkWapiParam()) {
            if(configController.getCurSecurity() == AccessPoint.SECURITY_WAPI_CERT) {
                startWapiCertManage();
            }
            return;
        }
        mWifiManager.save(config, mSaveListener);
    } else {
        if(!configController.checkWapiParam()) {
            if(configController.getCurSecurity() == AccessPoint.SECURITY_WAPI_CERT) {
                startWapiCertManage();
            }
            return;
        }
        mWifiManager.save(config, mSaveListener);
        if (mSelectedAccessPoint != null) { // Not an "Add network"
            connect(config, false /* isSavedNetwork */,
                    CONNECT_SOURCE_UNSPECIFIED);
        }
    }

    mWifiTracker.resumeScanning();
}
protected void connect(final WifiConfiguration config,
        boolean isSavedNetwork, @ConnectSource int connectSource) {
    // Log subtype if configuration is a saved network.
    mMetricsFeatureProvider.action(getContext(), SettingsEnums.ACTION_WIFI_CONNECT,
            isSavedNetwork);
    mConnectSource = connectSource;
    mWifiManager.connect(config, mConnectListener);
    mClickedConnect = true;
}

二、這里我們先看connect是怎么實作的,save的程序最后再看,具體實作還是在service,wifimanager只是一個橋梁、
frameworks/base/wifi/java/android/net/wifi/WifiManager.java

public void connect(@NonNull WifiConfiguration config, @Nullable ActionListener listener) {
    if (config == null) throw new IllegalArgumentException("config cannot be null");
    connectInternal(config, WifiConfiguration.INVALID_NETWORK_ID, listener);
}
private void connectInternal(@Nullable WifiConfiguration config, int networkId,
        @Nullable ActionListener listener) {
    ActionListenerProxy listenerProxy = null;
    Binder binder = null;
    if (listener != null) {
        listenerProxy = new ActionListenerProxy("connect", mLooper, listener);
        binder = new Binder();
    }
    try {
        mService.connect(config, networkId, binder, listenerProxy,
                listener == null ? 0 : listener.hashCode());
    } catch (RemoteException e) {
        if (listenerProxy != null) listenerProxy.onFailure(ERROR);
    } catch (SecurityException e) {
        if (listenerProxy != null) listenerProxy.onFailure(NOT_AUTHORIZED);
    }
}

三、wifiservice會判斷uid的權限,然后這里會判斷staid,因為android11上層是支持了雙wifi的,就是連接倆個AP,當然具體功能還要廠商自己實作,雙AP具體可以看Android11 wifi開啟流程,這里開啟wifi時就會分配staid,這里如果是AP1則是正常流程走ClientModeImpl,如果是AP2則會走QtiClientModeImpl,
frameworks/opt/net/wifi/service/java/com/android/server/wifi/WifiServiceImpl.java

public void connect(WifiConfiguration config, int netId, IBinder binder,
        @Nullable IActionListener callback, int callbackIdentifier) {
    int uid = Binder.getCallingUid();
    if (!isPrivileged(Binder.getCallingPid(), uid)) {
        throw new SecurityException(TAG + ": Permission denied");
    }
    mLog.info("connect uid=%").c(uid).flush();
    int staId;
    if(config != null) staId = config.staId;
    else staId = getIdentityForNetwork(netId);
    if(staId == STA_PRIMARY) {
    } else {
        QtiClientModeImpl qtiClientModeImpl = mActiveModeWarden.getQtiClientModeImpl(staId);
        if (qtiClientModeImpl != null)
            qtiClientModeImpl.connect(config, netId, binder, callback, callbackIdentifier, uid);
    }
    if (mWifiPermissionsUtil.checkNetworkSettingsPermission(uid)) {
        if (config == null) {
            mWifiMetrics.logUserActionEvent(UserActionEvent.EVENT_MANUAL_CONNECT, netId);
        } else {
            mWifiMetrics.logUserActionEvent(
                    UserActionEvent.EVENT_ADD_OR_UPDATE_NETWORK, config.networkId);
        }
    }
    mClientModeImpl.connect(config, netId, binder, callback, callbackIdentifier, uid);
}

四、看一下wifi狀態機里做了什么事情,
首先是呼叫WifiConfigManager.addOrUpdateNetwork來更新網路配置,如果保存成功則發送廣播,然后檢查網路權限等等各項操作結束以后,發送訊息CMD_CONNECT_NETWORK,
frameworks/opt/net/wifi/service/java/com/android/server/wifi/ClientModeImpl.java

public void connect(WifiConfiguration config, int netId, @Nullable IBinder binder,
        @Nullable IActionListener callback, int callbackIdentifier, int callingUid) {
    mWifiInjector.getWifiThreadRunner().post(() -> {
        if (callback != null && binder != null) {
            mProcessingActionListeners.add(binder, callback, callbackIdentifier);
        }
        /**
         * The connect message can contain a network id passed as arg1 on message or
         * or a config passed as obj on message.
         * For a new network, a config is passed to create and connect.
         * For an existing network, a network id is passed
         */
        NetworkUpdateResult result = null;
        if (config != null) {
            result = mWifiConfigManager.addOrUpdateNetwork(config, callingUid);
            if (!result.isSuccess()) {
                loge("connectNetwork adding/updating config=" + config + " failed");
                sendActionListenerFailure(callbackIdentifier, WifiManager.ERROR);
                return;
            }
            broadcastWifiCredentialChanged(WifiManager.WIFI_CREDENTIAL_SAVED, config);
        } else {
            if (mWifiConfigManager.getConfiguredNetwork(netId) == null) {
                loge("connectNetwork Invalid network Id=" + netId);
                sendActionListenerFailure(callbackIdentifier, WifiManager.ERROR);
                return;
            }
            result = new NetworkUpdateResult(netId);
        }
        final int networkId = result.getNetworkId();
        mWifiConfigManager.userEnabledNetwork(networkId);
        if (!mWifiConfigManager.enableNetwork(networkId, true, callingUid, null)
                || !mWifiConfigManager.updateLastConnectUid(networkId, callingUid)) {
            logi("connect Allowing uid " + callingUid
                    + " with insufficient permissions to connect=" + networkId);
        } else if (mWifiPermissionsUtil.checkNetworkSettingsPermission(callingUid)) {
            // Note user connect choice here, so that it will be considered in the
            // next network selection.
            mWifiConnectivityManager.setUserConnectChoice(networkId);
        }
        Message message =
                obtainMessage(CMD_CONNECT_NETWORK, -1, callbackIdentifier, result);
        message.sendingUid = callingUid;
        sendMessage(message);
    });
}

現在wifi狀態機應該在ConnectModeState,我們看它怎么處理,

case CMD_CONNECT_NETWORK:
    callbackIdentifier = message.arg2;
    result = (NetworkUpdateResult) message.obj;
    netId = result.getNetworkId();
    connectToUserSelectNetwork(
            netId, message.sendingUid, result.hasCredentialChanged());
    mWifiMetrics.logStaEvent(
            StaEvent.TYPE_CONNECT_NETWORK,
            mWifiConfigManager.getConfiguredNetwork(netId));
    sendActionListenerSuccess(callbackIdentifier);
    break;
private void connectToUserSelectNetwork(int netId, int uid, boolean forceReconnect) {
    logd("connectToUserSelectNetwork netId " + netId + ", uid " + uid
            + ", forceReconnect = " + forceReconnect);
    if (!forceReconnect && (mLastNetworkId == netId || mTargetNetworkId == netId)) {
        // We're already connecting/connected to the user specified network, don't trigger a
        // reconnection unless it was forced.
        logi("connectToUserSelectNetwork already connecting/connected=" + netId);
    } else {
        mWifiConnectivityManager.prepareForForcedConnection(netId);
        if (uid == Process.SYSTEM_UID) {
            mWifiMetrics.setNominatorForNetwork(netId,
                    WifiMetricsProto.ConnectionEvent.NOMINATOR_MANUAL);
        }
        startConnectToNetwork(netId, uid, SUPPLICANT_BSSID_ANY);
    }
}

這里有發送了CMD_START_CONNECT訊息,

public void startConnectToNetwork(int networkId, int uid, String bssid) {
    sendMessage(CMD_START_CONNECT, networkId, uid, bssid);
}

還是在ConnectModeState中處理,在這里會更新AP的資訊,然后計分器打分,從底層獲取macaddress,然后開啟IPClient,上述完成以后開始connectToNetwork

case CMD_START_CONNECT:
    /* connect command coming from auto-join */
    netId = message.arg1;
    int uid = message.arg2;
    bssid = (String) message.obj;
    mSentHLPs = false;

    if (!hasConnectionRequests()) {
        if (mNetworkAgent == null) {
            loge("CMD_START_CONNECT but no requests and not connected,"
                    + " bailing");
            break;
        } else if (!mWifiPermissionsUtil.checkNetworkSettingsPermission(uid)) {
            loge("CMD_START_CONNECT but no requests and connected, but app "
                    + "does not have sufficient permissions, bailing");
            break;
        }
    }
    config = mWifiConfigManager.getConfiguredNetworkWithoutMasking(netId);
    logd("CMD_START_CONNECT "
            + " my state " + getCurrentState().getName()
            + " nid=" + Integer.toString(netId)
            + " roam=" + Boolean.toString(mIsAutoRoaming));
    if (config == null) {
        loge("CMD_START_CONNECT and no config, bail out...");
        break;
    }
    mTargetNetworkId = netId;
    // Update scorecard while there is still state from existing connection
    int scanRssi = mWifiConfigManager.findScanRssi(netId,
            mWifiHealthMonitor.getScanRssiValidTimeMs());
    mWifiScoreCard.noteConnectionAttempt(mWifiInfo, scanRssi, config.SSID);
    mBssidBlocklistMonitor.updateFirmwareRoamingConfiguration(config.SSID);

    updateWifiConfigOnStartConnection(config, bssid);
    reportConnectionAttemptStart(config, mTargetBssid,
            WifiMetricsProto.ConnectionEvent.ROAM_UNRELATED);

    String currentMacAddress = mWifiNative.getMacAddress(mInterfaceName);
    mWifiInfo.setMacAddress(currentMacAddress);
    Log.i(TAG, "Connecting with " + currentMacAddress + " as the mac address");
    mTargetWifiConfiguration = config;
    /* Check for FILS configuration again after updating the config */
    if (config.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.FILS_SHA256)
            || config.allowedKeyManagement.get(
            WifiConfiguration.KeyMgmt.FILS_SHA384)) {

        boolean isIpClientStarted = startIpClient(config, true);
        if (isIpClientStarted) {
            mIpClientWithPreConnection = true;
            break;
        }
    }
    connectToNetwork(config);
    break;
void connectToNetwork(WifiConfiguration config) {
    if ((config != null) && mWifiNative.connectToNetwork(mInterfaceName, config)) {
        mWifiInjector.getWifiLastResortWatchdog().noteStartConnectTime();
        mWifiMetrics.logStaEvent(StaEvent.TYPE_CMD_START_CONNECT, config);
        mLastConnectAttemptTimestamp = mClock.getWallClockMillis();
        mIsAutoRoaming = false;
        if (getCurrentState() != mDisconnectedState) {
            transitionTo(mDisconnectingState);
        }
    } else {
        loge("CMD_START_CONNECT Failed to start connection to network " + config);
        mTargetWifiConfiguration = null;
        stopIpClient();
        reportConnectionAttemptEnd(
                WifiMetrics.ConnectionEvent.FAILURE_CONNECT_NETWORK_FAILED,
                WifiMetricsProto.ConnectionEvent.HLF_NONE,
                WifiMetricsProto.ConnectionEvent.FAILURE_REASON_UNKNOWN);
    }
}

五、然后通過WifiNative到了SupplicantStaIfaceHal
frameworks/opt/net/wifi/service/java/com/android/server/wifi/SupplicantStaIfaceHal.java

connectToNetwork->addNetworkAndSaveConfig->addNetwork->supplicant

六、到了supplicant里面,添加網路,注冊網路,完成以后就要開始連接了
external/wpa_supplicant_8/wpa_supplicant/hidl/1.3/sta_iface.cpp

std::pair<SupplicantStatus, sp<ISupplicantNetwork>>
StaIface::addNetworkInternal()
{
	android::sp<ISupplicantStaNetwork> network;
	struct wpa_supplicant *wpa_s = retrieveIfacePtr();
	struct wpa_ssid *ssid = wpa_supplicant_add_network(wpa_s);
	if (!ssid) {
		return {{SupplicantStatusCode::FAILURE_UNKNOWN, ""}, network};
	}
	HidlManager *hidl_manager = HidlManager::getInstance();
	if (!hidl_manager ||
	    hidl_manager->getStaNetworkHidlObjectByIfnameAndNetworkId(
		wpa_s->ifname, ssid->id, &network)) {
		return {{SupplicantStatusCode::FAILURE_UNKNOWN, ""}, network};
	}
	return {{SupplicantStatusCode::SUCCESS, ""}, network};
}

external/wpa_supplicant_8/wpa_supplicant/wpa_supplicant.c

struct wpa_ssid * wpa_supplicant_add_network(struct wpa_supplicant *wpa_s)
{
	struct wpa_ssid *ssid;

	ssid = wpa_config_add_network(wpa_s->conf);
	if (!ssid)
		return NULL;
	wpas_notify_network_added(wpa_s, ssid);
	ssid->disabled = 1;
	wpa_config_set_network_defaults(ssid);

	return ssid;
}

/external/wpa_supplicant_8/wpa_supplicant/config.c

struct wpa_ssid * wpa_config_add_network(struct wpa_config *config)
{
	int id;
	struct wpa_ssid *ssid, *last = NULL;

	id = -1;
	ssid = config->ssid;
	while (ssid) {
		if (ssid->id > id)
			id = ssid->id;
		last = ssid;
		ssid = ssid->next;
	}
	id++;

	ssid = os_zalloc(sizeof(*ssid));
	if (ssid == NULL)
		return NULL;
	ssid->id = id;
	dl_list_init(&ssid->psk_list);
	if (last)
		last->next = ssid;
	else
		config->ssid = ssid;

	wpa_config_update_prio_list(config);

	return ssid;
}

external/wpa_supplicant_8/wpa_supplicant/notify.c

void wpas_notify_network_added(struct wpa_supplicant *wpa_s,
			       struct wpa_ssid *ssid)
{
	if (wpa_s->p2p_mgmt)
		return;

	/*
	 * Networks objects created during any P2P activities should not be
	 * exposed out. They might/will confuse certain non-P2P aware
	 * applications since these network objects won't behave like
	 * regular ones.
	 */
	if (!ssid->p2p_group && wpa_s->global->p2p_group_formation != wpa_s) {
		wpas_dbus_register_network(wpa_s, ssid);
#ifdef CONFIG_HIDL
		wpas_hidl_register_network(wpa_s, ssid);
#endif
	}
}

external/wpa_supplicant_8/wpa_supplicant/hidl/1.3/hidl.cpp

int wpas_hidl_register_network(
    struct wpa_supplicant *wpa_s, struct wpa_ssid *ssid)
{
	if (!wpa_s || !wpa_s->global->hidl || !ssid)
		return 1;

	wpa_printf(
	    MSG_DEBUG, "Registering network to hidl control: %d", ssid->id);

	HidlManager *hidl_manager = HidlManager::getInstance();
	if (!hidl_manager)
		return 1;

	return hidl_manager->registerNetwork(wpa_s, ssid);
}

七、接著第五步,SupplicantStaIfaceHal中的connectToNetwork最后會執行select,我們看supplicant中select具體做了什么

SupplicantStaNetworkHal networkHandle =
       checkSupplicantStaNetworkAndLogFailure(ifaceName, "connectToNetwork");
if (networkHandle == null) {
   loge("No valid remote network handle for network configuration: "
           + config.getKey());
   return false;
}

PmkCacheStoreData pmkData = mPmkCacheEntries.get(config.networkId);
if (pmkData != null
       && !WifiConfigurationUtil.isConfigForPskNetwork(config)
       && pmkData.expirationTimeInSec > mClock.getElapsedSinceBootMillis() / 1000) {
   logi("Set PMK cache for config id " + config.networkId);
   if (networkHandle.setPmkCache(pmkData.data)) {
       mWifiMetrics.setConnectionPmkCache(true);
   }
}

if (!networkHandle.select()) {
   loge("Failed to select network configuration: " + config.getKey());
   return false;
}
return true;
}

八、這里選擇AP以后就開始關聯了,關聯成功就是四次握手,
external/wpa_supplicant_8/wpa_supplicant/hidl/1.3/sta_network.cpp
external/wpa_supplicant_8/wpa_supplicant/wpa_supplicant.c
/external/wpa_supplicant_8/wpa_supplicant/events.c
external/wpa_supplicant_8/src/rsn_supp/wpa.c
select->selectInternal->wpa_supplicant_select_network->wpa_supplicant_fast_associate->wpas_select_network_from_last_scan->wpa_supplicant_pick_network->wpa_supplicant_select_bss->wpa_supplicant_connect->wpa_supplicant_associate->wpas_start_assoc_cb->wpa_sm_set_assoc_wpa_ie

supplicant狀態的關鍵日志

09-07 11:17:32.502  3911  3911 D wpa_supplicant: wlan0: State: DISCONNECTED -> ASSOCIATING
09-07 11:17:32.554  3911  3911 D wpa_supplicant: wlan0: State: ASSOCIATING -> ASSOCIATED
09-07 11:17:32.665  3911  3911 D wpa_supplicant: wlan0: State: ASSOCIATED -> 4WAY_HANDSHAKE
09-07 11:17:32.683  3911  3911 D wpa_supplicant: wlan0: State: 4WAY_HANDSHAKE -> 4WAY_HANDSHAKE
09-07 11:17:32.686  3911  3911 D wpa_supplicant: wlan0: State: 4WAY_HANDSHAKE -> GROUP_HANDSHAKE
09-07 11:17:32.689  3911  3911 D wpa_supplicant: wlan0: State: GROUP_HANDSHAKE -> COMPLETED

總體流程如下圖,第一次畫流程圖,有點丑,

請添加圖片描述

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/299209.html

標籤:其他

上一篇:Android的訊息機制,handler多種用法

下一篇:開發4年13K,轉行自動化測驗,薪資還能漲嗎···

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more