仅需6步,教你轻易撕掉app开发框架的神秘面纱(6):各种公共方法及工具类的封装

9 篇文章 0 订阅
6 篇文章 0 订阅

为什么要封装公共方法

封装公共方法有2方面的原因:
一是功能方面的原因:有些方法很多地方都会用,而且它输入输出明确,并且跟业务逻辑无关。比如检查用户是否登录,检查某串数字是否为合法的手机号。像这种方法就应该封装起来,供各个模块调用,避免重复造轮子。

二是防止出错:每一个合格的程序员就是从一个个错误中走出来的,任何一个架构包括android/iOS都有一些容易犯的错,我们可以把这些容易犯错的地方封装一下,每次用统一的,规定好的处理方式,这样就不会出错了。

防止重复:这个词在框架构造中提到的次数最多。这也是编写高可用代码的最重要的因素之一。

封装哪些内容及依据

下面就是一个最基本的应用需要封装的方法:

  1. 时间相关:获取本地时间,获取服务器时间,获取时间格式化等等。
  2. log和toast:log和toast的封装是为了做开关。应用正式版本中大多数调试log都应关闭。
  3. 异常上传:把客户端错误上传至服务器,就可以从服务端查看客户端哪里问题最严重,有的放矢。对于android来说,获取未捕获异常也很重要,请 查看此文 ,向下滚动至第三点:异常类捕获。
  4. 常见错误规避:如类型转换,subString这些容易引发异常的地方。
  5. 一些工具方法:如dp转px,px转dp,md5,判断手机号邮箱是否合法,获取设备信息等等。

代码:
贴这些代码的目的是分辨出哪些代码可以放在utils中,并且要养成往utils中提取代码的习惯。

更充分一点儿说:只要有2个地方使用的类似代码,就需要考虑是否可以提取成公共方法了。

时间相关函数:

//android: TimeUtils.java
public class TimeUtils {
    private static long timeOffset = Long.MAX_VALUE;//同服务器的时间差

    public static long getLocalTime(){//获取本地时间,单位:s
        return (long)(getLocalTimeMs() / 1000.0);
    }

    public static long getCurrTime(){//获取服务器时间,单位:s
        return getCurrTimeInner(getLocalTime(), 1);
    }

    private static long getCurrTimeInner(long base, long factor){
        if(timeOffset != Long.MAX_VALUE && timeOffset != 0){
            return base + timeOffset * factor;
        }
        return base;
    }

    public static long getLocalTimeMs(){//获取本地时间,单位:ms
        return System.currentTimeMillis();
    }

    public static long getCurrTimeMs(){//获取服务器时间,单位:ms
        return getCurrTimeInner(getLocalTimeMs(), 1000);
    }

    public static void adjustTimeOff(long serverTimeStamp){//调整时间差,需要在调用服务器接口时获取到服务器时间后调用
        timeOffset = serverTimeStamp - getLocalTime();
    }
}
//iOS: TimeUtils.h

#import <Foundation/Foundation.h>

@interface TimeUtils : NSObject

+(double)getLocalTimeWithSec;//获取本地时间,单位:s
+(double)getLocalTimeWithMSec;//获取本地时间,单位:ms
+(double)getCurrentTimeWithSec;//获取服务器时间,单位:s
+(double)getCurrentTimeWithMSec;//获取服务器时间,单位:ms
+(void)setTimeOffsetWithServer:(double) serverTime;调整时间差,需要在调用服务器接口时获取到服务器时间后调用

@end
//iOS: TimeUtils.m

#import "TimeUtils.h"

static double sTimeOffWithServer = 0;

@implementation TimeUtils

+(double)getLocalTimeWithSec{
    return [[NSDate date]timeIntervalSince1970];
}

+(double)getLocalTimeWithMSec{
    return [self getCurrentTimeWithSec] * 1000;
}

+(void)setTimeOffsetWithServer:(double) serverTime{
    sTimeOffWithServer = serverTime - [self getLocalTimeWithSec];
}

+(double)getCurrentTimeWithSec{
    return [self getLocalTimeWithSec] + sTimeOffWithServer;
}

+(double)getCurrentTimeWithMSec{
    return [self getLocalTimeWithMSec] + sTimeOffWithServer;
}

@end

log及toast封装

关联章节:网络模块封装

//android LogUtils.java
public class LogUtils {
    public static boolean isOpen = true;

    //所有非错误log必须使用此方法打印
    public static void d(String tag, String msg){
        if (isOpen){
            Log.d(tag, msg);
        }
    }

    //所有不带有异常的错误必须使用此方法打印
    public static void e(String tag, String msg){
        if (isOpen){
            Log.e(tag, msg);
        }else{
            uploadErrorLog(Utils.getClientInfo(), msg);
        }
    }

    public static String[] exceptionToString(Throwable e){
        StackTraceElement[] eles = e.getStackTrace();
        String []ret = new String[eles.length + 1];
        ret[0] = e.toString();
        for (int i = 0; i < eles.length; i++) {
            ret[i + 1] = "  at " + eles[i].toString();
        }
        return ret;
    }

    //所有带有异常的错误必须使用此方法打印
    public static void e(String tag, Throwable tr){
        if(isOpen){
            String exarr[] = exceptionToString(tr);
            for (int i = 0; i < exarr.length; i++){
                e(tag, exarr[i]);
            }
        }else{
            String exarr[] = exceptionToString(tr);
            StringBuilder sb = new StringBuilder();
            for (String s: exarr){
                sb.append(s);
            }
            //注:ClientInfo中包含:设备信息,应用信息,设备号等信息。
            uploadErrorLog(Utils.getClientInfo(), sb.toString());
        }
    }

    //上传错误至服务器
    private static void uploadErrorLog(String clientInfo, String errorString){
        //TODO:向特定服务器上传
        //此处为伪代码,具体代码请联系本系列的第四章,统一编写。
        Server.post("{\"clientInfo\":" + clientInfo + ",\"errorString\":" + errorString);
    }

    //提示用toast
    public static void toastTip(Context context,@NonNull String msg) {
        showToast(context, msg);
    }

    //调试用toast
    public static void toastDebug(Context context,@NonNull String msg) {
        if (isOpen) {
            showToast(context, msg);
        }
    }

    private static void showToast(Context context,@NonNull String msg) {
        Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
    }
}
//iOS: LogUtils.h

//debug log的宏定义:所有非错误log必须使用此宏打印
#if defined(DEBUG) && DEBUG == 1
#define DebugLog(...) NSLog(__VA_ARGS__)
#else
#define DebugLog(...)
#endif

//error log 的宏定义:所有错误log必须使用此宏打印
#if defined(DEBUG) && DEBUG == 1
#define ErrorLog(...) DebugLog(__VA_ARGS__)
#else
#define ErrorLog(...)                                                                         \
do{                                                                                           \
    NSString *errorString = [NSString stringWithFormat: __VA_ARGS__];                         \
    NSLog(errorString);                                                                       \
    [LogUtils uploadErrorLogWithClientInfo:[Utils getClientInfo] andErrorString:errorString]; \
}while(0);
#endif

//tip toast宏定义:所有提示用户所用的宏定义必须使用此宏打印
#define TipToast(msg, duration, viewCtl) [LogUtils toastWithMessage: msg andDuration: duration andViewController:viewCtl]

//debug toast宏定义:所有debug用的宏定义必须使用此宏打印
#if defined(DEBUG) && DEBUG == 1
#define DebugToast(msg, duration, viewCtl) TipToast(msg, duration, viewCtl)
#else
#define DebugToast(msg, duration, viewCtl)
#endif

@interface LogUtils : NSObject
//上传错误数据至服务器。
+(void) uploadErrorLogWithClientInfo:(NSString *)clientInfo andErrorString:(NSString*) errorString;
//为某个页面显示toast,模拟android
+(void) toastWithMessage:(NSString *)msg andDuration:(NSInteger) duration andViewCotroller:(UIViewController *)viewCtl;
@end
//iOS: LogUtils.m
#import "LogUtils.h"
@implements LogUtils
+(void)uploadErrorLogWithClientInfo:(NSString *)clientInfo andErrorString:(NSString*) errorString{
    //TODO:向特定服务器上传
    //此处为伪代码,具体代码请联系本系列的第四章:[网络模块封装](http://blog.csdn.net/hard_man/article/details/50699346),统一编写。
    [Server postWithString: "{\"clientInfo\":" + clientInfo + ",\"errorString\":" + errorString];
}
+(void) toastWithMessage:(NSString *)msg andDuration:(NSInteger) duration andViewCotroller:(UIViewController *)viewCtl{
    UIView *baseView = [[UIView alloc] init];
    [viewCtl.view addSubview:baseView];
    baseView.userInteractionEnabled = NO;
    //此处使用Masory来指定View的大小和位置
    [baseView makeConstraints:^(MASConstraintMaker *make) {
        make.edges.equalTo(viewCtl.view);
    }];

    UIView *toastBg = [[UIView alloc] init];
    //背景色
    toastBg.backgroundColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.8];
    //设置圆角
    toastBg.layer.cornerRadius = 3;
    toastBg.layer.masksToBounds = YES;
    //关闭点击
    toastBg.userInteractionEnabled = NO;
    [baseView addSubview:toastBg];
    [toastBg makeConstraints:^(MASConstraintMaker *make) {
        make.width.lessThanOrEqualTo(viewCtl.view.bounds.size.width - 30);
        make.centerX.equalTo(baseView);
        make.centerY.equalTo(baseView).offset(0);
    }];

    //文字
    UILabel *label = [[UILabel alloc] init];
    label.text = msg;
    label.textColor = [Color whiteColor];
    label.textAlign = NSTextAlignmentLeft;
    label.font = [UIFont systemFontOfSize: 15];
    label.numberOfLines = 3;
    label.userInteractionEnabled = NO;
    [toastBg addSubview:label];
    [label makeConstraints:^(MASConstraintMaker *make) {
        make.edges.equalTo(toastBg).insets(UIEdgeInsetsMake(5, 10, 5, 10));
    }];
    [baseView layoutIfNeeded];

    [UIView animateWithDuration:0.3 delay:duration options:UIViewAnimationOptionCurveLinear animations:^{
        toastBg.alpha = 0;
    } completion:^(BOOL finished) {
        [baseView removeFromSuperview];
    }];
}
@end
//android: Utils.java
//下面是我项目中使用的,查看更多可[点击此处](http://www.cnblogs.com/cr330326/p/4422507.html)。
public class Utils{

    //获取app信息
    public static String getClientInfo(){
        StringBuilder ret = new StringBuilder();
        //系统版本
        ret.append("os:android");
        //系统版本号:
        ret.append(",osVersion:" + android.os.Build.VERSION.RELEASE);
        //手机型号:
        ret.append(",phoneBrand:" + android.os.Build.BRAND);
        ret.append(",phoneModel:" + android.os.Build.MODEL);

//      ret.append(",phoneDevice:" + android.os.Build.DEVICE);
//      ret.append(",phoneID:" + android.os.Build.ID);
//      ret.append(",phoneBootLoader:" + android.os.Build.BOOTLOADER);
//      ret.append(",phoneBoard:" + android.os.Build.BOARD);
//      ret.append(",phoneCpuAbi:" + android.os.Build.CPU_ABI);
//      ret.append(",phoneCpuAbi2:" + android.os.Build.CPU_ABI2);
//      ret.append(",phoneDisplay:" + android.os.Build.DISPLAY);
//      ret.append(",phoneFingerPrint:" + android.os.Build.FINGERPRINT);
//      ret.append(",phoneHardware:" + android.os.Build.HARDWARE);
//      ret.append(",phoneHost:" + android.os.Build.HOST);
//      ret.append(",phoneManufacturer:" + android.os.Build.MANUFACTURER);
//      ret.append(",phoneProduct:" + android.os.Build.PRODUCT);
//      ret.append(",phoneRadio:" + android.os.Build.RADIO);
//      ret.append(",phoneSerial:" + android.os.Build.SERIAL);
//      ret.append(",phoneTags:" + android.os.Build.TAGS);
//      ret.append(",phoneTime:" + android.os.Build.TIME);
//      ret.append(",phoneType:" + android.os.Build.TYPE);
//      ret.append(",phoneUser:" + android.os.Build.USER);
//      ret.append(",phoneGetRadioVersion:" + android.os.Build.getRadioVersion());

        //app 版本号
        try {
            PackageInfo pkgInfo = Constants.GLOBAL_CONTEXT.getPackageManager().getPackageInfo(Constants.GLOBAL_CONTEXT.getPackageName(), 0);
            ret.append(",appVersionName:" + pkgInfo.versionName);
            ret.append(",appVersionCode:" + pkgInfo.versionCode);
        } catch (NameNotFoundException e) {
        }

        //uuid
        TelephonyManager telephonyManager = (TelephonyManager) Constants.GLOBAL_CONTEXT.getSystemService(Context.TELEPHONY_SERVICE);
        ret.append(",uuid:" + telephonyManager.getDeviceId());

        return ret.toString();
    }

    //避免出错的substring
    public static String subString(String src, int start, int to){
        if(src != null){
            int len = src.length;
            int wantLen = to - start + 1;
            if(wantLen < len){
                to = len - 1;
                wantLen = to - start + 1;
            }
            if(to >= start){
                return src.substring(start, wantLen);
            }else{
                return null;
            }
        }
        return null;
    }

    public static int getStatusBarHeight() {
        Class<?> c = null;
        Object obj = null;
        Field field = null;
        int x = 0, sbar = 0;
        try {
            c = Class.forName("com.android.internal.R$dimen");
            obj = c.newInstance();
            field = c.getField("status_bar_height");
            x = Integer.parseInt(field.get(obj).toString());
            sbar = context.getResources().getDimensionPixelSize(x);
        } catch (Exception e1) {
            e1.printStackTrace();
        }
        if (sbar == 0) {
            sbar = Util.dp(20);
        }
        return sbar;
    }

    //获取屏幕高度
    public static int getScreenHeight() {
        return context.getResources().getDisplayMetrics().heightPixels;
    }

    public static int getScreenWidth() {
        return context.getResources().getDisplayMetrics().widthPixels;
    }

    //dp sp px 之间转换
    public static int px2dp(int px) {
        return (int) (1.0f * px / context.getResources().getDisplayMetrics().density + 0.5f);
    }

    public static int dp2px(int dp) {
        return (int) (1.0f * dp * context.getResources().getDisplayMetrics().density + 0.5f);
    }

    public static int sp2px(int sp) {
        return (int) (1.0f * sp * context.getResources().getDisplayMetrics().scaledDensity + 0.5f);
    }

    public static int px2sp(int px) {
        return (int) (1.0f * px / context.getResources().getDisplayMetrics().scaledDensity + 0.5f);
    }

    public static int sp2dp(int sp) {
        return px2dp(sp2px(sp));
    }

    public static int dp2sp(int dp) {
        return px2sp(dp2px(dp));
    }

    public static int dp(int dp) {
        return dp2px(dp);
    }

    public static int sp(int sp) {
        return sp2px(sp);
    }
    //读取asstes图片
    public static Bitmap getImageFromAssetsFile(String fileName, Context context) {
        Bitmap image = null;
        AssetManager am = context.getResources().getAssets();
        try {
            InputStream is = am.open(fileName);
            image = BitmapFactory.decodeStream(is);
            is.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return image;
    }

    //验证邮箱格式
    public static boolean isEmail(String email) {
        String str = "^([\\w-\\.]+)@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.)|(([\\w-]+\\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\\]?)$";
        Pattern p = Pattern.compile(str);
        Matcher m = p.matcher(email);
        return m.matches();

    }
    //验证手机号码格式
    public static boolean isMobileNO(String mobiles) {
        Pattern p = Pattern
                .compile("^1(3[0-9]|4[57]|5[0-35-9]|8[025-9]|7[0-9])\\d{8}$");
        Matcher m = p.matcher(mobiles);
        return m.matches();
    }
}
//iOS: Utils.h
@interface Utils:NSObject

//下面是一系列回调函数block
typedef void (^VoidIntCallback) (NSUInteger);
typedef void (^VoidCallback) ();
typedef void (^VoidStringCallback) (NSString *);
typedef void (^VoidBoolCallback) (BOOL);
typedef void (^VoidIdCallback) (id);

//获取当前设备名称
+ (NSString *)getDeviceName;
//获取设备信息
+(NSString *)getClientInfo;
+(int)getRandomNumber:(int)from to:(int)to;
//获取文件夹的大小
+(float) folderSizeAtPath:(NSString*) folderPath;
+(long long) fileSizeAtPath:(NSString*) filePath;
//清除缓存
+(NSString *)clearCache;
//判断输入是否合法
+(BOOL) isValidPhone:(NSString *)num;
+(BOOL) isValidEmail:(NSString *)email;

//一个页面中局部view显示/隐藏时所用的动画。默认采取渐显/渐隐的方式
+(void) createShowAnimForViewTypeChangeWithOneView:(UIView *) view andComplete:(VoidCallback) completeCb;
+(void) createHideAnimForViewTypeChangeWithOneView:(UIView *) view andComplete:(VoidCallback) completeCb;
+(void) createAnimForViewTypeChangeWithFromView:(UIView *) fromView toView:(UIView *)toView andComplete:(VoidCallback) completeCb;
@end
//iOS: Utils.m
#import "Utils.h"
#import <ADSupport/ASIdentifierManager.h>
@implements Utils

//这个函数是从网上找的代码,不是很准确,使用的方法也奇怪,这里只是表示一个意思,可令getClientInfo调用。
//想要更正确的代码,请自行查找。
+ (NSString *)getDeviceName{
    CGRect rect = [[UIScreen mainScreen] bounds];
    CGFloat width = rect.size.width;
    CGFloat height = rect.size.height;

    //get current interface Orientation
    UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation];
    //unknown
    if (UIInterfaceOrientationUnknown == orientation) {
        return @"unknown";
    }

    //    portrait  width * height
    //    iPhone4:320*480
    //    iPhone5:320*568
    //    iPhone6:375*667
    //    iPhone6Plus:414*736

    //portrait
    if (UIInterfaceOrientationPortrait == orientation) {
        if (width ==  320.0f) {
            if (height == 480.0f) {
                return @"iphone4/iPhone4s";//iphone4
            } else {
                return @"iPhone5/iPhone5s";
            }
        } else if (width == 375.0f) {
            return @"iPhone6/iPhone6s";
        } else if (width == 414.0f) {
            return @"iPhone6plus/iPhone6sPlus";
        }
    } else if (UIInterfaceOrientationLandscapeLeft == orientation || UIInterfaceOrientationLandscapeRight == orientation) {//landscape
        if (height == 320.0) {
            if (width == 480.0f) {
                return @"iphone4/iPhone4s";
            } else {
                return @"iPhone5/iPhone5s";
            }
        } else if (height == 375.0f) {
            return @"iPhone6/iPhone6s";
        } else if (height == 414.0f) {
            return @"iPhone6plus/iPhone6sPlus";
        }
    }
    return -1;
}
//获取设备信息
+(NSString *)getClientInfo{
    NSMutableString *ret = [[NSMutableString alloc]init];
    [ret appendString:@"os:iphone"];
    [ret appendFormat:@",osVersion:%f", [[[UIDevice currentDevice] systemVersion] floatValue]];
    [ret appendString:@",phoneBrand:iphone"];
    [ret appendFormat:@",phoneModel:%@", [self getDeviceName]];
    [ret appendFormat:@",appVersionName:%@", [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]];
    [ret appendFormat:@",appVersionCode:%@", [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"]];
    NSString * adId = [[[ASIdentifierManager sharedManager] advertisingIdentifier] UUIDString];
    NSString * deviceId = [[UIDevice currentDevice].identifierForVendor UUIDString];
    [ret appendFormat:@",deviceId:%@", deviceId];
    NSString *identifier = nil;
    if(!adId){
        identifier = deviceId;
    }else{
        identifier = adId;
        [ret appendFormat:@",adId:%@", adId];
    }
    [ret appendFormat:@",uuid:%@", identifier];
    return ret;
}

+ (long long) fileSizeAtPath:(NSString*) filePath{
    NSFileManager* manager = [NSFileManager defaultManager];
    if ([manager fileExistsAtPath:filePath]){
        return [[manager attributesOfItemAtPath:filePath error:nil] fileSize];
    }
    return 0;
}

+(int)getRandomNumber:(int)from to:(int)to{
    return (int)(from + (arc4random() % (to - from + 1)));
}

//遍历文件夹获得文件夹大小,返回多少M
+ (float ) folderSizeAtPath:(NSString*) folderPath{
    NSFileManager* manager = [NSFileManager defaultManager];
    if (![manager fileExistsAtPath:folderPath]) return 0;
    NSEnumerator *childFilesEnumerator = [[manager subpathsAtPath:folderPath] objectEnumerator];
    NSString* fileName;
    long long folderSize = 0;
    while ((fileName = [childFilesEnumerator nextObject]) != nil){
        NSString* fileAbsolutePath = [folderPath stringByAppendingPathComponent:fileName];
        folderSize += [self fileSizeAtPath:fileAbsolutePath];
    }
    return folderSize/(1024.0*1024.0);
}

+ (NSString *)clearCache
{
    //清除缓存目录
    NSArray *searchPaths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
    NSString *searchPath = [searchPaths lastObject];

    NSString *str = [NSString stringWithFormat:@"缓存已清除%.1fM", [self folderSizeAtPath:searchPath]];
    NSLog(@"%@",str);
    NSArray *files = [[NSFileManager defaultManager] subpathsAtPath:searchPath];
    for (NSString *p in files) {
        NSError *error;
        NSString *currPath = [searchPath stringByAppendingPathComponent:p];
        if ([[NSFileManager defaultManager] fileExistsAtPath:currPath]) {
            BOOL ret = [[NSFileManager defaultManager] removeItemAtPath:currPath error:&error];
            YYLog(@"移除文件 %@ ret= %d", currPath, ret);
        }else{
            YYLog(@"文件不存在 %@", currPath);
        }
    }
    return str;
}
+(BOOL) isValidNum:(NSString *)num{
    const char *cvalue = [num UTF8String];
    int len = (int)strlen(cvalue);
    for (int i = 0; i < len; i++) {
        if(cvalue[i] < '0' || cvalue[i] > '9'){
            return NO;
        }
    }
    return YES;
}
+(BOOL) isValidPhone:(NSString *)num{
    if (!num) {
        return NO;
    }
    const char *cvalue = [num UTF8String];
    int len = (int)strlen(cvalue);
    if (len != 11) {
        return NO;
    }
    if (![Util isValidNum:num])
    {
        return NO;
    }
    NSString *preString = [[NSString stringWithFormat:@"%@",num] substringToIndex:2];
    if ([preString isEqualToString:@"13"] ||
        [preString isEqualToString: @"15"] ||
        [preString isEqualToString: @"18"] ||
        [preString isEqualToString: @"17"])
    {
        return YES;
    }
    else
    {
        return NO;
    }
    return YES;
}

+ (BOOL) isValidEmail:(NSString *)e
{
    if (!e) {
        return NO;
    }
    NSArray *array = [e componentsSeparatedByString:@"."];
    if ([array count] >= 4) {
        return NO;
    }
    NSString *emailRegex = @"[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,4}";
    NSPredicate *emailTest = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", emailRegex];
    return [emailTest evaluateWithObject:e];
}

@end

代码清单:

    //android: 
        LogUtils.java
        TimeUtils.java
        Utils.java
    //iOS:
        LogUtils.h
        LogUtils.m
        TimeUtils.h
        TimeUtils.m
        Utils.h
        Utils.m

至此框架基本搭建完毕,可以快乐地写页面去啦。
当然在项目进行的过程中,也需要慢慢给这个还瘦弱的框架添枝加叶。
让它慢慢壮大,更加完整。
等经过一个或2个项目的洗礼,就会成为一个完整的,不错的框架了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值