僅需6步,教你輕易撕掉app開發框架的神祕面紗(6):各種公共方法及工具類的封裝

爲什麼要封裝公共方法

封裝公共方法有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個項目的洗禮,就會成爲一個完整的,不錯的框架了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章