开机默认壁纸加载流程分析

原文:https://www.zybuluo.com/guhuizaifeiyang/note/866798

Android壁纸开发流程分析 
android壁纸服务流程浅析 
深入理解Android卷III 第八章深入理解Android壁纸


本文讨论的是开机默认壁纸的加载流程,这里只分析静态壁纸。静态壁纸是运行于SystemUI进程中的一个名为ImageWallpaper的特殊WallpaperService。

Android的壁纸功能的实现主要由散布在下面几个文件中的类来完成: 
// TODO 类图

  • frameworks/base/core/java/android/app/WallpaperManager.java 
    API类,提供了各种函数接口为应用开发者所使用。
  • frameworks/base/services/java/com/android/server/WallpaperManagerService.java 
    系统服务,用于管理壁纸的运行与切换。
  • frameworks/base/core/java/android/service/wallpaper/WallpaperService.java 
    同SystemUI一样,壁纸运行在一个Android服务之中,这个服务的名字叫做WallpaperService。当用户选择了一个壁纸之后,此壁纸所对应的WallpaperService便会启动并开始进行壁纸的绘制工作
  • frameworks/base/packages/SystemUI/src/com/android/systemui/ImageWallpaper.java 
    类ImageWallpaper实现了上面的WallpaperService。

下面是WallpaperManagerService的启动时序图: 
此处输入图片的描述

此处输入图片的描述

开机加载壁纸

Step 1. new WallpaperManagerService()

SystemServer.java->startOtherServices()

 
  1. try {
  2. Slog.i(TAG, "Wallpaper Service");
  3. wallpaper = new WallpaperManagerService(context);
  4. ServiceManager.addService(Context.WALLPAPER_SERVICE, wallpaper);
  5. } catch (Throwable e) {
  6. reportWtf("starting Wallpaper Service", e);
  7. }

WallpaperManagerService.java->WallpaperManagerService()

 
  1. public WallpaperManagerService(Context context) {
  2. mImageWallpaper = ComponentName.unflattenFromString(
  3. context.getResources().getString(R.string.image_wallpaper_component));
  4. // 获取WindowManagerService的实例
  5. mIWindowManager = IWindowManager.Stub.asInterface(
  6. ServiceManager.getService(Context.WINDOW_SERVICE));
  7. // ...
  8. getWallpaperDir(UserHandle.USER_OWNER).mkdirs();
  9. loadSettingsLocked(UserHandle.USER_OWNER);
  10. }
  11.  
  12. public static final @UserIdInt int USER_OWNER = 0;

Step 2. getWallpaperDir()

WallpaperManagerService.java->getWallpaperDir()

 
  1. private static File getWallpaperDir(int userId) {
  2. // 返回系统的用户目录
  3. return Environment.getUserSystemDirectory(userId);
  4. }

这里返回的结果是:/data/system/users/0。所以Step1中getWallpaperDir(UserHandle.USER_OWNER).mkdirs();就是创建了:/data/system/users/0/这个目录。

Step 3. loadSettingsLocked()

WallpaperManagerService.java->loadSettingsLocked()

 
  1. private void loadSettingsLocked(int userId) {
  2. // (1)
  3. JournaledFile journal = makeJournaledFile(userId);
  4. FileInputStream stream = null;
  5. File file = journal.chooseForRead();
  6. if (!file.exists()) {
  7. // This should only happen one time, when upgrading from a legacy system
  8. migrateFromOld();
  9. }
  10.  
  11. // (2)
  12. WallpaperData wallpaper = mWallpaperMap.get(userId);
  13. if (wallpaper == null) {
  14. wallpaper = new WallpaperData(userId);
  15. mWallpaperMap.put(userId, wallpaper);
  16. }
  17. boolean success = false;
  18.  
  19. // (3) 解析/data/system/users/0/wallpaper_info.xml文件
  20. // 省略
  21.  
  22. // 第一次启动时,wallpaer_info.xml不存在,也就不采用其里面的值。这时调用getMaximumSizeDimension来赋值。
  23. int baseSize = getMaximumSizeDimension();
  24. if (wallpaper.width < baseSize) {
  25. wallpaper.width = baseSize;
  26. }
  27. if (wallpaper.height < baseSize) {
  28. wallpaper.height = baseSize;
  29. }
  30. }

载入系统保存的配置,这里也是分三步:

1.用JournaledFile这个工具类封装/data/system/users/0/wallpaper_info.xml文件。这个工具类会包装两个文件,一个是wallpaper_info.xml正式文件,另一个是wallpaper_info.xml.tmp临时文件。如果正式文件存在就选出正式文件,并删除临时文件;如果正式文件不存在就将临时文件重名为正式文件。

 
  1. private static JournaledFile makeJournaledFile(int userId) {
  2. final String base = new File(getWallpaperDir(userId), WALLPAPER_INFO).getAbsolutePath();
  3. return new JournaledFile(new File(base), new File(base + ".tmp"));
  4. }
  5. static final String WALLPAPER_INFO = "wallpaper_info.xml";

2.创建一个WallpaperData并存入mWallpaperMap ,我们可以看看WallpaperData 的构造函数,这是一个内部类:

 
  1. static final String WALLPAPER = "wallpaper";
  2. WallpaperData(int userId) {//0
  3. this.userId = userId;
  4. //位于/data/system/users/0/wallpaper,是存放壁纸的文件
  5. wallpaperFile = new File(getWallpaperDir(userId), WALLPAPER);
  6. }

WallpaperData 的构造方法主要是创建了一个/data/system/users/0/wallpaper的File对象。 
3.最后就是解析/data/system/users/0/wallpaper_info.xml文件

Android系统的壁纸信息存放在/data/system/users/0/目录下,WallpaperManagerService启动后,会生成如下两个文件在/data/system/users/0/目录下:

 
  1. static final String WALLPAPER = wallpaper; //设置的壁纸图片,一般为jpeg格式
  2. static final String WALLPAPER_INFO = wallpaper_info.xml; //包含墙纸的规格信息:高、宽

Wallpaper_info.xml的解析可以查看WallpaperManagerService的loadSettingsLocked()方法。

 
  1. <?xml version='1.0' encoding='utf-8' standalone='yes' ?>
  2. <wp id="1" width="720" height="1280" cropLeft="0" cropTop="0" cropRight="1280" cropBottom="1280" name="" backup="true" />
  3. <kwp id="2" width="1280" height="1280" cropLeft="0" cropTop="0" cropRight="0" cropBottom="0" name="" />

第一次启动时,wallpaer_info.xml不存在,也就不采用其里面的值。这时,则采用的是getMaximumSizeDimension,也就是背景图的宽和高由getMaximumSizeDimension决定。

分析到这里,我们看到,除了创建了WallpaperData,并没有其它和加载壁纸相关的操作。并且,这里创建的WallpaperData还未填充壁纸的有效信息。 
所以,接下来我们从另外一条线索开始分析。壁纸经常伴随着创建、移动、切换、删除等操作,那么肯定会有一个监听机制来监听壁纸的这一系列动作。于是,我们从WallpaperManagerService类的systemRunning方法中找到了一个WallpaperObserver类。

Step 4. wallpaperF.systemRunning()

SystemServer.java->startOtherServices()

 
  1. try {
  2. if (wallpaperF != null) wallpaperF.systemRunning();
  3. } catch (Throwable e) {
  4. reportWtf("Notifying WallpaperService running", e);
  5. }

WallpaperManagerService.java->systemRunning()

 
  1. public void systemRunning() {
  2. if (DEBUG) Slog.v(TAG, "systemReady");
  3. WallpaperData wallpaper = mWallpaperMap.get(UserHandle.USER_OWNER);
  4. switchWallpaper(wallpaper, null);
  5. wallpaper.wallpaperObserver = new WallpaperObserver(wallpaper);
  6. wallpaper.wallpaperObserver.startWatching();
  7. // ...
  8. }

wallpaperObserver是监听壁纸变化的重要组件,后面会详细介绍。

Step 5. switchWallpaper()

WallpaperManagerService.java->switchWallpaper()

 
  1. void switchWallpaper(WallpaperData wallpaper, IRemoteCallback reply) {
  2. synchronized (mLock) {
  3. RuntimeException e = null;
  4. try {
  5. ComponentName cname = wallpaper.wallpaperComponent != null ? wallpaper.wallpaperComponent : wallpaper.nextWallpaperComponent;
  6. if (bindWallpaperComponentLocked(cname, true, false, wallpaper, reply)) {
  7. return;
  8. }
  9. } catch (RuntimeException e1) {
  10. e = e1;
  11. }
  12. Slog.w(TAG, "Failure starting previous wallpaper", e);
  13. clearWallpaperLocked(false, wallpaper.userId, reply);
  14. }
  15. }

switchWallpaper中正常执行到bindWallpaperComponentLocked就会返回了,bindWallpaperComponentLocked的工作是启动一个新的壁纸服务并终止旧的壁纸服务,只有启动壁纸异常的情况下才会执行下面的clearWallpaperLocked清理操作。

Step 6. WallpaperManagerService&WallpaperObserver

WallpaperObserver是一个内部类,用来监听壁纸的变化。首次设置壁纸时会调用CREATE事件,CLOSE_WRITE事件在壁纸每次变化时都会被调用。

 
  1. private class WallpaperObserver extends FileObserver {
  2. final WallpaperData mWallpaper;
  3. final File mWallpaperDir;
  4. final File mWallpaperFile;
  5. final File mWallpaperInfoFile;
  6.  
  7. public WallpaperObserver(WallpaperData wallpaper) {
  8. // 监听/data/system/users/0/下CLOSE_WRITE | MOVED_TO | DELETE | DELETE_SELF几个事件
  9. super(getWallpaperDir(wallpaper.userId).getAbsolutePath(),
  10. CLOSE_WRITE | MOVED_TO | DELETE | DELETE_SELF);
  11. mWallpaperDir = getWallpaperDir(wallpaper.userId);
  12. mWallpaper = wallpaper;
  13. mWallpaperFile = new File(mWallpaperDir, WALLPAPER);
  14. mWallpaperInfoFile = new File(mWallpaperDir, WALLPAPER_INFO);
  15. }
  16.  
  17. @Override
  18. public void onEvent(int event, String path) {
  19. if (path == null) {
  20. return;
  21. }
  22. synchronized (mLock) {
  23. // ...
  24. // 绑定壁纸组件
  25. File changedFile = new File(mWallpaperDir, path);
  26. if (mWallpaperFile.equals(changedFile)) {
  27. bindWallpaperComponentLocked(mImageWallpaper, true,
  28. false, mWallpaper, null);
  29. // 保存壁纸信息,和loadSettingsLocked相反。
  30. saveSettingsLocked(mWallpaper);
  31. }
  32. }
  33. }
  34. }
  35.  
  36. 当/data/system/users/0/wallpaper发生改动时,则需要绑定壁纸组件,调用bindWallpaperComponentLocked方法。这里mImageWallpaper是个ComponentName,指向com.android.systemui/com.android.systemui.ImageWallpaper。

Step 7. bindWallpaperComponentLocked()

bindWallpaperComponentLocked()方法将会启动由ComponentName所指定的WallpaperService,并向WMS申请用于加入壁纸窗体的窗体令牌。

 
  1. boolean bindWallpaperComponentLocked(ComponentName componentName, boolean force,
  2. boolean fromUser, WallpaperData wallpaper, IRemoteCallback reply) {
  3. // ...
  4. try {
  5. /* 当componentName为null时表示使用默认壁纸。
  6. 这里会将componentName參数改为默认壁纸的componentName */
  7. if (componentName == null) {
  8. /* 首先会尝试从com.android.internal.R.string.default_wallpaper_component
  9. 中获取默认壁纸的componentName,这个值的设置位于res/values/config.xml中。*/
  10. // 这里获取的componentName=null
  11. componentName = WallpaperManager.getDefaultWallpaperComponent(mContext);
  12. if (componentName == null) {
  13. // 使用静态壁纸
  14. // Fall back to static image wallpaper
  15. componentName = mImageWallpaper;
  16. if (DEBUG) Slog.v(TAG, "Using image wallpaper");
  17. }
  18. }
  19.  
  20. // ...
  21. // 对ComponentName所描写叙述的Service进行一系列的验证,以确保它是一个壁纸服务。
  22.  
  23. // WallpaperConnection实现了ServiceConnection接口用于监听和WallpaperService之间的连接状态。
  24. WallpaperConnection newConn = new WallpaperConnection(wi, wallpaper);
  25. intent.setComponent(componentName);
  26. // ...
  27.  
  28. /* 启动指定的壁纸服务。当服务启动完毕后,剩下的启动流程会在WallpaperConnection.onServiceConnected()中继续 */
  29. if (!mContext.bindServiceAsUser(intent, newConn,
  30. Context.BIND_AUTO_CREATE | Context.BIND_SHOWING_UI
  31. | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE,
  32. new UserHandle(serviceUserId))) {
  33. // ...
  34. }
  35. // 新的的壁纸服务启动成功后。便通过detachWallpaperLocked()销毁旧有的壁纸服务
  36. if (wallpaper.userId == mCurrentUserId && mLastWallpaper != null) {
  37. detachWallpaperLocked(mLastWallpaper);
  38. }
  39.  
  40. // 将新的壁纸服务的执行信息保存到WallpaperData中
  41. wallpaper.wallpaperComponent = componentName;
  42. wallpaper.connection = newConn;
  43.  
  44. // 最后向WMS申请注冊一个WALLPAPER类型的窗体令牌。
  45. mIWindowManager.addWindowToken(newConn.mToken,
  46. WindowManager.LayoutParams.TYPE_WALLPAPER);
  47. }

Step 8. onServiceConnected()

WallpaperManagerService.java$WallpaperConnection->onServiceConnected()

 
  1. @Override
  2. public void onServiceConnected(ComponentName name, IBinder service) {
  3. synchronized (mLock) {
  4. if (mWallpaper.connection == this) {
  5. // ImageWallpaper的onBind方法返回值就是这个mService
  6. mService = IWallpaperService.Stub.asInterface(service);
  7. attachServiceLocked(this, mWallpaper);
  8. saveSettingsLocked(mWallpaper);
  9. }
  10. }
  11. }

WallpaperConnection它不仅实现了ServiceConnection接口用于监听和WallpaperService之间的连接状态。在服务绑定成功后的WallpaperConnection.onServiceConnected()方法调用中,WallpaperConnection的实例会被发送给WallpaperService,使其作为WallpaperService向WallpaperManagerService进行通信的桥梁。

Step 9. attachServiceLocked()

WallpaperManagerService.java->attachServiceLocked()

 
  1. void attachServiceLocked(WallpaperConnection conn, WallpaperData wallpaper) {
  2. try {
  3. // 这里会调用IWallpaperServiceWrapper的attach方法,将创建壁纸窗体所需的信息发送给壁纸服务。
  4. conn.mService.attach(conn, conn.mToken,
  5. WindowManager.LayoutParams.TYPE_WALLPAPER, false,
  6. wallpaper.width, wallpaper.height, wallpaper.padding);
  7. } catch (RemoteException e) {
  8. // ...
  9. }
  10. }
  11. }

Step 10. attach()

WallpaperService$IWallpaperServiceWrapper->attach

 
  1. public void attach(IWallpaperConnection conn, IBinder windowToken,
  2. int windowType, boolean isPreview, int reqWidth, int reqHeight, Rect padding) {
  3. // 使用WallpaperManagerService提供的參数构造一个IWallpaperEngineWarapper实例
  4. new IWallpaperEngineWrapper(mTarget, conn, windowToken,
  5. windowType, isPreview, reqWidth, reqHeight, padding);
  6. }
  7.  
  8. IWallpaperEngineWrapper(WallpaperService context,
  9. IWallpaperConnection conn, IBinder windowToken,
  10. int windowType, boolean isPreview, int reqWidth, int reqHeight, Rect padding) {
  11. // 类成员变量初始化
  12. // ...
  13.  
  14. Message msg = mCaller.obtainMessage(DO_ATTACH);
  15. mCaller.sendMessage(msg);
  16. }

接下来分析DO_ATTACH消息的处理:

Step 11. executeMessage()

WallpaperService$IWallpaperEngineWrapper->executeMessage()

 
  1. public void executeMessage(Message message) {
  2. switch (message.what) {
  3. case DO_ATTACH: {
  4. try {
  5. // 把IWallpaperEngineWrapper实例传递给WallpaperConnection进行保存。
  6. mConnection.attachEngine(this);
  7. } catch (RemoteException e) {
  8. Log.w(TAG, "Wallpaper host disappeared", e);
  9. return;
  10. }
  11. // 通过onCreateEngine()方法创建一个Engine。WallpaperService仅仅是提供壁纸执行的场所,而Engine才是真正的壁纸的实现
  12. Engine engine = onCreateEngine();
  13. mEngine = engine;
  14. mActiveEngines.add(engine);
  15. engine.attach(this);
  16. return;
  17. }
  18. // ...
  19. }
  20. }

Step 12. onCreateEngine()

ImageWallpaper.java->onCreateEngine():

WallpaperService类中的onCreateEngine方法是一个抽象方法,ImageWallpaper类中实现了此方法。回想一下,在bindWallpaperComponentLocked方法中我们绑定的就是ImageWallpaper服务。

 
  1. @Override
  2. public Engine onCreateEngine() {
  3. // 创建了一个DrawableEngine实例
  4. mEngine = new DrawableEngine();
  5. return mEngine;
  6. }

Step 13. WallpaperService$Engine->attach()

Engine创建完毕之后会通过Engine.attach()方法完毕Engine的初始化工作。

 
  1. void attach(IWallpaperEngineWrapper wrapper) {
  2. // ...
  3. // ...
  4. // Engine的初始化工作
  5. onCreate(mSurfaceHolder);
  6.  
  7. mInitializing = false;
  8. mReportedVisible = false;
  9. // 绘制壁纸
  10. updateSurface(false, false, false);
  11. }

Step 14. updateSurfaceSize()

ImageWallpaper$DrawableEngine->updateSurfaceSize():

 
  1. @Override
  2. public void onCreate(SurfaceHolder surfaceHolder) {
  3. // ...
  4. updateSurfaceSize(surfaceHolder, getDefaultDisplayInfo());
  5. }
  6.  
  7.  
  8. void updateSurfaceSize(SurfaceHolder surfaceHolder, DisplayInfo displayInfo) {
  9. // ...
  10. // 将mWallpaper和mDefaultWallpaper重置为null
  11. WallpaperManager.forgetLoadedWallpaper();
  12. updateWallpaperLocked();
  13. ...
  14. }
  15.  
  16. private void updateWallpaperLocked() {
  17. // ...
  18.  
  19. mBackground = mWallpaperManager.getBitmap();
  20. // ...
  21. }
  22.  
  23. public Bitmap getBitmap() {
  24. return sGlobals.peekWallpaperBitmap(mContext, true);
  25. }
  26.  
  27. public Bitmap peekWallpaperBitmap(Context context, boolean returnDefault) {
  28. synchronized (this) {
  29. // 如果已经加载了壁纸,则直接返回。
  30. if (mWallpaper != null) {
  31. return mWallpaper;
  32. }
  33. // 如果没有加载壁纸,则返回默认壁纸。
  34. if (mDefaultWallpaper != null) {
  35. return mDefaultWallpaper;
  36. }
  37. mWallpaper = null;
  38. try {
  39. // 首次开机,这里获取的wallpaper=null
  40. mWallpaper = getCurrentWallpaperLocked(context);
  41. } catch (OutOfMemoryError e) {
  42. Log.w(TAG, "No memory load current wallpaper", e);
  43. }
  44. if (returnDefault) {
  45. if (mWallpaper == null) {
  46. mDefaultWallpaper = getDefaultWallpaperLocked(context);
  47. return mDefaultWallpaper;
  48. } else {
  49. mDefaultWallpaper = null;
  50. }
  51. }
  52. return mWallpaper;
  53. }
  54. }

Step 15. getCurrentWallpaperLocked

 
  1. private Bitmap getCurrentWallpaperLocked(Context context) {
  2. if (mService == null) {
  3. Log.w(TAG, "WallpaperService not running");
  4. return null;
  5. }
  6.  
  7. try {
  8. Bundle params = new Bundle();
  9. // 获取/data/system/users/0/wallpaper的文件描述符
  10. ParcelFileDescriptor fd = mService.getWallpaper(this, params);
  11. if (fd != null) {
  12. try {
  13. BitmapFactory.Options options = new BitmapFactory.Options();
  14. return BitmapFactory.decodeFileDescriptor(
  15. fd.getFileDescriptor(), null, options);
  16. } catch (OutOfMemoryError e) {
  17. Log.w(TAG, "Can't decode file", e);
  18. } finally {
  19. try {
  20. fd.close();
  21. } catch (IOException e) {
  22. // Ignore
  23. }
  24. }
  25. }
  26. } catch (RemoteException e) {
  27. // Ignore
  28. }
  29. return null;
  30. }

Step 16. getWallpaper()

WallpaperManagerService.java->getWallpaper()

 
  1. public ParcelFileDescriptor getWallpaper(IWallpaperManagerCallback cb,
  2. Bundle outParams) {
  3. synchronized (mLock) {
  4. // ...
  5. // 从mWallpaperMap中获取对应用户的WallpapaerData
  6. WallpaperData wallpaper = mWallpaperMap.get(wallpaperUserId);
  7. if (wallpaper == null) {
  8. return null;
  9. }
  10.  
  11. // 从WallpaperData中获取壁纸的尺寸信息并保存在outParms中
  12. try {
  13. if (outParams != null) {
  14. outParams.putInt("width", wallpaper.width);
  15. outParams.putInt("height", wallpaper.height);
  16. }
  17. wallpaper.callbacks.register(cb);
  18. File f = new File(getWallpaperDir(wallpaperUserId), WALLPAPER);
  19. if (!f.exists()) {
  20. return null;
  21. }
  22. // 将文件包装为一个文件描述符并返回给调用者
  23. return ParcelFileDescriptor.open(f, MODE_READ_ONLY);
  24. } catch (FileNotFoundException e) {
  25. /* Shouldn't happen as we check to see if the file exists */
  26. Slog.w(TAG, "Error getting wallpaper", e);
  27. }
  28. return null;
  29. }
  30. }

Step 17. getDefaultWallpaperLocked()

 
  1. private Bitmap getDefaultWallpaperLocked(Context context) {
  2. InputStream is = openDefaultWallpaper(context);
  3. // ...
  4. }
  5.  
  6. public static InputStream openDefaultWallpaper(Context context) {
  7. // 配置wallpaper路径
  8. // ...
  9.  
  10. return context.getResources().openRawResource(
  11. com.android.internal.R.drawable.default_wallpaper);
  12. }

Step 18. updateSurface()

WallpaperService$Engine->updateSurface():

Engine的updateSurface()方法将会创建壁纸窗体及Surface,并进行壁纸的绘制。


案例分析

// Question 
根据机器颜色加载相应的壁纸

// Solution 
WallpaperManager.java->getDefaultWallpaperLocked:

 
  1. private Bitmap getDefaultWallpaperLocked(Context context) {
  2. try {
  3. InputStream is = context.getResources().openRawResource( com.android.internal.R.drawable.default_wallpaper);
  4. }

直接修改com.android.internal.R.drawable.default_wallpaper。

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Android 开机动画的流程如下: 1. 开机自检:开机后,首先进行硬件自检。如果自检通过,将会启动 bootloader。 2. Bootloader:Bootloader 负责初始化硬件设备, kernel,并且将 kernel 到内存中。 3. Kernel:Kernel 是 Android 操作系统的核心,负责初始化硬件设备,启动驱动程序,文件系统等。 4. Init 进程:Init 进程是 Android 系统中的第一个用户进程,它负责启动系统中所有的服务,并且所有的配置文件和属性。 5. SurfaceFlinger:SurfaceFlinger 是 Android 系统中用于显示图形的核心服务,它在启动后会创建一个显示屏幕,显示屏幕上的内容就是从应用程序中传递过来的。 6. Zygote 进程:Zygote 进程是 Android 系统中的一个特殊进程,它负责预常用的类和资源,以提高应用程序的启动速度。当应用程序需要启动时,Zygote 进程会 fork 出一个新的进程,该进程会继承 Zygote 进程的状态,从而速应用程序的启动。 7. 开机动画:在上述进程启动后,系统会开机动画。开机动画通常是一个视频或者一组图片,这些图片或视频会被 SurfaceFlinger 显示在屏幕上。 8. 启动屏幕:当开机动画结束后,系统会显示一个启动屏幕,表示 Android 系统已经启动完毕。此时,用户就可以开始使用 Android 设备了。 总的来说,Android 开机动画流程比较复杂,其中涉及到多个进程和服务,需要相互配合才能完成整个过程。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值