Android 程序优化总结

Android程序优化总结
第一部分 编程规范
1.1 基本要求:
程序结构清晰,简单易懂,单个函数的程序行数不得超过100行。
打算干什么,要简单,直接。
尽量使用标准库函数和公共函数
不要随意定义全局变量,尽量使用局部变量。
使用括号可以避免二意性
可读性要求:
可读性第一,效率第二
源程序文件,都有文件头说明
每个函数,都有函数头说明
常量定义 和变量定义和引用时,要添加注释
处理过程的每个阶段都有相关注释说明
在典型算法前都有注释
注意编程格式
循环,分支层次不要超过五层
一目了然的程序不加注释
注释行数应占总行数的1/5~1/3(不包括程序头和函数头说明部分)
结构化要求:
禁止出现两条等价支路
用case实现多路分支
避免从程序引出多个出口,函数只有一个出口
不使用条件赋值语句
不要轻易用条件分支去代替逻辑表达式
正确性和容错性要求
程序首先是正确,其次是优美
所以变量在调用前必须被初始化
对所有的用户输入,必须进行合法检查
不要比较浮点数的相等,如 100.0*0.1=10
程序与环境或状态发生关系时,必须主动去处理发生的意外 如程序能否逻辑锁定
单元程序也是编程的一部分,提交联调测试的程序必须通过单元测试
可重用性要求
重复使用的完成相对独立功能的算法或代码应抽象为公共控件或类
公共控件或类应考虑面向对象思想,减少外界联系,考虑独立性或封装性

第二部分 程序性能测试
1.1 程序的性能测试包括:
计算性能:就是说一段代码执行后所用的时间
内存消耗:
启动时间:从启动程序到正常运行的时间
可伸缩性(scalability):指应用程序如何应对增长的流量
用户察觉性能:不是程序运行有多快,而是用户感觉程序运行有多快

动态代理类可以在运行时实现若干接口,每一个动态代理类都有一个Invocation handler对象与之对应,这个对象实现了InvocationHandler接口,通过动态代理的接口对动态代理对象
的方法调用会转而调用Invocation handler 对象的invoke方法,通过动态代理实例,方法对象和参数可以执行调用并返回结果。

1.1.1 计算性能测试
方法一:
我们可以在程序中加入long start = System.currentTimeMillis()
long end = System.currenTimeMillis()
System.out.println(“所用时间是”+(end -start)+"ms")//单位是毫秒

方法二:
通过Java的java.lang.reflect.Proxy 和java.lang,reflect.InvocationHandler利用动态代理测试。
下例中比较两个方法的执行时间。我们要测试的是java.util.LinkedList和java.util.ArrayList 的get(int index)方法,显然ArrayList 要比LinkeList效率高
因为前者是随机的,后者是需要顺序访问
第一步:首先创建一个接口
public interface Testing{
public void testArrayList();
public void testLinkedList();
}
第二步:创建测试对象 实现这个接口
public class TestingImpl implements Testing{
private Listlink= new LinkedList();
private Listarray= new ArrayList();
public TestingImpl(){
for (int i = 0; i < 10000; i++){
array.add(new Integer(i));
link.add(new Integer(i));
}
}
public void testArrayList(){
for (int i = 0; i < 10000; i++)
array.get(i);
}
public void testLinkedList(){
for (int i = 0; i < 10000; i++)
link.get(i);
}
}
第三步:实现InvocationHandler接口
public class Handler implements InvocationHandler{
private Objectobj;
public Handler(Object obj){
this.obj = obj;
}
public static Object newInstance(Object obj){
Object result = Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), new Handler(obj));
return (result);
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{
Object result;
try{
Log.i("Handler", "begin method " + method.getName());

long start = System.currentTimeMillis();
result = method.invoke(obj, args);
long end = System.currentTimeMillis();

Log.i("Handler", "the method " + method.getName() + " lasts " + (end - start) + "ms");
}catch (InvocationTargetException e){
throw e.getTargetException();
}catch (Exception e){
throw new RuntimeException("unexpected invocation exception: " + e.getMessage());
}finally{
Log.i("Handler", "end method " + method.getName());
}
return result;
}
}
第四步:创建测试程序
public class Activity01 extends Activity{
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
try {
Testing testing = (Testing) Handler.newInstance(new TestingImpl());
testing.testArrayList();
testing.testLinkedList();
}catch (Exception e){
e.printStackTrace();
}
setContentView(R.layout.main);
}
}
第五步:运行程序后,在DDMS中可以看到程序输出的Log信息。可看出每个方法所用的时间
使用动态代理的好处是你不必修改原有代码,但有一个缺点—如果你的类原来没有实现接口你必须写一个接口

1.1.2 内存消耗测试
程序中消耗内存的因素:如对象,加载类,线程等都会消耗虚拟机堆空间。
方法一:
我们可以在程序中加入如下代码进行测试。long total = Runtime.getRuntime().totalMemory();//总内存
long free = Runtime.getRuntime().freeMemory();//可用内存
long use = total - free;//使用了的内存
方法二:
动态代理比较 此例中只考虑对象消耗的虚拟机空间。
第一步 创建接口
public interface MemoConsumer{
public void creatArray();
public void creatHashMap();
}
第二步 创建测试对象实现这个接口
public class MemoConsumerImpl implements MemoConsumer{
ArrayListarr= null;
HashMaphash= null;
public void creatArray(){
arr = new ArrayList(1000);
}
public void creatHashMap(){
hash = new HashMap(1000);
}
}
第三步 计算当前内存消耗
public class Memory{
public static long used(){
long total = Runtime.getRuntime().totalMemory();
long free = Runtime.getRuntime().freeMemory();
return (total - free);
}
}
第四步 修改Handler类的invoke()方法
public class Handler implements InvocationHandler{
private Objectobj;
public Handler(Object obj){
this.obj = obj;
}
public static Object newInstance(Object obj){
Object result = Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), new Handler(obj));
return (result);
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{
Object result;
try{
Log.i("Handler", "begin method " + method.getName());

long start = Memory.used();
result = method.invoke(obj, args);
long end = Memory.used();

Log.i("Handler", "memory increased by " + (end - start) + "bytes");
}catch (InvocationTargetException e){
throw e.getTargetException();
}catch (Exception e){
throw new RuntimeException("unexpected invocation exception: " + e.getMessage());
}finally{
Log.i("Handler", "end method " + method.getName());
}
return result;
}
}
第五步 在Activity中加入代码,测试程序运行的内存情况
public class Activity01 extends Activity{
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);

MemoConsumer arrayMemo=(MemoConsumer)Handler.newInstance(new MemoConsumerImpl ());
arrayMemo.creatArray();
arrayMemo.creatHashMap();

setContentView(R.layout.main);
}
}
第六步 查看DDMS中log信息
缺点:测试对象必须实现接口在一定程度上限制了动态代理的使用,可以借鉴Spring中使用的CGlib来实现任何接口的类创建动态处理。

第三部分 初级优化
优化通常包括两方面:减少代码的体积,提高代码的运行效率
以下主要是如何提高代码效率
1、尽量在类中使用final修饰符
带有final修饰符的类是不可派生的。例如:为String类指定final防止覆盖length()方法。如果一个类为final修饰,则该类所有方法都是final。此举会使Java编译器寻找机会内联
(inline)所有的fina方法。可以使性能评价提高50%。
2、尽量重用对象
特别是String对象的使用中,出现字符串连接情况时应用StringBuffer代替。由于系统不仅要花时间生成对象,以后可能还需要花时间对这些对象进行垃圾回收和处理。所以生成过
多的对象将会给程序带来很大的影响。
3、尽量使用局部变量
调用方法时传递的参数以及在调用中创建的临时变量都保存在栈(stack)中,速度快。其他变量,如静态变量、实例变量等。都在堆(Heap)中创建,速度较慢。
4、不要重复初始化变量
在默认情况下,调用类的构造函数时,Java会把变量初始化成确定的值。所有对象被设置成null,整数变量(int,byted等)设置为0,逻辑值设置成false.当一个类从另一个类派生时,
这一点尤其要注意,因为用new关键字创建一个对象时,构造函数链中所有的构造函数都会被自动调用。
5、在Java+Oracle 的应用系统开发中,Java中内嵌的SQL语句尽量使用大写的形式,以减轻Oracle解析器的解析负担
6、Java编程过程中,进行数据库连接、I/O流操作时务必小心,在使用完毕后,及时关闭以释放资源。
7、保证过期对象及时回收。JVM自动回收垃圾的条件是:对象不再被引用;但是JVM的GC并非十分的机智,即使对象满足了垃圾回收条件也不一定被立即回收,所以,建议在对象使用完毕后,
手动设置成null
8、在使用同步机制时,应尽量使用方法同步代替代码块同步
9、尽量减少对代码的同步计算
例如:for(int i =0;i<list.size;i++){
}
应替换为:for(int i = 0, int len= list.size();i<len;i++){
}
10、尽量采用lazy loading的策略,在需要的时候才开始创建
例如:String str = "aaa";
if(i==1){
list.add(str)
}
应替换为:
if(i ==1){
String str = "aaa"
list.add(str)
}
11、慎用异常,异常对性能不利
因抛出异常首先要创建一个新的对象.只要有异常被抛出,VM就必须调整调用栈,因为在处理过程中创建了一个新的对象。异常只能用于错误处理,不应该用来控制程序流程。
12、不要在循环中使用Try/Catch语句。应将其放置在最外层。
13、StringBuffer 的使用。
应该使用一个合适的容量值来初始化StringBuffer.因为只要StringBuffer到达它容量的最大值时,它就不得不创建一个新的字符数组,然后重新将旧的字符和新的字符都复制一遍。
所以选择一个合适的容量很重要。
StringBuffer表示了可变,可写的字符串。有三个工作方法:
StringBuffer(); //默认分配16个字符的空间
StringBuffer(int size); //分配size个字符空间
StringBuffer(String str);//分配16个字符+str.length()个字符空间
你可以使用第二个构造函数来设定它的初始化容量,这样可以明显提升性能。也可以在StringBuffer对象创建之后通过ensureCapacity(int minimumcapacity)设置它的容量.
14、合理使用Java类java.util.Vector.
一个Vector就是一个java.lang.Object实例数组。
Vector与数组类似,它的元素可以通过整数形式索引访问。但是,Vector类型的对象在创建之后,对象的大小依元素的增加或者删除而定。Vector在默认构造函数中,Vector的初始存储
能力是10个元素,如果所加元素超过其存储能力时,则以后存储能力每次都加倍,同StringBuffer类一样,每次拓展存储能力时,所有元素都要复制到存储空间中去。

下例向Vector中加入元素的例子
Object obj = new Objectt();
Vector v = new Vector(1000);
for(int i=0;i<1000;i++){
v.add(0,obj);//每次都在Vector中的第一位处加入Object对象,除非必须在第一位处加入 否则不要使用此方法。
}
下面这个例子要比上面例子快几个数量级
Object obj = new Objectt();
Vector v = new Vector(1000);
for(int i=0;i<1000;i++){
v.add(obj);
}
Vector类中使用remove()方法删出一个元素后,其他元素都要往前移动,也就是说从Vector删除元素时从最后一位删除效率要高些。
for(int i = 0;i<10000;i++){
v.remove(0);//每次都从第一个删除
}
for(int i =0;i<10000;i++){
v.remove(v.size()-1);//每次从最后一个删除 效率要比上一个高
}
从Vector类的对象v删除所有元素最好的方法是v.removeAllElements();
15、当复制打了数据时。使用System.arraycopy();
16、代码重构:增强代码的可读性
例如:
public class ShopCart{
private List carts;
public void add(Object item){
if(carts ==null){
carts = new ArrayList();
}
carts.add(item);
}
public void remove(Object item){
if(carts.contains(item)){
carts.remove(item);
}
}
public List getCarts(){
return Collections.unmodifiableList(carts);//返回只读列表
}
//this.getCarts().add(item);//不推荐使用此种写法,代码可读性不好
}
17、某些情况下可以不用new关键字创建类的实例
用new关键字创建类的实例时,构造函数链中所有构造函数都会被自动调用。但如果一个对象实现了Cloneable接口,我们可以调用它的clone()方法。clone()方法不会调用任何类
构造函数。
例如:此思路对数组也适用
public static Credit getNewCredit(){
return new Credit();
}
改进后
private static Credit BaseCredit = new Credit();
public static Credit getNewCredit(){
return (Credit)BaseCredit.clone();
}
18、乘法和除法
x = i*8
y = i/2
改进后
x = i<<3;//每左移动一位相当于乘以2 此处移动了3位相当于除以8
y = i>>1;//没右移动一位相当于除以2;
用移位操作代替乘、除 可以极大提高性能。因其移位符难以理解 所以最好加入注释
19、不要将数组声明为 public static final
20、HashMap的遍历效率
例如:
Map<String,String[]> paraMap = new HashMap<String , String[]>();
//第一种方法
Set<String> string01 = paraMap.keySet();
for(String string02 : string01){
String [] values = paraMap.get(string02);
}
//第二种方法
for(Entry<String ,String[]> entry01 : paraMap.entrySet()){
String string01 = entry01.getKey();
String [] values = entry.getValue();
}
第二种效率明显高于第一种。第一种是从HashMap中取得keySet值,然后在循环取值相当于两次进入HashMap中,第二个则是 每次循环时都取value值和key值 是一起操作的所有效率高。
21、array(数组)和ArrayList的使用。
array([])最高效,但其容量固定且无法动态改变。
ArrayList 容量可动态增长,但消息较低。ArrayList内部封装了一个Object类型数组,在加入对象时,抛弃类型信息,所有对象都屏蔽为Object,编译时不继承类型,但运行时会报错
但加入泛型后就可以使用ArrayList进行类型检测了。
所以在尽可能使用array,无法确定数组大小时,才使用ArrayList.
22、尽量使用HashMap和ArrayList,除非必要,不要使用HashTable 和Vector ,后者由于使用同步机制,而导致效率降低。
23、StringBuffer和StringBuider的区别在于
StringBuffer是线程安全的可变字符序列。
StringBuider它支持所有相同操作,但由于它不执行同步,所以速度快但是非线程安全的
相同情况下,使用StringBuilder 比StringBuffer 仅仅能获得10%~15%左右的性能提升,但要冒线程不安全风险,因此当你能确定你不是在的多线程的环境中才能使用StringBuilder。
为了获得更好的性能,在构造Stringbuilder和StingBuffer时应尽可能指定其容量,当然如果操作没有超过16个字符就不用了。

第四部分 高级优化
优化方法:
优化循环,通过重新组织重复的子表达式来提高循环体的运行性能
减少使用对象的数量来提高运行性能
缩减网络传输数据来缩短等待时间
下面为另外几种优化方法
1、采用对象池技术,提高对象的利用效率
作用:可有效的解决不断的创建和释放对象带来的性能损失问题。
从研究来看,游戏性能消耗主要源于创建和释放对象,而不创建对象又无法实现逻辑功能,因此要尽量避免对象的创建和释放。
本思路是根据需要创建一定量的对象,在需要创建对象的时候从池中申请空闲对象,释放对象时把对象释放回对象池中,以有效避免创建和释放对象带来的性能损失。
例如:
分析游戏需求发现同时显示的敌机数量最多不过5架,采用对象池技术先定义一个对象池,容量为同时显示的敌机的最大数量。
Enemy[5] enemy = new Enemy[5];
for(int i =0;i<5;i++){
enemy[i] = new Enemy();
}
在类Enemy里增加标志属性used和带参数的reset方法是对象可重置到初始状态,在载入游戏卡时初始化对象池,在需要创建对象的时候从对象池获取一个未被使用的对象并使用reset
方法初始化,需要释放对象的时候只需要将标志位修改以供下次使用。
2、尽可能的使用基本数据类型代替对象
使用基本数据类型仅需要少量的存储空间(对象在逻辑实现上具有优势),所以在局部不影响逻辑的情况下可以考虑用基本数据类型代替对象实现。
例如:
在游戏中飞机要发射子弹,基于面向对象的设计模式可将子弹抽象成Bullet类,然后定义代表子弹的对象池,在发射子弹的时候,在对象池中查找可用对象并重置其位置为飞机所在位置,
即在每一次gameloop中将Bullet对象的纵坐标值减少一定数值。由于对象操作的效率低,可以想到用基本数据类型来代替对象实现如下:
int[][] bullet = new int [2][10];//假设屏幕同时显示10颗子弹
for(int i = 0;i<10;i++){
bullet[0][i] =999;//对应Bullet对象的x坐标
bullet[1][i] =999;//对应Bullet对象的y坐标
}
在每一次gameloop中;
while(run){
for(int i = 0;i<10; i++){
if(bullet[1][i]==999)
//重置为发射子弹飞机当前的位置
bullet[0][i] == myPlane.getXposition();
bullet[1][i] == myPlane.getYposition();
}else{
//当前使用的子弹y坐标减少
bullet[1][j] -=10;
}
}
3、用简单的数据计算代表复制的函数计算
即减少函数计算复杂度或者减少复杂函数的调用次数甚至避免使用复杂函数计算。

第五部分 Android的高效开发
判断一个系统是否合理,有俩个基本原则:
不要做不必要做的事情
尽可能的节省内存
1、尽可能避免创建对象(Object)
当从原始的输入数据中提取字符串时。试着从原始字符串返回一个子字符串,而不是创建一份复本。你将创建一个新的字符串对象,但是它和你的原始数据共享数据空间。
如果你有一个返回字符串的方法,你应该知道无论如何返回的结果都是StringBuffer,改变你的函数的定义和执行,让函数直接返回而不是通过创建一个临时的对象。一个比较激进的
方法就是把一个多维数组分割成几个平行的一维数组。
一个int型数组要比一个Integer类型数组要好。两个int类型要比一个(int,int)对象数组效率要高。其他数据类型也适用。
当创建一个包含一系列 A 和 B 对象的容器时。两个平行的A[]和B[]类型数组要比一个(int , int)对象数组效率高得多
总之 我们应该尽量避免创建短期的临时对象,越少的对象创建意味着越少的垃圾回收。
2、使用自身方法
当处理字符串的时候,尽量使用使用如String.indexOf()、String.lastIndexOf()这些对象自身带有的方法。
3、使用虚拟优于使用接口
如声明一个HashMap对象
Map map01 = new HashMap();
HashMap map02 = new HashMap();
第一种速度要快,但如果第二个更适合你编程的话第一个就不可去了。看情况而定
4、使用静态优于使用虚拟
如果你没有必要去访问对象外部,那么就使你的方法成为静态方法,它会更快被调用,因为它不需要虚拟函数导向表。
5、尽量避免使用内在的Get,Set方法
在android编程中,虚方法的调用会产生很多代价,比实例属性查询的代价还要多。我们应该在外部调用Get和Set函数,但是在内部调用时,我们应该直接调用。
6、缓冲属性调用
例如:绝不在一个for语句中第二次调用一个类的方法
for(int i=0;i <this.mCount;i++){
dumpItem(this.mItems[i])
}
改进后
int count = this.mCount;
Item [] items = this.mItems;
for(int i = 0;i<count;i++){
dumpItem(items[i])
}
当你不止一次的调用某个实例时,直接本地化这个实例,把这个实例中的某些值赋给一个本地变量。
7、慎重使用增强for循环
增强for循环在数组里面表现很好,但是当和Interble对象一起使用时要谨慎,因为多了一个对象创建。
8、避免列举类型
9、注意通过内内部类使用包空间
10、避免浮点类型的使用

第六部分 Android UI优化
RelativeLayout比Linearlayout占用的资源更少。
对应RelativeLayout需要注意,因为它是通过多个View之间的关系而确定的框架,那么当其中一个View因为需要调用GONE来完全隐藏掉后,会影响与其相关联的View。为此Android提供了
alignWithParentIfMissing来解决这类问题,即当某一个View无法找到与其相关联的View后,它将依据alignWithParentIfMissing来设定判断是否与父级View对齐
Android 的4个特别标签 其中与资源复用有关
1.1<viewStub/>:此标签可以使UI在特殊情况下,直观效果类似于设置View的不可见性,更大意义在于被此标签包裹的View在默认状态下不会占用任何内存空间。viewStub通过include
从外部导入View元素。用法是通过android:layout来制定所包含的内容。默认情况下,ViewStub所包含的标签都属于visibility = GONE。viewStub通过方法inflate()
来使系统加载内部的View.
<ViewStub
android:id="@+id/stub"
android:inflatedId="@+id/subTree"
android:layout="@layout/mySubTree"
android:layout_width ="120dip"
android:layout_height="40dip"/>

1.2 <include/>:可以通过此标签直接加载外部的xml到当前结构中,是复用UI资源的常用标签。用法是将需要复用xml文件路径赋予include标签的Layout属性。
<include
android:id="@+id/cell"
layout="@layout/ar01"/>
<include
android:layout_width="fill_parent"
layout="@layout/ar02"/>

1.3 <requestFocus>:此标签用于指定屏幕内的焦点View.用法是将标签置于View标签内部。
<EditText
android:id="@+id/text"
android:layout_width="fill_parent"
android:layout_weight="0"
android:paddingBottom="4">
<requestFocus>
</EditText>
1.4<merge/>:优化UI结构时起到很重要的作用。目的是通过删减多余或额外的层级,从而优化整个Android layout结构。
例如:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<ImageView
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:scaleType="center"
android:src="@drawable/golden_gate"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="20dip"
android:layout_gravity="center_horizontal|bottom"
android:padding="12dip"
android:background="#AA000000"
android:textColor="#ffffffff"
android:text="Golden Gate"
/>
</FrameLayout>
以上是在xml文件中的设计。启动后Android SDK目录下的tools文件夹下的hierarchyviewer.bat工具,查看当前UI结构视图可以看到出现了两个framelayout节点,很明显这两个
意义完全相同的节点造成了资源浪费(在开发中可以通过hierarchyViewer查看当前UI资源分配情况)。此时可以通过<merge/>标签来处理类似的问题,做法将framLayout替换成merge
如下:
<merge
xmlns:android="http://schemas.android.com/apk/res/android"
>
<ImageView
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:scaleType="center"
android:src="@drawable/golden_gate"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="20dip"
android:layout_gravity="center_horizontal|bottom"
android:padding="12dip"
android:background="#AA000000"
android:textColor="#ffffffff"
android:text="Golden Gate"
/>
</merge>
替换后通过hierarchyviewer查看的UI结构变化,当成多余的frameLayout节点被合并在一起了,或者理解为将merge标签中的子集直接加到Activity的frameLayout根节点下,注意
所有的Activity视图的根节点都是frameLayout。
如果你所创建的Layout并不是把frameLayout作为根节点,如是用LinearLayout等定义root标签,就不能通过merge来优化UI结构。
merge另一个用法,当用Include或者ViewStub标签从外部导入xml结构时,可以将导入的xml用merge作为根节点表示,这样当嵌入父级结构中后可以很好地将它所包含的子集融合到
级结构中,而不会出现多余节点。
注意:<merge/>只可以作为xml layout的根节点,当需要扩充的xml layout本身是由merge作为根节点的话,需要将被导入的xml layout置于viewGroup中,同时需要设
置attachToRoot为true.

第七部分 zipalign
zipalign 它优化了应用程序的打包方式。这样使Android与你的应用程序更加有效和简便的交互,有可能提高应用程序和整个系统的运行速度。
使用ADT选择自动校验程序文件资源。步骤:鼠标由键点击Eclipse中项目名称---Android Tools ----Export Signed Application Package,剩下的工作就交给Eclipse了.

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值