Android性能优化之内存优化,跳槽字节跳动

1.实现网络图片显示

ImageLoader是实现图片加载的基类,其中ImageLoader有一个内部类BitmapLoadTask是继承AsyncTask的异步下载管理类,负责图片的下载和刷新,MiniImageLoader是ImageLoader的子类,维护类一个ImageLoader的单例,并且实现了基类的网络加载功能,因为具体的下载在应用中有不同的下载引擎,抽象成接口便于替换。代码如下所示:

public abstract class ImageLoader {
private boolean mExitTasksEarly = false; //是否提前结束
protected boolean mPauseWork = false;
private final Object mPauseWorkLock = new Object();

protected ImageLoader() {

}

public void loadImage(String url, ImageView imageView) {
if (url == null) {
return;
}

BitmapDrawable bitmapDrawable = null;
if (bitmapDrawable != null) {
imageView.setImageDrawable(bitmapDrawable);
} else {
final BitmapLoadTask task = new BitmapLoadTask(url, imageView);
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}

private class BitmapLoadTask extends AsyncTask<Void, Void, Bitmap> {

private String mUrl;
private final WeakReference imageViewWeakReference;

public BitmapLoadTask(String url, ImageView imageView) {
mUrl = url;
imageViewWeakReference = new WeakReference(imageView);
}

@Override
protected Bitmap doInBackground(Void… params) {
Bitmap bitmap = null;
BitmapDrawable drawable = null;

synchronized (mPauseWorkLock) {
while (mPauseWork && !isCancelled()) {
try {
mPauseWorkLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

if (bitmap == null
&& !isCancelled()
&& imageViewWeakReference.get() != null
&& !mExitTasksEarly) {
bitmap = downLoadBitmap(mUrl);
}
return bitmap;
}

@Override
protected void onPostExecute(Bitmap bitmap) {
if (isCancelled() || mExitTasksEarly) {
bitmap = null;
}

ImageView imageView = imageViewWeakReference.get();
if (bitmap != null && imageView != null) {
setImageBitmap(imageView, bitmap);
}
}

@Override
protected void onCancelled(Bitmap bitmap) {
super.onCancelled(bitmap);
synchronized (mPauseWorkLock) {
mPauseWorkLock.notifyAll();
}
}
}

public void setPauseWork(boolean pauseWork) {
synchronized (mPauseWorkLock) {
mPauseWork = pauseWork;
if (!mPauseWork) {
mPauseWorkLock.notifyAll();
}
}
}

public void setExitTasksEarly(boolean exitTasksEarly) {
mExitTasksEarly = exitTasksEarly;
setPauseWork(false);
}

private void setImageBitmap(ImageView imageView, Bitmap bitmap) {
imageView.setImageBitmap(bitmap);
}

protected abstract Bitmap downLoadBitmap(String mUrl);
}

setPauseWork方法是图片加载线程控制接口,pauseWork控制图片模块的暂停和继续工作,一般在listView等控件中,滑动时停止加载图片,保证滑动流畅。另外,具体的图片下载和解码是和业务强相关的,因此在ImageLoader中不做具体的实现,只是定义类一个抽象方法。

MiniImageLoader是一个单例,保证一个应用只维护一个ImageLoader,减少对象开销,并管理应用中所有的图片加载。MiniImageLoader代码如下所示:

public class MiniImageLoader extends ImageLoader {

private volatile static MiniImageLoader sMiniImageLoader = null;

private ImageCache mImageCache = null;

public static MiniImageLoader getInstance() {
if (null == sMiniImageLoader) {
synchronized (MiniImageLoader.class) {
MiniImageLoader tmp = sMiniImageLoader;
if (tmp == null) {
tmp = new MiniImageLoader();
}
sMiniImageLoader = tmp;
}
}
return sMiniImageLoader;
}

public MiniImageLoader() {
mImageCache = new ImageCache();
}

@Override
protected Bitmap downLoadBitmap(String mUrl) {
HttpURLConnection urlConnection = null;
InputStream in = null;
try {
final URL url = new URL(mUrl);
urlConnection = (HttpURLConnection) url.openConnection();
in = urlConnection.getInputStream();
Bitmap bitmap = decodeSampledBitmapFromStream(in, null);
return bitmap;

} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
urlConnection = null;
}

if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

return null;
}

public Bitmap decodeSampledBitmapFromStream(InputStream is, BitmapFactory.Options options) {
return BitmapFactory.decodeStream(is, null, options);
}
}

其中,volatile保证了对象从主内存加载。并且,上面的try …cache层级太多,Java中有一个Closeable接口,该接口标识类一个可关闭的对象,因此可以写如下的工具类:

public class CloseUtils {

public static void closeQuietly(Closeable closeable) {
if (null != closeable) {
try {
closeable.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

改造后如下所示:

finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
CloseUtil.closeQuietly(in);
}

同时,为了使ListView在滑动过程中更流畅,在滑动时暂停图片加载,减少系统开销,代码如下所示:

listView.setOnScrollListener(new AbsListView.OnScrollListener() {

@Override
public void onScrollStateChanged(AbsListView absListView, int scrollState) {
if (scorllState == AbsListView.OnScrollListener.SCROLL_STAE_FLING) {
MiniImageLoader.getInstance().setPauseWork(true);
} else {
MiniImageLoader.getInstance().setPauseWork(false);
}

}

2 单个图片内存优化

这里使用一个BitmapConfig类来实现参数的配置,代码如下所示:

public class BitmapConfig {

private int mWidth, mHeight;
private Bitmap.Config mPreferred;

public BitmapConfig(int width, int height) {
this.mWidth = width;
this.mHeight = height;
this.mPreferred = Bitmap.Config.RGB_565;
}

public BitmapConfig(int width, int height, Bitmap.Config preferred) {
this.mWidth = width;
this.mHeight = height;
this.mPreferred = preferred;
}

public BitmapFactory.Options getBitmapOptions() {
return getBitmapOptions(null);
}

// 精确计算,需要图片is流现解码,再计算宽高比
public BitmapFactory.Options getBitmapOptions(InputStream is) {
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
if (is != null) {
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(is, null, options);
options.inSampleSize = calculateInSampleSize(options, mWidth, mHeight);
}
options.inJustDecodeBounds = false;
return options;
}

private static int calculateInSampleSize(BitmapFactory.Options options, int mWidth, int mHeight) {
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > mHeight || width > mWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
while ((halfHeight / inSampleSize) > mHeight
&& (halfWidth / inSampleSize) > mWidth) {
inSampleSize *= 2;
}
}

return inSampleSize;
}
}

然后,调用MiniImageLoader的downLoadBitmap方法,增加获取BitmapFactory.Options的步骤:

final URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
in = urlConnection.getInputStream();
final BitmapFactory.Options options = mConfig.getBitmapOptions(in);
in.close();
urlConnection.disconnect();
urlConnection = (HttpURLConnection) url.openConnection();
in = urlConnection.getInputStream();
Bitmap bitmap = decodeSampledBitmapFromStream(in, options);

优化后仍存在一些问题:

  • 1.相同的图片,每次都要重新加载;
  • 2.整体内存开销不可控,虽然减少了单个图片开销,但是在片非常多的情况下,没有合理管理机制仍然对性能有严重影的。

为了解决这两个问题,就需要有内存池的设计理念,通过内存池控制整体图片内存,不重新加载和解码已经显示过的图片。

2、实现三级缓存

内存–本地–网络

1、内存缓存

使用软引用和弱引用(SoftReference or WeakReference)来实现内存池是以前的常用做法,但是现在不建议。从API 9起(Android 2.3)开始,Android系统垃圾回收器更倾向于回收持有软引用和弱引用的对象,所以不是很靠谱,从Android 3.0开始(API 11)开始,图片的数据无法用一种可遇见的方式将其释放,这就存在潜在的内存溢出风险。 使用LruCache来实现内存管理是一种可靠的方式,它的主要算法原理是把最近使用的对象用强引用来存储在LinkedHashMap中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除。使用LruCache实现一个图片的内存缓存的代码如下所示:

public class MemoryCache {

private final int DEFAULT_MEM_CACHE_SIZE = 1024 * 12;
private LruCache<String, Bitmap> mMemoryCache;
private final String TAG = “MemoryCache”;
public MemoryCache(float sizePer) {
init(sizePer);
}

private void init(float sizePer) {
int cacheSize = DEFAULT_MEM_CACHE_SIZE;
if (sizePer > 0) {
cacheSize = Math.round(sizePer * Runtime.getRuntime().maxMemory() / 1024);
}

mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
final int bitmapSize = getBitmapSize(value) / 1024;
return bitmapSize == 0 ? 1 : bitmapSize;
}

@Override
protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
super.entryRemoved(evicted, key, oldValue, newValue);
}
};
}

@TargetApi(Build.VERSION_CODES.KITKAT)
public int getBitmapSize(Bitmap bitmap) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return bitmap.getAllocationByteCount();
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
return bitmap.getByteCount();
}

return bitmap.getRowBytes() * bitmap.getHeight();
}

public Bitmap getBitmap(String url) {
Bitmap bitmap = null;
if (mMemoryCache != null) {
bitmap = mMemoryCache.get(url);
}
if (bitmap != null) {
Log.d(TAG, “Memory cache exiet”);
}

return bitmap;
}

public void addBitmapToCache(String url, Bitmap bitmap) {
if (url == null || bitmap == null) {
return;
}

mMemoryCache.put(url, bitmap);
}

public void clearCache() {
if (mMemoryCache != null) {
mMemoryCache.evictAll();
}
}
}

上述代码中cacheSize百分比占比多少合适?可以基于以下几点来考虑:

  • 1.应用中内存的占用情况,除了图片以外,是否还有大内存的数据需要缓存到内存。
  • 2.在应用中大部分情况要同时显示多少张图片,优先保证最大图片的显示数量的缓存支持。
  • 3.Bitmap的规格,计算出一张图片占用的内存大小。
  • 4.图片访问的频率。

在应用中,如果有一些图片的访问频率要比其它的大一些,或者必须一直显示出来,就需要一直保持在内存中,这种情况可以使用多个LruCache对象来管理多组Bitmap,对Bitmap进行分级,不同级别的Bitmap放到不同的LruCache中。

2、bitmap内存复用

从Android3.0开始Bitmap支持内存复用,也就是BitmapFactoy.Options.inBitmap属性,如果这个属性被设置有效的目标用对象,decode方法就在加载内容时重用已经存在的bitmap,这意味着Bitmap的内存被重新利用,这可以减少内存的分配回收,提高图片的性能。代码如下所示:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
mReusableBitmaps = Collections.synchronizedSet(newHashSet<SoftReference>());
}

因为inBitmap属性在Android3.0以后才支持,在entryRemoved方法中加入软引用集合,作为复用的源对象,之前是直接删除,代码如下所示:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
mReusableBitmaps.add(new SoftReference(oldValue));
}

同样在3.0以上判断,需要分配一个新的bitmap对象时,首先检查是否有可复用的bitmap对象:

public static Bitmap decodeSampledBitmapFromStream(InputStream is, BitmapFactory.Options options, ImageCache cache) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
addInBitmapOptions(options, cache);
}
return BitmapFactory.decodeStream(is, null, options);
}

@TargetApi(Build.VERSION_CODES.HONEYCOMB)
private static void addInBitmapOptions(BitmapFactory.Options options, ImageCache cache) {
options.inMutable = true;
if (cache != null) {
Bitmap inBitmap = cache.getBitmapFromReusableSet(options);
if (inBitmap != null) {
options.inBitmap = inBitmap;
}
}

}

接着,我们使用cache.getBitmapForResubleSet方法查找一个合适的bitmap赋值给inBitmap。代码如下所示:

// 获取inBitmap,实现内存复用
public Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) {
Bitmap bitmap = null;

if (mReusableBitmaps != null && !mReusableBitmaps.isEmpty()) {
final Iterator<SoftReference> iterator = mReusableBitmaps.iterator();
Bitmap item;

while (iterator.hasNext()) {
item = iterator.next().get();

if (null != item && item.isMutable()) {
if (canUseForInBitmap(item, options)) {

Log.v(“TEST”, “canUseForInBitmap!!!”);

bitmap = item;

// Remove from reusable set so it can’t be used again
iterator.remove();
break;
}
} else {
// Remove from the set if the reference has been cleared.
iterator.remove();
}
}
}

return bitmap;
}

上述方法从软引用集合中查找规格可利用的Bitamp作为内存复用对象,因为使用inBitmap有一些限制,在Android 4.4之前,只支持同等大小的位图。因此使用了canUseForInBitmap方法来判断该Bitmap是否可以复用,代码如下所示:

@TargetApi(Build.VERSION_CODES.KITKAT)
private static boolean canUseForInBitmap(
Bitmap candidate, BitmapFactory.Options targetOptions) {

if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
return candidate.getWidth() == targetOptions.outWidth
&& candidate.getHeight() == targetOptions.outHeight
&& targetOptions.inSampleSize == 1;
}
int width = targetOptions.outWidth / targetOptions.inSampleSize;
int height = targetOptions.outHeight / targetOptions.inSampleSize;

int byteCount = width * height * getBytesPerPixel(candidate.getConfig());

return byteCount <= candidate.getAllocationByteCount();
}

3、磁盘缓存

由于磁盘读取时间是不可预知的,所以图片的解码和文件读取都应该在后台进程中完成。DisLruCache是Android提供的一个管理磁盘缓存的类。

1、首先调用DiskLruCache的open方法进行初始化,代码如下:

public static DiskLruCache open(File directory, int appVersion, int valueCou9nt, long maxSize)

directory一般建议缓存到SD卡上。appVersion发生变化时,会自动删除前一个版本的数据。valueCount是指Key与Value的对应关系,一般情况下是1对1的关系。maxSize是缓存图片的最大缓存数据大小。初始化DiskLruCache的代码如下所示:

private void init(final long cacheSize,final File cacheFile) {
new Thread(new Runnable() {
@Override
public void run() {
synchronized (mDiskCacheLock) {
if(!cacheFile.exists()){
cacheFile.mkdir();
}
MLog.d(TAG,“Init DiskLruCache cache path:” + cacheFile.getPath() + “\r\n” + “Disk Size:” + cacheSize);
try {
mDiskLruCache = DiskLruCache.open(cacheFile, MiniImageLoaderConfig.VESION_IMAGELOADER, 1, cacheSize);
mDiskCacheStarting = false;
// Finished initialization
mDiskCacheLock.notifyAll();
// Wake any waiting threads
}catch(IOException e){
MLog.e(TAG,“Init err:” + e.getMessage());
}
}
}
}).start();
}

如果在初始化前就要操作写或者读会导致失败,所以在整个DiskCache中使用的Object的wait/notifyAll机制来避免同步问题。

2、写入DiskLruCache

首先,获取Editor实例,它需要传入一个key来获取参数,Key必须与图片有唯一对应关系,但由于URL中的字符可能会带来文件名不支持的字符类型,所以取URL的MD4值作为文件名,实现Key与图片的对应关系,通过URL获取MD5值的代码如下所示:

private String hashKeyForDisk(String key) {
String cacheKey;
try {
final MessageDigest mDigest = MessageDigest.getInstance(“MD5”);
mDigest.update(key.getBytes());
cacheKey = bytesToHexString(mDigest.digest());
} catch (NoSuchAlgorithmException e) {
cacheKey = String.valueOf(key.hashCode());
}
return cacheKey;
}
private String bytesToHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(0xFF & bytes[i]);
if (hex.length() == 1) {
sb.append(‘0’);
}
sb.append(hex);
}
return sb.toString();
}

然后,写入需要保存的图片数据,图片数据写入本地缓存的整体代码如下所示:

public void saveToDisk(String imageUrl, InputStream in) {
// add to disk cache
synchronized (mDiskCacheLock) {
try {
while (mDiskCacheStarting) {
try {
mDiskCacheLock.wait();
} catch (InterruptedException e) {}
}
String key = hashKeyForDisk(imageUrl);
MLog.d(TAG,“saveToDisk get key:” + key);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (in != null && editor != null) {
// 当 valueCount指定为1时,index传0即可
OutputStream outputStream = editor.newOutputStream(0);
MLog.d(TAG, “saveToDisk”);
if (FileUtil.copyStream(in,outputStream)) {
MLog.d(TAG, “saveToDisk commit start”);
editor.commit();
MLog.d(TAG, “saveToDisk commit over”);
} else {
editor.abort();
MLog.e(TAG, “saveToDisk commit abort”);
}
}
mDiskLruCache.flush();
} catch (IOException e) {
e.printStackTrace();
}

}
}

接着,读取图片缓存,通过DiskLruCache的get方法实现,代码如下所示:

public Bitmap getBitmapFromDiskCache(String imageUrl,BitmapConfig bitmapconfig) {
synchronized (mDiskCacheLock) {
// Wait while disk cache is started from background thread
while (mDiskCacheStarting) {
try {
mDiskCacheLock.wait();
} catch (InterruptedException e) {}
}
if (mDiskLruCache != null) {
try {

String key = hashKeyForDisk(imageUrl);
MLog.d(TAG,“getBitmapFromDiskCache get key:” + key);
DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
if(null == snapShot){
return null;
}
InputStream is = snapShot.getInputStream(0);
if(is != null){
final BitmapFactory.Options options = bitmapconfig.getBitmapOptions();
return BitmapUtil.decodeSampledBitmapFromStream(is, options);
}else{
MLog.e(TAG,“is not exist”);
}
}catch (IOException e){
MLog.e(TAG,“getBitmapFromDiskCache ERROR”);
}
}
}
return null;
}

最后,要注意读取并解码Bitmap数据和保存图片数据都是有一定耗时的IO操作。所以这些方法都是在ImageLoader中的doInBackground方法中调用,代码如下所示:

@Override
protected Bitmap doInBackground(Void… params) {

Bitmap bitmap = null;
synchronized (mPauseWorkLock) {
while (mPauseWork && !isCancelled()) {
try {
mPauseWorkLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
if (bitmap == null && !isCancelled()
&& imageViewReference.get() != null && !mExitTasksEarly) {
bitmap = getmImageCache().getBitmapFromDisk(mUrl, mBitmapConfig);
}

if (bitmap == null && !isCancelled()
&& imageViewReference.get() != null && !mExitTasksEarly) {
bitmap = downLoadBitmap(mUrl, mBitmapConfig);
}
if (bitmap != null) {
getmImageCache().addToCache(mUrl, bitmap);
}

return bitmap;
}

3、图片加载三方库

目前使用最广泛的有Picasso、Glide和Fresco。Glide和Picasso比较相似,但是Glide相对于Picasso来说,功能更丰富,内部实现更复杂,对Glide有兴趣的同学可以阅读这篇文章Android主流三方库源码分析(三、深入理解Glide源码)。Fresco最大的亮点在于它的内存管理,特别是在低端机和Android 5.0以下的机器上的优势更加明显,而使用Fresco将很好地解决图片占用内存大的问题。因为,Fresco会将图片放到一个特别的内存区域,当图片不再显示时,占用的内存会自动释放。这类总结下Fresco的优点,如下所示:

  • 1、内存管理
  • 2、渐进式呈现:先呈现大致的图片轮廓,然后随着图片下载的继续,呈现逐渐清晰的图片。
  • 3、支持更多的图片格式:如Gif和Webp。
  • 4、图像加载策略丰富:其中的Image Pipeline可以为同一个图片指定不同的远程路径,比如先显示已经存在本地缓存中的图片,等高清图下载完成之后在显示高清图集。

缺点

安装包过大,所以对图片加载和显示要求不是比较高的情况下建议使用Glide。

六、总结

对于内存优化,一般都是通过使用MAT等工具来进行检查和使用LeakCanary等内存泄漏监控工具来进行监控,以此来发现问题,再分析问题原因,解决发现的问题或者对当前的实现逻辑进行优化优化完后再进行检查,直到达到预定的性能指标。下一篇文章,将会和大家一起来深入探索Android的内存优化,尽请期待~

最后

我坚信,坚持学习,每天进步一点,滴水穿石,我们离成功都很近!
以下是总结出来的字节经典面试题目,包含:计算机网络,Kotlin,数据结构与算法,Framework源码,微信小程序,NDK音视频开发,计算机网络等。

字节高级Android经典面试题和答案


领取方法:

所有资料获取方式:评论666+点赞即可咨询资料免费领取方式!

直达领取链接:【Android高级架构师】文件夹下载!

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
img

-U1oAdmCV-1710962309034)]

领取方法:

所有资料获取方式:评论666+点赞即可咨询资料免费领取方式!

直达领取链接:【Android高级架构师】文件夹下载!

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-4aHbwJnF-1710962309034)]
[外链图片转存中…(img-ly5jGFT8-1710962309035)]
[外链图片转存中…(img-iiXv3fLb-1710962309035)]
[外链图片转存中…(img-pU18Nl36-1710962309036)]

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
[外链图片转存中…(img-TY5zUK1B-1710962309036)]

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值