掌阅群分享技术点收集(app性能优化专攻)

保活 先从老式最基础的开始:

  • 使用startService方式启动一个独立进程的服务,这样系统会在service意外死亡后自动重启。
  • 使用RTC定时闹钟每5分钟检测一下(4.0以上基本无效)
  • 启动linux守护进程,每几分钟检测一下进程是否存在,不存在就startService(5.0以下除MIUI和华为外有效)
  • 5.0以上使用JobScheduler代替闹钟定时检测启动 。
  • 启动隐藏的前台通知。(支付宝即采用该方式,为系统的一个bug,在7.1.1中已修复,具体体现为下拉任务栏可以看到该通知。)但这些措施都不能100%保活。尤其是M中引入的doze模式,在doze模式中甚至无网络连接。
  • 官方建议:1.引导用户加入白名单;2.使用Google系列服务如GCM,Firebase

内存三级缓存思路 对于列表页中的图片应使用LRU之类的内存缓存,对于activity和fragment销毁时应该把其对应的图片释放掉。每个图片都是有依赖引用的,我们一般默认是fragment或者是activity,当activity或者fragment销毁时,我们会将只依赖当前页面的图片移出LRU进行释放。

  • 内存图片缓存还是有三级:一级是bitmap列表,一级是LRU强引用,一级是弱引用。加载图片时,先在弱引用缓存和LRU强引用中查找没有被释放的bitmap,如果存在并满足复用条件,则不创建bitmap,而使用该图片内存,之后将其从弱引用或者LRU中移出放入bitmap列表强引用中。bitmap列表强引用保存了该图片依赖的直接控件。当view的onDetachedFromWindow被调用则从bitmap列表中移除只有依赖该view的对象到LRU强应用中。如果LRU强引用满了则放到弱引用中。
  • 这种方案可以达到如下效果:列表滑动、或者打开新页面时会申请较小的内存或不申请内存。列表页来回滑动时依然有缓存可用。
  • 对于adapterView和recycleView中的控件则需单独写个工具方法来封装一下。
  • 这里实际上已经不需要弱引用了,只要从LRU中移除即可调用recycle方法释放该图片。
  • 图片内存的申请和释放都由框架来控制,不由gc管理。
  • LRU的大小就是可调,它里面的图片内存是都可以释放的,它只是作为缓存而存在。

卡顿问题快速定位的方法GPU monitor分析

  • 打开开发者模式中GPU呈现模式分析,查看是那种颜色条高
  • 如果是蓝色偏高,说明是单位消息里CPU太耗时,得把方法的执行都打出来看看哪个耗时。比如,在某处先看看是不是应该出现onMeasure,然后可以通过sdk自带的View布局工具,看一下哪个View的onMeasure耗时最多。
  • 如果红色偏高,说明GPU忙不过来。优化过渡绘制,使用离屏缓存来优化。
  • 黄色偏高,说明半透明GPU不仅在忙着绘制你的window也还忙着绘制别的,可能的情况为透明window叠加多了,window里的contentView有多个且相对复杂,或者GPU降频了等等,想具体分析需要查看GPU的trace。
  • 画动画时蓝色偏高是不正常的

**蓝色偏高的常见原因:**1. 动画或者交互时缓存失效的太多,验证方法是打出方法trace看看是不是有很多次的invalidate调用和dispatchDraw耗时在前面。2. 触发了GC,验证看看trace中主线程是不是被莫名的暂停了。3. 触发了layout,这种蓝色会很高,trace中measure方法会耗时较高。

如何优化启动速度 没有闪屏页activity,一旦存在闪屏页activity那么启动速度就不大可能在200ms内跳过。 把window的背景设置为闪屏页,一旦MainActivity加载完毕就显示主页了。 虽然用户也会看到一个类似的闪屏页,但那个闪屏页实际只是activity在theme中设置的background。 之前好像有人问我怎么优化启动速度。这个方案适合启动画面不是作为广告页只是过渡页使用的场景。

<item name="android:windowBackground">@drawable/splash_logo</item>
复制代码

自定义高性能可拖动GridView 在拖动过程中没有触发过invalidate,也没有触发requestLayout,别的应用每次移动动画会触发notifyDateChange,这会触发layout,影响性能。我们在拖动过程中会建立虚拟的视觉关系,只要不松手就会改变子view的顺序,只有松手才触发datechange。 提升性能绘制方法

  • 调用方法把它移出可见区域,移出可见区域后,在进行绘制的时候 native 层也不会去绘制它。

TextView优化

  • 列表直接不用textview,自定义view,通过提前缓存 StaticLayout提升TextView性能,可参考:http://ragnraok.github.io/textview-pre-render-research.html

Fragment留意点 每个页面都是Fragment,自己管理其生命周期和栈,每次启动是以window的方式添加进来,进入动画为window动画,手势回退为View动画,为了节省内存,页面栈只保留2个对象,FragmentManager会进行回收释放和Fragment的恢复。

  • 优点:加载动画非常流畅,内存占用低,支持页面的无穷层级叠加。
  • 缺点:以Window方式启动,很多系统的特性需要自己实现,难以驾驭。

状态栏兼容注意点 针对状态栏我们单独适配了4.4以上版本,5.0以上版本,6.0以上版本,Flyme系统,6.0以上的Flyme系统,MIUI系统,6.0以上的MIUI系统,YunOS系统,VIVO Funtouch 2.5以下版本和2.5以上版本.......

try-catch

  • 如果你的代码一定会抛出异常,那try catch会有一些影响。
  • 一旦捕获到异常,系统打到寄存器中获取当前函数调用栈,生成一堆信息,这总归是浪费性能的。
  • 所以说一般不影响,非要扣那肯定影响,本来get方法可以进行内联的,用来try catch肯定就不能进行内联优化了,就会让性能下降一点。
  • android这几个版本推出了JIT,art虚拟机中重点对内联函数的范围进行扩充,try catch会阻止这些优化。
  • 在android 5.0以上(ART进行OPT优化时)所有函数中存在try catch的方法都不能被JIT优化和进行内联优化

查看应用真实内存

  • adb shell dumpsys meminfo 你的包名 (monitor中不会显示WebView的内存占用)

  • (注意点)开了线程来执行耗时操作,可是这耗时操作执行的时候把主线的CPU占用给抢了。。。。

高绘制性能函数 看一个ListView的函数

offsetChildrenTopAndBottom(int index)
复制代码

这个方法性能很高,但是隐藏方法,listView移动子控件是用这个方法来移动的,它不破坏缓存,系统相当于是做了1+1+1+2,我们自己做就是1+2+1+2+1+2,我们自己写ListView的时候发现了这个函数,我们做for循环的性能还是不如系统的这个隐藏方法。

使用windowManager的addView来添加控件 例子:通过调用Fragment的onCreateView来生成一个View,然后addView进来,这导致跳转界面需要较长时间。现在得先addView一个View到windowManager中,然后在调用onCreateView,因为windowManager添加控件是在server进程,所以会立即addView进来,这时这里的View就需要显示点东西,这就需要类似windowBackground的东西来显示。windowBackground它存在的目的就是为了加快界面响应。

实现自己的windowbackground 先add一个类似decoreView进来,设置decoreView的background为windowBackground,然后在往这个decoreView上添加实际的控件。

避免使用LayoutParams实现动画

  • setLayoutParams()会触发requestLayout()从而导致所有View重新measure、layout、draw,导致卡顿。 排查方法:可以用布局边界排查大小变化的。 例:爱范儿 下拉刷新。(排查方法为:重写顶端控件的requestLayout方法,打上断点,看看动画或者交互过程中谁调用到了这个方法)

  • 还有查看谁刷新了页面导致重绘的排查方法是:重写顶端控件的invalidateChildInParent方法,看看谁调用到了。

使用硬件离屏缓存进行优化。(要保证缓存不失效)

正确的使用:显示硬件层更新绿色闪一下。

错误的使用:过程中一直绿色。

错误例:微信大图。

正确例:掌阅首页切换
复制代码
  • 硬件加速本质上是属于window级别的东西,在创建ViewRootImpl的时候就确定了是否使用硬件加速,View级别所谓的关闭只是创建一张bitmap然后调用View的draw方法往这个上面绘制,绘制完成再往硬件加速的canvas上绘制。
  • 系统对OpenGl方法进行了封装和优化,封装实现了canvas的方法,使用它有的时候比直接使用OpenGl性能还好。所以开硬件加速几乎等效于调用OpenGl接口来绘制,OpenGl是通用绘制接口,一般GPU都会实现这些接口,所以硬件加速是让GPU来绘制,而非硬件加速就是CPU自己绘制。
  • CPU要实现那么多的通用计算,而GPU就那么几个简单接口,它就极端优化,所以这几个简单方法的性能非常高。(OpenGl标准方法创建纹理很耗时,一张1080p的全屏图需要40ms以上,而android系统自己私有的方法10ms以内就创建完毕了)
  • Opengl创建纹理(texture)太耗时,后面使用比系统的速度快,系统被它那套递归绘制等拖累了性能。(opengl来实现ViewPager的效果,android2.2手机除了初始化创建交互的纹理,进行移动的时候8ms左右一帧。)

主动释放控件资源

	//释放布局资源
   private void releaseDradable(View view){
        if (view instanceof ViewGroup) {
        int count = ((ViewGroup)view).getChildCount();
        for (int i=0;i<count;i++) {
             View childView = ((ViewGroup)view).getChildAt(i);
             if (childView instanceof ViewGroup) {
                  releaseDradable(childView);
               }else {
                releaseOneDrawable(childView);
              }
          }
         }else {
            releaseOneDrawable(view);
        }
  }
//释放常见控件的资源,imageView是多种控件的根类
private void releaseOneDrawable(View view){
    if (view !=null) {
        BitmapDrawable src;
        BitmapDrawable backGround;
        if (view instanceof ImageView) {
            src = (BitmapDrawable) ((ImageView) view).getDrawable();
            recycleBitmap(src.getBitmap());
        }
        backGround = (BitmapDrawable) view.getBackground();
        recycleBitmap(backGround.getBitmap());
    }
}
private  void recycleBitmap(Bitmap bitmap) {
    if (bitmap != null && !bitmap.isRecycled()) {
        bitmap.recycle();
        bitmap = null;    }
}
复制代码

以上思路存在一个问题,即当某一个资源被多个activity引用时,回收该资源则会造成其他持有该资源的activity发生异常。

  • 维护一个Drawable链表用以记录引用次数

  • 将控件的getDrawable()和getBackground()设置为null

系统Viewpager的性能优化

  • V4包里的SwipeRefreshLayout类在接收到down事件的时候,会调用bringToFront方法,该方法会触发requestLayout。

*这里主要是优化ViewPager在添加和删除item的时候,会触发requestLayout导致的卡顿问题。第一次加载是没有优化的,因为必须得触发layout。 * 经过分析,我们的场景不需要这个方法,就去掉了该方法, viewPager的adapter中instantiateItem()会执行container.addView(object),这也会触发requestLayout;destroyItem也会触发requestLayout。可以替换为attachViewToParent和detachViewFromParent方法来进行add和remove。这俩方法是listView中进行动态add和remove的方法,性能很高,不会让缓存失效和触发requestLayout。

我们的ViewPager的setOffscreenPageLimit设置为1。调用detachViewFromParent方法后为了让ViewPager重新录制一下View的绘制,所以又手动调用了invalidate。 录制绘制就是dispatchDraw流程,不然会走getDisplayList流程

在 instantateItem()中调用如下代码

微信db打开方式 用户设备的IMEI+uin值计算MD5值,注意是小写字符,然后在取MD5的前7位字符构成的密码。

关于RelativeLayout的使用 大量使用了RelativeLayout,导致了多次mesure,一个relativelayout都要measure两次,多个层次这种叠加之后,measure次数指数级上升。

关于 SoftwareRefrence 在android低版本上,SoftwareRefrence是遵循java标准的GC回收流程,即只有触发GC的情况为内存不足时,才会去检查SoftReference,但在高版本上,SoftReference被检查的更频繁了,即不是只有内存不足时才去检查,其存在的概率与WeakReference接近。

FragmentTabHost的问题 每次FragmentTabHost切换fragment时会调用onCreateView()重绘UI。 解决方法:

	private FragmentTransaction doTabChanged(String tabId,
			FragmentTransaction ft) {
		TabInfo newTab = null;
		for (int i = 0; i < mTabs.size(); i++) {
			TabInfo tab = mTabs.get(i);
			if (tab.tag.equals(tabId)) {
				newTab = tab;
			}
		}
		if (newTab == null) {
			throw new IllegalStateException("No tab known for tag " + tabId);
		}
		if (mLastTab != newTab) {
			if (ft == null) {
				ft = mFragmentManager.beginTransaction();
			}
			if (mLastTab != null) {
				if (mLastTab.fragment != null) {
					// 将detach替换为hide,隐藏Fragment
					// ft.detach(mLastTab.fragment);
					ft.hide(mLastTab.fragment);
				}
			}
			if (newTab != null) {
				if (newTab.fragment == null) {
					newTab.fragment = Fragment.instantiate(mContext,
							newTab.clss.getName(), newTab.args);
					ft.add(mContainerId, newTab.fragment, newTab.tag);
				} else {
					// 将attach替换为show,显示Fragment
					// ft.attach(newTab.fragment);
					ft.show(newTab.fragment);
				}
			}

			mLastTab = newTab;
		}
		return ft;
	}
复制代码

app启动优化

  • 之前优化软件启动时间的时候,就是Application的attachBaseContext()开启method trace,首页的dispatchDraw方法被调用之后关闭,然后这段时间CPU都干了什么。

  • 耗时的操作放到了dispatchDraw方法之后post一个回调来执行。

    Looper.getMainLooper().setMessageLogging(new Printer() {
             @Override
             public void println(String x) {
                 Log.e("msg", x);
             }
         });
    复制代码

用这个来测算每个消息花了多长时间,如果消息的执行时间超过了16ms,则获取当前的函数调用栈。 (可以确保你的耗时操作在页面显示之后才执行。)

  • 或者还有个onPostResume()方法,我们现在已经不用View的绘制之后再执行操作了,我们改为放到onPostResume方法中执行。(draw方法执行了,页面就显示了,然后不会黑屏或者白屏了)
  • app启动流程:Application的构造方法->attachBaseContext()->onCreate()->Activity的构造方法->onCreate()->配置主题中背景灯属性->onStart()...

关于inflate

  • inflate本身是io操作,而手机性能下降很大的一个原因就是io性能变差。

  • 动态添加可以解决 xml 加载时间问题;自定义view 可以解决嵌套层级问题。

strings文件下多个同种类型字符串的问题

  • 共有%1s条报价,已下%2s单
  • ]]>

RecyclerView的item的其他思路

  • recyleView中所有类型的item均为继承View,内容完全自己canvas绘制(一个个add进去),View中保存每个item的状态,获得该状态则可绘制出该View,bindView中无xml的inflate,已展示过的item再次显示时无需measure和耗时计算,ViewPager中limited item数为默认1,item被移除时,View内存被释放,再次进入时依靠保存的数据复原原item,此为同步操作。目前看来复原速度很快,用户对其是复原还是缓存的是感知不出来的。
  • android的新版本上也measure的结果进行了缓存,文本的测量也使用了100多K的空间进行全局缓存。

今日头条跟手回退实现--群分享记录

  • 今日头条也是基于Activity的透明主题来实现的,但是这个方案都有两个缺点,一就是叠加层级一多,滑动性能会下降明显,基本叠加5层就很卡了,二是透明主题破坏了系统内存回收释放的策略,导致所有的activity都是前台Activity,系统都不会回收,就会OOM。解决这个问题有一个方案就是利用android 4.4里提供的动态设置Activity透明主题来实现,当叠加了三层之后就将底部的第三层改为非透明主题。

           /**
            * 动态将一个activity设置为不透明主题
            *
           * @param activity
           */
          public static void convertActivityFromTranslucent(Activity activity) {
                   try {
                          Method method = Activity.class.getDeclaredMethod("convertFromTranslucent");
                          method.setAccessible(true);
                          method.invoke(activity);
                       } catch (Throwable t) {
                      }
        }
    复制代码
         /**
    * 动态将一个activity设置为透明主题
    *
    * @param activity
    */
   public static void convertActivityToTranslucent(Activity activity) {
       try {
           Class<?>[] classes = Activity.class.getDeclaredClasses();
           Class<?> translucentConversionListenerClazz = null;
           for (Class clazz : classes) {
               if (clazz.getSimpleName().contains(
                       "TranslucentConversionListener")) {
                   translucentConversionListenerClazz = clazz;
               }
           }
           if (Build.VERSION.SDK_INT < 21) {//这个也仅支持4.4及以上
               Method method = Activity.class.getDeclaredMethod(
                       "convertToTranslucent",
                       translucentConversionListenerClazz);
               method.setAccessible(true);
               method.invoke(activity, new Object[]{null});
           } else {//5.0以上的系统
               Method method = Activity.class.getDeclaredMethod(
                       "convertToTranslucent",
                       translucentConversionListenerClazz,
                       ActivityOptions.class);
               method.setAccessible(true);
               method.invoke(activity, null, null);
           }
       } catch (Throwable t) {
           t.printStackTrace();
       }
   }

复制代码

  • 反编译了QQ空间的apk,他们把每个类的构造函数中添加了一行Zygote.class.getName(),Zygote是系统中存在的一个类。他们选用Zygote是android SDK中不存在的,被隐藏的类,他们应该是认为以后每个版本系统中都会有这个类,所以才选择了它。那么系统进行检查的时候就不会认为初始化该类只需要使用当前Dex。(防止CLASS_ISPREVERIFIED

  • animation有个onAnimationStart和onAnmationEnd方法里面不可使用addView/removeView的方法,有可用handler.postRunable()来执行;开启硬件加速的时候有一些手机上会有概率性问题。硬件加速中draw流程只是进行录制,如果在录制的之后进行绘制的时候发现之前录制的已经无效了,在4.X的机型上就可能发生崩溃。动画的回调是在draw流程中执行的,在回调进行动态的removeView就会导致录制的绘制命令无效。

  • 线程的使用

ArrayList <File> list = new ArrayList<>();
    public void scanDir(String dirString){
        File dirFile = new File(dirString);
        list.add(dirFile);
        while (list.size() > 0){
            File files[] = list.remove(0).listFiles();
            for(File file : files){
                if(file.isDirectory()){
                    list.add(file);
                }else{
                    if(file.getAbsolutePath().endsWith("mp3")){
                        Log.e("", "这是音乐文件");
                    }
                }
            }
        }
    }
复制代码

把递归变成队列,再把队列变成多线程执行,下面只需要开启多个线程来执行scanDir(),比如一共有n+1(CPU核心数+1)个线程来执行scanDir(),当list有第一个元素时开启一个线程执行scanDir(),这个线程会往list中继续添加元素,当开启的线程小于n+1时,继续开启线程,直到达到n+1,达到之后就等待线程执行完毕,其中某个线程执行完毕之后再次去list中获取底部的元素来执行scanDir,直到list大小为0。

  • LayoutParams 的问题 直接new出来 View 如果不设置LayoutParams 就 add进一个viewgroup类型 它的LayoutParams 是由 父viewgroup generateDefaultLayoutParams函数 决定的。

  • 类型强转的注意点 强转前加 if xxx instanceof xx的校验,典型问题: 兼容包下的控件getContext不能强转为Activity(布局文件写入控件,activity继承AppCompatActivity ) 追溯View第二个构造函数发现context是LayoutInfalte传来的,发现是LayoutInflater.from传来的,发是phoneWindow传来的 发现newPhoneWindow(Activity) ,这样按理说view的context本身就是activity,可是报错说是 tintContextWrapper

  • activity在做动画的时候,页面的绘制是暂停的,或者只是绘制几帧。调用一个方法可以让其在动画过程中不暂停绘制。 反射调用ViewRootImpl 中的 setDrawDuringWindowsAnimating(true) 在onAttachedToWindow后`调用(api 19及以上)类似的可以有:
   private boolean sInited = false;
   private Method msetDrawDuringWindowsAnimatingMethod;
   @Override
   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
       super.onMeasure(widthMeasureSpec, heightMeasureSpec);
       if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2 && Build.VERSION.SDK_INT< 24) {
           Object viewRootImpl = ((Activity)getContext()).getWindow().getDecorView().getParent();
           if (!sInited) {
               try {
                   msetDrawDuringWindowsAnimatingMethod = viewRootImpl.getClass().getMethod("setDrawDuringWindowsAnimating", boolean.class);
               } catch (NoSuchMethodException e) {
                   e.printStackTrace();
               }
               if (msetDrawDuringWindowsAnimatingMethod != null) {
                   try {
                       msetDrawDuringWindowsAnimatingMethod.invoke(viewRootImpl, true);
                   } catch (IllegalAccessException e) {
                       e.printStackTrace();
                   } catch (InvocationTargetException e) {
                       e.printStackTrace();
                   }
               }
               sInited = true;
           }
       }
   }
复制代码
  • 优化View的inflate 如果你发现创建某个View的inflate很耗时,或者是measure和layout很耗时,你就只有采用类似这样的方式来优化它

耗时的工作完成的,把addViewInLayout之类的post到主线程就行了。 只有被添加到View树上的时候才可能会绘制,刚inflate出来的View没有被添加到View树上,所以不会进行measure和layout。measure是耗时的,如果在线程中执行,它就会减少主线程的卡顿,最后再添加到View树上,并且不用触发requestLayout。相当于异步加载view 使用场景:比如进入一个页面开启网络加载一段内容,当网络数据回来了就刷新页面,如果这个页面是列表,那么如果是正在滑动的时候刷新页面就会卡顿,这时就可以使用这种方式来进行优化。

  • 利用aapt解析apk信息

aapt dump badging demo.apk |grep version

aapt的其他参数,也比较使用,比如向apl中插入文件,删除文件,这个和unzip的效果是一样一样的。过去没用appt时,我们修改apk信息经常用zip/unzip,现在用appt也可以搞定,还能避免有的系统没有安装zip/unzip的问题

aapt a demo.apk test.txt

  • Activity被销毁分为两种:1. Activity对象被从ActivityThread中移除了,这时只是把java对象置null,如果你其他地方还持有该对象,这个activity是不会被释放的。 2. Activity所在的进程被回收,那它所有的资源都被回收了。只会有一些可序列化的数据被保存。

  • 以下条件webview可以关闭硬件加速

   //mModelNumber = Build.MODEL;
复制代码
  • 获取GPU刷新帧率

可以利用Looper的log机制和添加全局控件来自己实现那个柱状图。

  • 列表优化思路 1.item中不能使用任何xml,包括xml的drawable;2.减少View个数层级降低过度绘制;3.自己实现TextView,系统的TextView(特别是android 7.0以下系统)性能太烂。

  • anr adb shell ls /data/anr/

    adb pull /data/anr/traces.txt ~/Desktop

  • Handler中一段泄漏风险检测的代码
public Handler(Callback callback, boolean async) {
        if (FIND_POTENTIAL_LEAKS) {
            final Class<? extends Handler> klass = getClass();
            if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                    (klass.getModifiers() & Modifier.STATIC) == 0) {
                Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                    klass.getCanonicalName());
            }
        }

        mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread that has not called Looper.prepare()");
        }
        mQueue = mLooper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }
复制代码

该思想可用于多种自测内存泄漏的场景。

  • 使用递归时,留意函数栈是否会爆
  • 线程池的大小经验值设置:(其中N为CPU的个数)

如果是CPU密集型应用,则线程池大小设置为N+1, 如果是IO密集型应用,则线程池大小设置为2N+1

如果一台服务器上只部署这一个应用并且只有这一个线程池,那么这种估算或许合理,具体还需自行测试验证。但是,IO优化中,这样的估算公式可能更适合:最佳线程数目 =((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目因为很显然,线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。下面举个例子:比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到: ((0.5+1.5)/0.5)*8=32。这个公式进一步转化为:最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1)* CPU数目

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值