Android 9.0 SIM卡初始化流程

转载:https://blog.csdn.net/linyongan/article/details/51406123

本文主要讲述Android 9.0 SIM卡初始化流程,这个过程也涉及到UICC框架的初始化,UICC(Universal Integrated Circuit Card)的架构图如下:
主要的类及其作用:
UiccController:整个Uicc相关信息的控制接口,监控SIM状态变化
UiccCard:Uicc卡的抽象,用来更新卡的状态
IccCardStatus:维护Uicc卡的状态,CardState&PinState
UiccCardApplication:Uicc的一个具体的应用,负责卡中数据读写,存取,pin和puk密码设置,解锁
CatService:主要负责SIM Tollkit相关
IccConstants:SIM卡中文件地址,不同数据在SIM卡上的字段地址。
IccRecords:记录SIM卡的数据
IccFileHandler:读取SIM卡数据以及处理接收的结果。
一、实例化UiccController对象

在这里插入图片描述

UiccController是整个UICC相关信息的控制接口,UiccController的实例化就是在RIL与UiccController 之间建立监听关系,这样的话,当SIM卡状态发生变化时,UiccController就可以马上知道并且做出相应的操作。
UiccController对象是在PhoneFacotry.java中的makeDefaultPhone()方法中初始化的,有个细节值得注意的是sCommandsInterfaces数组的i对应的是PhoneId。

    public static void makeDefaultPhone(Context context) {
        // Instantiate UiccController so that all other classes can just
        //得到UiccController对象
        mUiccController = UiccController.make(context, sCommandsInterfaces);
        for (int i = 0; i < numPhones; i++) {
            PhoneBase phone = null;
            int phoneType = TelephonyManager.getPhoneType(networkModes[i]);
            if (phoneType == PhoneConstants.PHONE_TYPE_GSM) {
                //sCommandsInterfaces的i对应的是PhoneId;第二小节会再次提到这个i值
                phone = TelephonyPluginDelegate.getInstance().makeGSMPhone(context,
                                sCommandsInterfaces[i], sPhoneNotifier, i);
            } else if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
                phone = TelephonyPluginDelegate.getInstance().makeCDMALTEPhone(context,
                                sCommandsInterfaces[i], sPhoneNotifier, i);
            }
            Rlog.i(LOG_TAG, "Creating Phone with type = " + phoneType + " sub = " + i);
            sProxyPhones[i] = TelephonyPluginDelegate.getInstance().makePhoneProxy(phone);
        }
   }

    @TelephonyPluginBase.java
    public PhoneBase makeGSMPhone(Context context, CommandsInterface ci,
            PhoneNotifier notifier, int phoneId) {
        return new GSMPhone(context, ci, notifier, phoneId);
    }
    }

在UiccController.java的make()方法中new了一个UiccController对象,

    public static UiccController make(Context c, CommandsInterface[] ci) {
        synchronized (mLock) {
            if (mInstance != null) {
                throw new RuntimeException("MSimUiccController.make() should only be called once");
            }
            //实例化UiccController对象
            mInstance = new UiccController(c, ci);
            return (UiccController)mInstance;
        }
    }

    private UiccController(Context c, CommandsInterface []ci) {
        if (DBG) log("Creating UiccController");
        mContext = c;
        mCis = ci;
        for (int i = 0; i < mCis.length; i++) {
            //index对应的是PhoneId
            Integer index = new Integer(i);
            //注册监听四种事件
            mCis[i].registerForIccStatusChanged(this, EVENT_ICC_STATUS_CHANGED, index);
            mCis[i].registerForAvailable(this, EVENT_ICC_STATUS_CHANGED, index);
            mCis[i].registerForNotAvailable(this, EVENT_RADIO_UNAVAILABLE, index);
            mCis[i].registerForIccRefresh(this, EVENT_SIM_REFRESH, index);
        }
    }

在上面UiccController的构造方法中可以看到,注册了三个事件EVENT_ICC_STATUS_CHANGED(监听SIM卡的状态变化),EVENT_RADIO_UNAVAILABLE(一旦radio变成不可用状态,就清空SIM卡的信息),EVENT_SIM_REFRESH。index对应的是PhoneId,当上面这三种消息上来时,就知道对应哪个Phone对象,也就对应那张卡。
当接收到EVENT_ICC_STATUS_CHANGED消息后,UiccController调用RIL.java的getIccCardStatus()方法给MODEM发送RIL_REQUEST_GET_SIM_STATUS消息,查询SIM卡的状态。

    public void handleMessage (Message msg) {
        .....
        //1.首先从Message中取出PhoneId
        Integer index = getCiIndex(msg);
        .....
        case EVENT_ICC_STATUS_CHANGED:
            if (DBG) log("Received EVENT_ICC_STATUS_CHANGED, calling getIccCardStatus");
            //2.查询当前SIM卡的状态
            mCis[index].getIccCardStatus(obtainMessage(EVENT_GET_ICC_STATUS_DONE, index));
        break;
        case EVENT_GET_ICC_STATUS_DONE:
            if (DBG) log("Received EVENT_GET_ICC_STATUS_DONE");
            //3.处理查询到的状态信息
            onGetIccCardStatusDone(ar, index);
        break;
   }

二、实例化UiccCard对象

当查询SIM卡的状态完毕后,先从result中解析出IccCardStatus,依据IccCardStatus来创建UiccCard对象,一个UiccCard 对象代表着一张SIM卡;如果UiccCard对象已存在就直接调它的update()方法更新UiccCard的信息。

    private synchronized void onGetIccCardStatusDone(AsyncResult ar, Integer index) {
        if (ar.exception != null) {
            Rlog.e(LOG_TAG,"Error getting ICC status. "
                    + "RIL_REQUEST_GET_ICC_STATUS should "
                    + "never return an error", ar.exception);
            return;
        }
        if (!isValidCardIndex(index)) {
            Rlog.e(LOG_TAG,"onGetIccCardStatusDone: invalid index : " + index);
            return;
        }

        IccCardStatus status = (IccCardStatus)ar.result;
        //1.创建或更新UiccCard
        if (mUiccCards[index] == null) {
            //Create new card(Android6.0 调的是4个参数的构造方法)
            mUiccCards[index] = new UiccCard(mContext, mCis[index], status, index);
        } else {
            //Update already existing card
            mUiccCards[index].update(mContext, mCis[index] , status);
        }

        if (DBG) log("Notifying IccChangedRegistrants");
        //2.通知监听UiccController的监听者
        mIccChangedRegistrants.notifyRegistrants(new AsyncResult(null, index, null));

    }

在UiccCard的构造方法中,最终还是调用了update()方法

    public UiccCard(Context c, CommandsInterface ci, IccCardStatus ics, int phoneId) {
        mCardState = ics.mCardState;
        mPhoneId = phoneId;
        update(c, ci, ics);
    }

    public void update(Context c, CommandsInterface ci, IccCardStatus ics) {
        synchronized (mLock) {
            CardState oldState = mCardState;
            mCardState = ics.mCardState;
            mUniversalPinState = ics.mUniversalPinState;
            mGsmUmtsSubscriptionAppIndex = ics.mGsmUmtsSubscriptionAppIndex;
            mCdmaSubscriptionAppIndex = ics.mCdmaSubscriptionAppIndex;
            mImsSubscriptionAppIndex = ics.mImsSubscriptionAppIndex;
            mContext = c;
            mCi = ci;

            //更新UiccApplications
            if (DBG) log(ics.mApplications.length + " applications");
            for ( int i = 0; i < mUiccApplications.length; i++) {
                if (mUiccApplications[i] == null) {
                    //Create newly added Applications
                    if (i < ics.mApplications.length) {
                        mUiccApplications[i] = new UiccCardApplication(this,
                                ics.mApplications[i], mContext, mCi);
                    }
                } else if (i >= ics.mApplications.length) {
                    //Delete removed applications
                    mUiccApplications[i].dispose();
                    mUiccApplications[i] = null;
                } else {
                    //Update the rest
                    mUiccApplications[i].update(ics.mApplications[i], mContext, mCi);
                }
            }
            //创建CatService,用于读取STK的信息
            createAndUpdateCatService();

            // Reload the carrier privilege rules if necessary.
            log("Before privilege rules: " + mCarrierPrivilegeRules + " : " + mCardState);
            if (mCarrierPrivilegeRules == null && mCardState == CardState.CARDSTATE_PRESENT) {
                mCarrierPrivilegeRules = new UiccCarrierPrivilegeRules(this,
                        mHandler.obtainMessage(EVENT_CARRIER_PRIVILIGES_LOADED));
            } else if (mCarrierPrivilegeRules != null && mCardState != CardState.CARDSTATE_PRESENT) {
                mCarrierPrivilegeRules = null;
            }

            sanitizeApplicationIndexes();
            //先获取radio的状态
            RadioState radioState = mCi.getRadioState();
            if (DBG) log("update: radioState=" + radioState + " mLastRadioState="
                    + mLastRadioState);
            // No notifications while radio is off or we just powering up
            //根据radio的状态和卡的状态来判断有没有插拔SIM卡动作
            if (radioState == RadioState.RADIO_ON && mLastRadioState == RadioState.RADIO_ON) {
                if (oldState != CardState.CARDSTATE_ABSENT &&
                        mCardState == CardState.CARDSTATE_ABSENT) {
                    if (DBG) log("update: notify card removed");
                    mAbsentRegistrants.notifyRegistrants();
                    mHandler.sendMessage(mHandler.obtainMessage(EVENT_CARD_REMOVED, null));
                } else if (oldState == CardState.CARDSTATE_ABSENT &&
                        mCardState != CardState.CARDSTATE_ABSENT) {
                    if (DBG) log("update: notify card added");
                    mHandler.sendMessage(mHandler.obtainMessage(EVENT_CARD_ADDED, null));
                }
           }
            mLastRadioState = radioState;
        }
    }

在UiccCard.java的update()方法中,实例化了UiccCardApplication对象,或者调UiccCardApplication的update()方法更新状态。mCardState记录着卡的状态,根据新旧mCardState就可以知道CARD_ADDED或者CARD_REMOVED。
三、实例化UiccCardApplication对象

    UiccCardApplication(UiccCard uiccCard,
                        IccCardApplicationStatus as,
                        Context c,
                        CommandsInterface ci) {
        if (DBG) log("Creating UiccApp: " + as);
        mUiccCard = uiccCard;
        mAppState = as.app_state;
        mAppType = as.app_type;
        mAuthContext = getAuthContext(mAppType);
        mPersoSubState = as.perso_substate;
        mAid = as.aid;
        mAppLabel = as.app_label;
        mPin1Replaced = (as.pin1_replaced != 0);
        mPin1State = as.pin1;
        mPin2State = as.pin2;

        mContext = c;
        mCi = ci;
        //根据AppType实例化IccFileHandler
        mIccFh = createIccFileHandler(as.app_type);
        //根据AppType实例化IccRecords
        mIccRecords = createIccRecords(as.app_type, mContext, mCi);
        if (mAppState == AppState.APPSTATE_READY) {
            queryFdn();
            //查询PIN1码的状态
            queryPin1State();
        }
        //注册监听
        mCi.registerForNotAvailable(mHandler, EVENT_RADIO_UNAVAILABLE, null);
    }

在UiccCardApplication初始化的时候,会调用自身的createIccRecords()方法,根据AppType创建对应的IccRecords 对象。

    private IccRecords createIccRecords(AppType type, Context c, CommandsInterface ci) {
        if (type == AppType.APPTYPE_USIM || type == AppType.APPTYPE_SIM) {
            //实例化SIMRecords对象
            return TelephonyPluginDelegate.getInstance().makeSIMRecords(this, c, ci);
        } else if (type == AppType.APPTYPE_RUIM || type == AppType.APPTYPE_CSIM){
            return new RuimRecords(this, c, ci);
        } else if (type == AppType.APPTYPE_ISIM) {
            return new IsimUiccRecords(this, c, ci);
        } else {
            // Unknown app type (maybe detection is still in progress)
            return null;
        }
    }

接着会进入TelephonyPluginDelegate.java和TelephonyPluginBase.java的makeSIMRecords()方法

    @TelephonyPluginDelegate.java
    public SIMRecords makeSIMRecords (UiccCardApplication app, Context c, CommandsInterface ci) {
        return sPlugin.makeSIMRecords (app, c, ci);
    }

    @TelephonyPluginBase.java
    public SIMRecords makeSIMRecords (UiccCardApplication app, Context c, CommandsInterface ci) {
        return new SIMRecords(app, c, ci);
    }

到这里就可以看到直接new 了一个SIMRecords对象。
四、实例化SIMRecords对象

    public SIMRecords(UiccCardApplication app, Context c, CommandsInterface ci) {
        super(app, c, ci);
        mAdnCache = new AdnRecordCache(mFh);
        mVmConfig = new VoiceMailConstants();
        //从spn-conf.xml文件中加载SPN
        mSpnOverride = new SpnOverride();
        // No load request is made till SIM ready
        mRecordsRequested = false;
        // recordsToLoad is set to 0 because no requests are made yet
        mRecordsToLoad = 0;

        mCi.setOnSmsOnSim(this, EVENT_SMS_ON_SIM, null);
        mCi.registerForIccRefresh(this, EVENT_SIM_REFRESH, null);

        //初始化成员变量
        resetRecords();
        //注册监听,如果已ready,那么开始加载数据
        mParentApp.registerForReady(this, EVENT_APP_READY, null);
        mParentApp.registerForLocked(this, EVENT_APP_LOCKED, null);
        if (DBG) log("SIMRecords X ctor this=" + this);
    }

等到AppState变成APPSTATE_READY是,UiccCardApplication会在notifyReadyRegistrantsIfNeeded()方法里通知SIMRecords,那么在SIMRecords的handleMessage()方法就会收到EVENT_APP_READY消息。

    public void handleMessage(Message msg) {
            case EVENT_APP_READY:
                onReady();
                break;
    }

    @Override
    public void onReady() {
        fetchSimRecords();
    }

五、读取EF文件信息

当执行到fetchSimRecords()方法时,才真正开始加载EF文件信息。
具体的读取SIM卡EF文件信息的过程是由 IccFileHandler 来实现的,根据EF文件的类型,调用不同的方法,loadEFTransparent()和loadEFLinearFixed()最终都会调用RIL.java的iccIOForApp()方法。
读取某一项信息时会调用两次RIL.java的iccIOForApp():
第一次:先读取当前分区的长度lc.mRecordSize。

   Message response = obtainMessage(EVENT_GET_RECORD_SIZE_DONE,
      new LoadLinearFixedContext(fileid, recordNum, efPath, onLoaded));

   mCi.iccIOForApp(COMMAND_GET_RESPONSE, fileid, efPath,
       0, 0,
       GET_RESPONSE_EF_SIZE_BYTES,
       null, null, mAid, response);

第二次:再根据lc.mRecordSize去读取具体内容。


case EVENT_GET_RECORD_SIZE_DONE:
    ......
    mCi.iccIOForApp(COMMAND_READ_RECORD, lc.mEfid, path,
       lc.mRecordNum,READ_RECORD_MODE_ABSOLUTE,
       lc.mRecordSize,
       null, null, mAid,obtainMessage(EVENT_READ_RECORD_DONE, lc));

某一项信息的加载流程如下流程图。

    protected void fetchSimRecords() {
        mRecordsRequested = true;
        if (DBG) log("fetchSimRecords " + mRecordsToLoad);
        //读取IMSI
        mCi.getIMSIForApp(mParentApp.getAid(), obtainMessage(EVENT_GET_IMSI_DONE));
        //每开始load一条记录,mRecordsToLoad加1,等加载完一条记录后
        //mRecordsToLoad就减1,当mRecordsToLoad等于0时说明已加载完所有数据了。
        mRecordsToLoad++;
        //从EF_ICCID(0x2fe2)读取ICCID
        mFh.loadEFTransparent(EF_ICCID, obtainMessage(EVENT_GET_ICCID_DONE));
        mRecordsToLoad++;

        // FIXME should examine EF[MSISDN]'s capability configuration
        // to determine which is the voice/data/fax line
        new AdnRecordLoader(mFh).loadFromEF(EF_MSISDN, getExtFromEf(EF_MSISDN), 1,
                    obtainMessage(EVENT_GET_MSISDN_DONE));
        mRecordsToLoad++;

        //读取VoiceMail信息
        mFh.loadEFLinearFixed(EF_MBI, 1, obtainMessage(EVENT_GET_MBI_DONE));
        mRecordsToLoad++;

        mFh.loadEFTransparent(EF_AD, obtainMessage(EVENT_GET_AD_DONE));
        mRecordsToLoad++;

        // Record number is subscriber profile
        mFh.loadEFLinearFixed(EF_MWIS, 1, obtainMessage(EVENT_GET_MWIS_DONE));
        mRecordsToLoad++;


        // Also load CPHS-style voice mail indicator, which stores
        // the same info as EF[MWIS]. If both exist, both are updated
        // but the EF[MWIS] data is preferred
        // Please note this must be loaded after EF[MWIS]
        mFh.loadEFTransparent(
                EF_VOICE_MAIL_INDICATOR_CPHS,
                obtainMessage(EVENT_GET_VOICE_MAIL_INDICATOR_CPHS_DONE));
        mRecordsToLoad++;

        // Same goes for Call Forward Status indicator: fetch both
        // EF[CFIS] and CPHS-EF, with EF[CFIS] preferred.
        loadCallForwardingRecords();
        //从EF_SPN(0x6F46)、EF_SPN_CPHS(0x6f14)、EF_SPN_SHORT_CPHS(0x6f18)三个地址上读取SPN
        getSpnFsm(true, null);
        //从EF_SPDI(0x6fcd)读取SPDI
        mFh.loadEFTransparent(EF_SPDI, obtainMessage(EVENT_GET_SPDI_DONE));
        mRecordsToLoad++;
        //从EF_PNN(0x6fc5)读取PNN
        mFh.loadEFLinearFixed(EF_PNN, 1, obtainMessage(EVENT_GET_PNN_DONE));
        mRecordsToLoad++;

        mFh.loadEFTransparent(EF_SST, obtainMessage(EVENT_GET_SST_DONE));
        mRecordsToLoad++;

        mFh.loadEFTransparent(EF_INFO_CPHS, obtainMessage(EVENT_GET_INFO_CPHS_DONE));
        mRecordsToLoad++;

        mFh.loadEFTransparent(EF_CSP_CPHS,obtainMessage(EVENT_GET_CSP_CPHS_DONE));
        mRecordsToLoad++;

        mFh.loadEFTransparent(EF_GID1, obtainMessage(EVENT_GET_GID1_DONE));
        mRecordsToLoad++;

        mFh.loadEFTransparent(EF_GID2, obtainMessage(EVENT_GET_GID2_DONE));
        mRecordsToLoad++;

        loadEfLiAndEfPl();

        // XXX should seek instead of examining them all
        if (false) { // XXX
            mFh.loadEFLinearFixedAll(EF_SMS, obtainMessage(EVENT_GET_ALL_SMS_DONE));
            mRecordsToLoad++;
        }

        if (CRASH_RIL) {
            String sms = "0107912160130310f20404d0110041007030208054832b0120"
                         + "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
                         + "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
                         + "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
                         + "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
                         + "ffffffffffffffffffffffffffffff";
            byte[] ba = IccUtils.hexStringToBytes(sms);

            mFh.updateEFLinearFixed(EF_SMS, 1, ba, null,
                            obtainMessage(EVENT_MARK_SMS_READ_DONE, 1));
        }
        if (DBG) log("fetchSimRecords " + mRecordsToLoad + " requested: " + mRecordsRequested);
    }

六、读取数据完毕

读取ICCID 、读取SPDI 、读取VoiceMail、读取SPN、读取PNN;
回到fetchSimRecords()方法,每加载一项,mRecordsToLoad就加1;等到某一项读取数据完毕,handleMessage()方法被执行,就会调onRecordLoaded()方法

    protected void onRecordLoaded() {
        // One record loaded successfully or failed, In either case
        // we need to update the recordsToLoad count
        //每查询完一条记录,mRecordsToLoad减1
        mRecordsToLoad -= 1;
        if (DBG) log("onRecordLoaded " + mRecordsToLoad + " requested: " + mRecordsRequested);
        //当mRecordsToLoad等于0,说明在fetchSimRecords()方法
        //中启动加载的数据都已经加载完了。
        if (mRecordsToLoad == 0 && mRecordsRequested == true) {
            onAllRecordsLoaded();
        } else if (mRecordsToLoad < 0) {
            loge("recordsToLoad <0, programmer error suspected");
            mRecordsToLoad = 0;
        }
    }

mRecordsToLoad的值会减1,直到mRecordsToLoad的值为0时,说明在fetchSimRecords()中启动加载的数据都已异步读取完成。就会进入onAllRecordsLoaded()方法。

    @Override
    protected void onAllRecordsLoaded() {
        if (DBG) log("record load complete");

        Resources resource = Resources.getSystem();
        if (resource.getBoolean(com.android.internal.R.bool.config_use_sim_language_file)) {
            setSimLanguage(mEfLi, mEfPl);
        } else {
            if (DBG) log ("Not using EF LI/EF PL");
        }

        setVoiceCallForwardingFlagFromSimRecords();

        if (mParentApp.getState() == AppState.APPSTATE_PIN ||
               mParentApp.getState() == AppState.APPSTATE_PUK) {
            // reset recordsRequested, since sim is not loaded really
            mRecordsRequested = false;
            // lock state, only update language
            return ;
        }

        // Some fields require more than one SIM record to set
        //获取MCC + MNC
        String operator = getOperatorNumeric();
        if (!TextUtils.isEmpty(operator)) {
            log("onAllRecordsLoaded set 'gsm.sim.operator.numeric' to operator='" +
                    operator + "'");
            log("update icc_operator_numeric=" + operator);
            mTelephonyManager.setSimOperatorNumericForPhone(
                    mParentApp.getPhoneId(), operator);
            final SubscriptionController subController = SubscriptionController.getInstance();
            subController.setMccMnc(operator, subController.getDefaultSmsSubId());
        } else {
            log("onAllRecordsLoaded empty 'gsm.sim.operator.numeric' skipping");
        }

        if (!TextUtils.isEmpty(mImsi)) {
            log("onAllRecordsLoaded set mcc imsi" + (VDBG ? ("=" + mImsi) : ""));
            mTelephonyManager.setSimCountryIsoForPhone(
                    mParentApp.getPhoneId(), MccTable.countryCodeForMcc(
                    Integer.parseInt(mImsi.substring(0,3))));
        } else {
            log("onAllRecordsLoaded empty imsi skipping setting mcc");
        }

        setVoiceMailByCountry(operator);
        setSpnFromConfig(operator);
        //通知应用层
        mRecordsLoadedRegistrants.notifyRegistrants(
            new AsyncResult(null, null, null));
    }

在onAllRecordsLoaded()方法中会对读取到的数据进行处理和存储,到这里,SIM卡初始化的流程就结束了。

Android 9.0系统的U-Boot Logo加载流程如下: 首先,当设备上电时,启动引导加载程序(Bootloader)会运行。U-Boot作为一个常用的开源Bootloader,被广泛用于Android设备。然后,U-Boot开始执行它的初始化代码,包括初始化硬件、加载环境变量和配置等。 接下来,U-Boot开始加载Logo图像。在Android 9.0,U-Boot会在启动过程的早期加载Logo图像,并在屏幕上显示。为了加载Logo图像,U-Boot需要的一些文件和配置如下: 1. Logo图像文件:通常是一个BMP格式的图片文件,作为设备启动时显示的Logo图像。这个文件会包含Logo图像的像素数据和相关的图像信息。 2. U-Boot配置文件:U-Boot需要通过一个配置文件来指定Logo图像的位置和属性。这个配置文件通常存储在设备的存储器,如NAND Flash或eMMC闪存。 3. 屏幕显示驱动:U-Boot还需要适配设备的屏幕显示驱动,以确保Logo图像可以正确显示在设备屏幕上。这包括配置屏幕分辨率、颜色深度等参数。 一旦U-Boot完成初始化和加载Logo图像所需的文件和配置,它会通过屏幕显示驱动将Logo图像显示在设备的屏幕上。Logo图像会持续显示一段时间,直到系统完成启动过程,然后才会被系统界面替换。 总结起来,Android 9.0的U-Boot Logo加载流程主要包括启动引导加载程序的初始化、配置加载、图像文件加载和屏幕显示等步骤。这样做的目的是为了在设备启动过程提供一个包含品牌标识或其他相关信息的界面,以增强用户体验和品牌识别。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值