Android 7.0 Doze模式分析

白名单机制

Android6.0及更高版本还提供了Doze模式白名单列表,通过设置应用程序进入白名单
列表可逃脱Doze模式的各种限制。检测应用程序是否存在白名单list里面,可使用
PowerManagerisIgnoringBatteryOptimizations()方法。
21
将某个应用加入白名单,可通过以下方法:
1Android6.0的原生设置中,用户能够在设置>电池>电池优化中进行手动配置白名单,
当然,用户也可以手动从此优化列表中移除app
2App能够启动带有ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGSintent,引
导用户到电池优化界面,让用户添加app到白名单里面或者删除;
3App拥有REQUEST_IGNORE_BATTERY_OPTIMIZATIONS权限,能够弹出一个系统对话
框 , 让 用 户 选 择 是 否 直 接 添 加app到 白 名 单 中 。 这 个app启 动 带 有
ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONSintent去弹出这个对话框。
deviceidle服务中有3种类型的白名单列表:
1、 仅doze模式下被限制的所有系统app,即仅用于doze
ArrayMap<String, Integer> mPowerSaveWhitelistAppsExceptIdle = new ArrayMap<>();
Stringapp包名
Integerapp id
该列表是deviceidle服务初始化时,取得SystemConfig.mAllowInPowerSaveExceptIdle
来赋值,即包含etc/permissions/platform.xmltagallow-in-power-save-except-idle
所有app
例如:
<allow-in-power-save-except-idle package="com.android.providers.downloads" />
2、 所有模式下被限制的所有系统app,即用于dozeapp standby
ArrayMap<String, Integer> mPowerSaveWhitelistApps = new ArrayMap<>();
该列表是deviceidle服务初始化时,取得SystemConfig.mPowerSaveWhitelistApps来赋值,
即包含etc/permissions/platform.xmltagallow-in-power-save的所有app
例如:
<allow-in-power-save package=" com.google.android.gms " />
3、 所有模式下被限制的所有用户app,即用于dozeapp standby
ArrayMap<String, Integer> mPowerSaveWhitelistUserApps = new ArrayMap<>();
包含data/system/deviceidle.xmltagwl的所有app
23类型中的appid将会当作powerManagerService中唤醒锁的白名单。

用户能够在设置>电池>电池优化中进行手动配置白名单,选择Don’t optimize 之后,会在自动生成 data/system/deviceidle.xml。 具体情况如下:



设置白名单

wakeLock白名单

在DeviceIdleController中我们调用PowerManagerService.java中的setDeviceIdleWhitelist设置白名单,这里的appids是uid

        @Override
        public void setDeviceIdleWhitelist(int[] appids) {
            setDeviceIdleWhitelistInternal(appids);
        }

继续调用setDeviceIdleWhitelistInternal,如果已经进入doze就调用updateWakeLockDisabledStatesLocked

    void setDeviceIdleWhitelistInternal(int[] appids) {
        synchronized (mLock) {
            mDeviceIdleWhitelist = appids;
            if (mDeviceIdleMode) {//doze模式
                updateWakeLockDisabledStatesLocked();
            }
        }
    }

updateWakeLockDisabledStatesLocked就是遍历所有的wakelock

    private void updateWakeLockDisabledStatesLocked() {
        boolean changed = false;
        final int numWakeLocks = mWakeLocks.size();
        for (int i = 0; i < numWakeLocks; i++) {
            final WakeLock wakeLock = mWakeLocks.get(i);
            if ((wakeLock.mFlags & PowerManager.WAKE_LOCK_LEVEL_MASK)
                    == PowerManager.PARTIAL_WAKE_LOCK) {
                if (setWakeLockDisabledStateLocked(wakeLock)) {
                    changed = true;
                    if (wakeLock.mDisabled) {
                        // This wake lock is no longer being respected.
                        notifyWakeLockReleasedLocked(wakeLock);
                    } else {
                        notifyWakeLockAcquiredLocked(wakeLock);
                    }
                }
            }
        }
        if (changed) {
            mDirty |= DIRTY_WAKE_LOCKS;
            updatePowerStateLocked();//更新power state
        }
    }

setWakeLockDisabledStateLocked就是决定是否disable某个wakelock,这个函数就是是否要将这个WakeLock置为disable

 private boolean setWakeLockDisabledStateLocked(WakeLock wakeLock) {
        if ((wakeLock.mFlags & PowerManager.WAKE_LOCK_LEVEL_MASK)
                == PowerManager.PARTIAL_WAKE_LOCK) {//cpu锁
            boolean disabled = false;
            if (mDeviceIdleMode) {
                final int appid = UserHandle.getAppId(wakeLock.mOwnerUid);
                // If we are in idle mode, we will ignore all partial wake locks that are
                // for application uids that are not whitelisted.
                if (appid >= Process.FIRST_APPLICATION_UID &&
                        Arrays.binarySearch(mDeviceIdleWhitelist, appid) < 0 &&//白名单、零时白名单中没有
                        Arrays.binarySearch(mDeviceIdleTempWhitelist, appid) < 0 &&
                        mUidState.get(wakeLock.mOwnerUid,
                                ActivityManager.PROCESS_STATE_CACHED_EMPTY)
                                > ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) {
                    disabled = true;
                }
            }
            if (wakeLock.mDisabled != disabled) {
                wakeLock.mDisabled = disabled;
                return true;
            }
        }
        return false;
    }


设置DOZE模式

设置Doze模式是通过PowerManagerService的如下接口

        @Override
        public boolean setDeviceIdleMode(boolean enabled) {
            return setDeviceIdleModeInternal(enabled);
        }
更新wakelock

boolean setDeviceIdleModeInternal(boolean enabled) {
        synchronized (mLock) {
            if (mDeviceIdleMode != enabled) {
                mDeviceIdleMode = enabled;
                updateWakeLockDisabledStatesLocked();
                if (enabled) {
                    EventLogTags.writeDeviceIdleOnPhase("power");
                } else {
                    EventLogTags.writeDeviceIdleOffPhase("power");
                }
                return true;
            }
            return false;
        }
    }

管理统计wakelock

在PowerManagerService中更新power的状态都是调用updatePowerStateLocked的,其中会调用updateWakeLockSummaryLocked来统计WakeLock

/**
     * Updates the value of mWakeLockSummary to summarize the state of all active wake locks.
     * Note that most wake-locks are ignored when the system is asleep.
     *
     * This function must have no other side-effects.
     */
    @SuppressWarnings("deprecation")
    private void updateWakeLockSummaryLocked(int dirty) {
        if ((dirty & (DIRTY_WAKE_LOCKS | DIRTY_WAKEFULNESS)) != 0) {
            mWakeLockSummary = 0;

            final int numWakeLocks = mWakeLocks.size();
            for (int i = 0; i < numWakeLocks; i++) {
                final WakeLock wakeLock = mWakeLocks.get(i);
                switch (wakeLock.mFlags & PowerManager.WAKE_LOCK_LEVEL_MASK) {
                    case PowerManager.PARTIAL_WAKE_LOCK:
                        if (!wakeLock.mDisabled) {
                            // We only respect this if the wake lock is not disabled.
                            mWakeLockSummary |= WAKE_LOCK_CPU;
                        }
                        break;
                    case PowerManager.FULL_WAKE_LOCK:
                        mWakeLockSummary |= WAKE_LOCK_SCREEN_BRIGHT | WAKE_LOCK_BUTTON_BRIGHT;
                        break;
                    case PowerManager.SCREEN_BRIGHT_WAKE_LOCK:
                        mWakeLockSummary |= WAKE_LOCK_SCREEN_BRIGHT;
                        break;
                    case PowerManager.SCREEN_DIM_WAKE_LOCK:
                        mWakeLockSummary |= WAKE_LOCK_SCREEN_DIM;
                        break;
                    case PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK:
                        mWakeLockSummary |= WAKE_LOCK_PROXIMITY_SCREEN_OFF;
                        break;
                    case PowerManager.DOZE_WAKE_LOCK:
                        mWakeLockSummary |= WAKE_LOCK_DOZE;
                        break;
                    case PowerManager.DRAW_WAKE_LOCK:
                        mWakeLockSummary |= WAKE_LOCK_DRAW;
                        break;
                }
            }

            // Cancel wake locks that make no sense based on the current state.
            if (mWakefulness != WAKEFULNESS_DOZING) {
                mWakeLockSummary &= ~(WAKE_LOCK_DOZE | WAKE_LOCK_DRAW);
            }
            if (mWakefulness == WAKEFULNESS_ASLEEP
                    || (mWakeLockSummary & WAKE_LOCK_DOZE) != 0) {
                mWakeLockSummary &= ~(WAKE_LOCK_SCREEN_BRIGHT | WAKE_LOCK_SCREEN_DIM
                        | WAKE_LOCK_BUTTON_BRIGHT);
                if (mWakefulness == WAKEFULNESS_ASLEEP) {
                    mWakeLockSummary &= ~WAKE_LOCK_PROXIMITY_SCREEN_OFF;
                }
            }

            // Infer implied wake locks where necessary based on the current state.
            if ((mWakeLockSummary & (WAKE_LOCK_SCREEN_BRIGHT | WAKE_LOCK_SCREEN_DIM)) != 0) {
                if (mWakefulness == WAKEFULNESS_AWAKE) {
                    mWakeLockSummary |= WAKE_LOCK_CPU | WAKE_LOCK_STAY_AWAKE;
                } else if (mWakefulness == WAKEFULNESS_DREAMING) {
                    mWakeLockSummary |= WAKE_LOCK_CPU;
                }
            }
            if ((mWakeLockSummary & WAKE_LOCK_DRAW) != 0) {
                mWakeLockSummary |= WAKE_LOCK_CPU;
            }

            if (DEBUG_SPEW) {
                Slog.d(TAG, "updateWakeLockSummaryLocked: mWakefulness="
                        + PowerManagerInternal.wakefulnessToString(mWakefulness)
                        + ", mWakeLockSummary=0x" + Integer.toHexString(mWakeLockSummary));
            }
        }
    }

总结:


WakeLock的管理逻辑很简单,会在DeviceIdleController设置白名单,然后进入Doze模式也会主动调用PowerManagerService接口设置状态,并且将相应的WakeLock的mDisabled置为true,让其无法持cpu锁。



Alarm白名单

AlarmManagerService.java  setDeviceIdleUserWhitelist设置alarm白名单

        public void setDeviceIdleUserWhitelist(int[] appids) {
            setDeviceIdleUserWhitelistImpl(appids);
        }


    void setDeviceIdleUserWhitelistImpl(int[] appids) {
        synchronized (mLock) {
            mDeviceIdleUserWhitelist = appids;
        }
    }

激活Alarm的Doze模式

    void scheduleAlarmLocked(long delay, boolean idleUntil) {
        if (DEBUG) Slog.d(TAG, "scheduleAlarmLocked(" + delay + ", " + idleUntil + ")");
        if (mMotionSensor == null) {
            // If there is no motion sensor on this device, then we won't schedule
            // alarms, because we can't determine if the device is not moving.  This effectively
            // turns off normal execution of device idling, although it is still possible to
            // manually poke it by pretending like the alarm is going off.
            return;
        }
        mNextAlarmTime = SystemClock.elapsedRealtime() + delay;
        if (idleUntil) {
            mAlarmManager.setIdleUntil(AlarmManager.ELAPSED_REALTIME_WAKEUP,
                    mNextAlarmTime, "DeviceIdleController.deep", mDeepAlarmListener, mHandler);
        } else {
            mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
                    mNextAlarmTime, "DeviceIdleController.deep", mDeepAlarmListener, mHandler);
        }
    }

 /**
     * Schedule an idle-until alarm, which will keep the alarm manager idle until
     * the given time.
     * @hide
     */
    public void setIdleUntil(int type, long triggerAtMillis, String tag, OnAlarmListener listener,
            Handler targetHandler) {
        setImpl(type, triggerAtMillis, WINDOW_EXACT, 0, FLAG_IDLE_UNTIL, null,
                listener, tag, targetHandler, null, null);
    }


AlarmManagerService.java中

    @Override  
    public void set(String callingPackage,  
            int type, long triggerAtTime, long windowLength, long interval, int flags,  
            PendingIntent operation, IAlarmListener directReceiver, String listenerTag,  
            WorkSource workSource, AlarmManager.AlarmClockInfo alarmClock) {  
        ......  
      
        // No incoming callers can request either WAKE_FROM_IDLE or  
        // ALLOW_WHILE_IDLE_UNRESTRICTED -- we will apply those later as appropriate.  
        flags &= ~(AlarmManager.FLAG_WAKE_FROM_IDLE//先把这两个flag清了  
                | AlarmManager.FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED);  
      
        // Only the system can use FLAG_IDLE_UNTIL -- this is used to tell the alarm  
        // manager when to come out of idle mode, which is only for DeviceIdleController.  
        if (callingUid != Process.SYSTEM_UID) {//不是system不允许有FLAG_IDLE_UNTIL,也就是调用setIdleUntil也无效  
            flags &= ~AlarmManager.FLAG_IDLE_UNTIL;  
        }  
      
        // If this is an exact time alarm, then it can't be batched with other alarms.  
        if (windowLength == AlarmManager.WINDOW_EXACT) {  
            flags |= AlarmManager.FLAG_STANDALONE;  
        }  
      
        // If this alarm is for an alarm clock, then it must be standalone and we will  
        // use it to wake early from idle if needed.  
        if (alarmClock != null) {  
            flags |= AlarmManager.FLAG_WAKE_FROM_IDLE | AlarmManager.FLAG_STANDALONE;//clock自带FLAG_WAKE_FROM_IDLE   
      
        // If the caller is a core system component or on the user's whitelist, and not calling  
        // to do work on behalf of someone else, then always set ALLOW_WHILE_IDLE_UNRESTRICTED.  
        // This means we will allow these alarms to go off as normal even while idle, with no  
        // timing restrictions.  
        } else if (workSource == null && (callingUid < Process.FIRST_APPLICATION_UID  
                || Arrays.binarySearch(mDeviceIdleUserWhitelist,  
                        UserHandle.getAppId(callingUid)) >= 0)) {  
            flags |= AlarmManager.FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED;//白名单  
            flags &= ~AlarmManager.FLAG_ALLOW_WHILE_IDLE;  
        }  
      
        setImpl(type, triggerAtTime, windowLength, interval, operation, directReceiver,  
                listenerTag, flags, workSource, alarmClock, callingUid, callingPackage);  
    }  

下面我们就看setImplLocked函数,看看和Doze模式相关的。如果是调用了setIdleUntil接口也就有FLAG_IDLE_UNTIL的flag,会调整期whenElapsed时间。然后设置mPendingIdleUntil变量代表已经进入Doze模式,当然进入Doze模式,肯定要rebatch所有的alarm。当有mPendingIdleUntil时(Doze模式),这个时候只有有FLAG_WAKE_FROM_IDLE、FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED、FLAG_ALLOW_WHILE_IDLE的flag的alarm才会被设置下去。其他的直接加入mPendingWhileIdleAlarms然后return了。自然设置的alarm就无效了。

    private void setImplLocked(Alarm a, boolean rebatching, boolean doValidate) {  
        if ((a.flags&AlarmManager.FLAG_IDLE_UNTIL) != 0) {//如果是该flag,会设置下alarm的whenElapsed  
            // This is a special alarm that will put the system into idle until it goes off.  
            // The caller has given the time they want this to happen at, however we need  
            // to pull that earlier if there are existing alarms that have requested to  
            // bring us out of idle at an earlier time.  
            if (mNextWakeFromIdle != null && a.whenElapsed > mNextWakeFromIdle.whenElapsed) {  
                a.when = a.whenElapsed = a.maxWhenElapsed = mNextWakeFromIdle.whenElapsed;  
            }  
            // Add fuzz to make the alarm go off some time before the actual desired time.  
            final long nowElapsed = SystemClock.elapsedRealtime();  
            final int fuzz = fuzzForDuration(a.whenElapsed-nowElapsed);  
            if (fuzz > 0) {  
                if (mRandom == null) {  
                    mRandom = new Random();  
                }  
                final int delta = mRandom.nextInt(fuzz);  
                a.whenElapsed -= delta;  
                if (false) {  
                    Slog.d(TAG, "Alarm when: " + a.whenElapsed);  
                    Slog.d(TAG, "Delta until alarm: " + (a.whenElapsed-nowElapsed));  
                    Slog.d(TAG, "Applied fuzz: " + fuzz);  
                    Slog.d(TAG, "Final delta: " + delta);  
                    Slog.d(TAG, "Final when: " + a.whenElapsed);  
                }  
                a.when = a.maxWhenElapsed = a.whenElapsed;  
            }  
      
        } else if (mPendingIdleUntil != null) {//有该变量代表之前调用过setIdleUntil接口了(进去Idle模式)  
            // We currently have an idle until alarm scheduled; if the new alarm has  
            // not explicitly stated it wants to run while idle, then put it on hold.  
            if ((a.flags&(AlarmManager.FLAG_ALLOW_WHILE_IDLE  
                    | AlarmManager.FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED  
                    | AlarmManager.FLAG_WAKE_FROM_IDLE))  
                    == 0) {  
                mPendingWhileIdleAlarms.add(a);//没有这些flag的alarm,加入mPendingWhileIdleAlarms直接退出  
                return;  
            }  
        }  
      
        int whichBatch = ((a.flags&AlarmManager.FLAG_STANDALONE) != 0)  
                ? -1 : attemptCoalesceLocked(a.whenElapsed, a.maxWhenElapsed);  
        if (whichBatch < 0) {  
            Batch batch = new Batch(a);  
            addBatchLocked(mAlarmBatches, batch);  
        } else {  
            Batch batch = mAlarmBatches.get(whichBatch);  
            if (batch.add(a)) {  
                // The start time of this batch advanced, so batch ordering may  
                // have just been broken.  Move it to where it now belongs.  
                mAlarmBatches.remove(whichBatch);  
                addBatchLocked(mAlarmBatches, batch);  
            }  
        }  
      
        if (a.alarmClock != null) {  
            mNextAlarmClockMayChange = true;  
        }  
      
        boolean needRebatch = false;  
      
        if ((a.flags&AlarmManager.FLAG_IDLE_UNTIL) != 0) {  
            mPendingIdleUntil = a;//设置mPendingIdleUntil代表进入Doze模式  
            mConstants.updateAllowWhileIdleMinTimeLocked();  
            needRebatch = true;//需要重新rebatch所有的alarm  
        } else if ((a.flags&AlarmManager.FLAG_WAKE_FROM_IDLE) != 0) {  
            if (mNextWakeFromIdle == null || mNextWakeFromIdle.whenElapsed > a.whenElapsed) {  
                mNextWakeFromIdle = a;  
                // If this wake from idle is earlier than whatever was previously scheduled,  
                // and we are currently idling, then we need to rebatch alarms in case the idle  
                // until time needs to be updated.  
                if (mPendingIdleUntil != null) {  
                    needRebatch = true;  
                }  
            }  
        }  
      
        if (!rebatching) {  
      
            if (needRebatch) {  
                rebatchAllAlarmsLocked(false);  
            }  
      
            rescheduleKernelAlarmsLocked();  
            updateNextAlarmClockLocked();  
        }  
    }  


设置alarm的白名单有FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED的flag,这个flag可以在Doze模式下继续设置alarm。

退出Doze模式

退出Doze模式有两种:

1.就是设置的setIdleUntil的alarm到时间了。

2.第二种就是自己主动删除的这个alarm

我们先来看看setIdleUntil的alarm时间到了的情况,在triggerAlarmsLocked函数(到期的alarm发送)有如下代码,如果发送的alarm是mPendingIdleUntil(也就是setIdleUntil的alarm),就把mPendingIdleUntil清除,然后重新rebatch所有的alarm,然后就是调用restorePendingWhileIdleAlarmsLocked函数,把之前没有设置的alarm(放在mPendingWhileIdleAlarms),重新设置alarm,然后把mPendingWhileIdleAlarms清零。

  1. if (mPendingIdleUntil == alarm) {  
  2.     mPendingIdleUntil = null;  
  3.     rebatchAllAlarmsLocked(false);  
  4.     restorePendingWhileIdleAlarmsLocked();  
  5. }  

我们来看下restorePendingWhileIdleAlarmsLocked函数

  1. void restorePendingWhileIdleAlarmsLocked() {  
  2.     // Bring pending alarms back into the main list.  
  3.     if (mPendingWhileIdleAlarms.size() > 0) {  
  4.         ArrayList<Alarm> alarms = mPendingWhileIdleAlarms;  
  5.         mPendingWhileIdleAlarms = new ArrayList<>();  
  6.         final long nowElapsed = SystemClock.elapsedRealtime();  
  7.         for (int i=alarms.size() - 1; i >= 0; i--) {  
  8.             Alarm a = alarms.get(i);  
  9.             reAddAlarmLocked(a, nowElapsed, false);//把mPendingWhileIdleAlarms重新设置  
  10.         }  
  11.     }  
  12.   
  13.     // Make sure we are using the correct ALLOW_WHILE_IDLE min time.  
  14.     mConstants.updateAllowWhileIdleMinTimeLocked();  
  15.   
  16.     // Reschedule everything.  
  17.     rescheduleKernelAlarmsLocked();  
  18.     updateNextAlarmClockLocked();  
  19.   
  20.     // And send a TIME_TICK right now, since it is important to get the UI updated.  
  21.     try {  
  22.         mTimeTickSender.send();  
  23.     } catch (PendingIntent.CanceledException e) {  
  24.     }  
  25. }  

还有一种情况是主动的删除了setIdleUntil设置的alarm,是调用了如下接口

  1. @Override  
  2. public void remove(PendingIntent operation, IAlarmListener listener) {  
  3.     if (operation == null && listener == null) {  
  4.         Slog.w(TAG, "remove() with no intent or listener");  
  5.         return;  
  6.     }  
  7.   
  8.     synchronized (mLock) {  
  9.         removeLocked(operation, listener);  
  10.     }  
  11. }  
removeLocked函数,先从mAlarmBatchs中删除,再从mPendingWhileIdleAlarms删除,如果从mAlarmBatchs中删除了alarm还要看删除的是否是mPendingIdleUntil,如果是要重新rebatch所有alarm,以及调用restorePendingWhileIdleAlarmsLocked函数恢复mPendingWhileIdleAlarms中的alarm。
  1. private void removeLocked(PendingIntent operation, IAlarmListener directReceiver) {  
  2.     boolean didRemove = false;  
  3.     for (int i = mAlarmBatches.size() - 1; i >= 0; i--) {  
  4.         Batch b = mAlarmBatches.get(i);  
  5.         didRemove |= b.remove(operation, directReceiver);  
  6.         if (b.size() == 0) {  
  7.             mAlarmBatches.remove(i);  
  8.         }  
  9.     }  
  10.     for (int i = mPendingWhileIdleAlarms.size() - 1; i >= 0; i--) {  
  11.         if (mPendingWhileIdleAlarms.get(i).matches(operation, directReceiver)) {  
  12.             // Don't set didRemove, since this doesn't impact the scheduled alarms.  
  13.             mPendingWhileIdleAlarms.remove(i);  
  14.         }  
  15.     }  
  16.   
  17.     if (didRemove) {  
  18.         boolean restorePending = false;  
  19.         if (mPendingIdleUntil != null && mPendingIdleUntil.matches(operation, directReceiver)) {  
  20.             mPendingIdleUntil = null;  
  21.             restorePending = true;  
  22.         }  
  23.         if (mNextWakeFromIdle != null && mNextWakeFromIdle.matches(operation, directReceiver)) {  
  24.             mNextWakeFromIdle = null;  
  25.         }  
  26.         rebatchAllAlarmsLocked(true);  
  27.         if (restorePending) {  
  28.             restorePendingWhileIdleAlarmsLocked();  
  29.         }  
  30.         updateNextAlarmClockLocked();  
  31.     }  
  32. }  
最后mPendingIdleUntil删除了,那么也就退出了Doze模式,所有的alarm设置下来都会正常了。



  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值