iOS Launch Screen.storyBoard白屏/黑屏問題修復(fù)

需求

棄用LaunchImage啟動(dòng)圖方式,改用Launch Screen.storyBoard啟動(dòng)圖方式,同時(shí)不對(duì)開屏廣告造成影響。

官方介紹

https://developer.apple.com/design/human-interface-guidelines/ios/visual-design/launch-screen/

注:該方案僅適用iOS13.0及以上版本。

iOS 12及以下系統(tǒng)沙盒目錄(Library/Caches/Snapshots)為不可讀、不可寫、可刪除(但是開發(fā)者無權(quán)刪除)權(quán)限,故本套方案不起作用。

若調(diào)試中出現(xiàn)問題,可卸載app,重啟手機(jī),重新裝載app測(cè)試。

背景

現(xiàn)階段網(wǎng)上流行的storyboard開屏:

1. 開屏圖片從主目錄讀取,一張圖適配所有界面。首先這種方式是不存在緩存的,實(shí)時(shí)取肯定是準(zhǔn)確的圖。但是作為業(yè)務(wù)方,這種滿足不了自定義的四季樣式,還會(huì)存在拉伸。
2. 圖片存在XCAsset中,每次使用完,都會(huì)刪除沙盒目錄(Library/SplashBoard)文件,雖然有版本限制,但是大概率存在黑/白屏風(fēng)險(xiǎn),也是不達(dá)標(biāo)。

PS:該文方案都是以xcasset緩存開屏為基礎(chǔ)的,首次storyboard替換LuanchImage,且未做沙盒緩存清空的,參考文章。如果之前對(duì)沙盒有操作的,可能會(huì)有異常。

Apple會(huì)將Launch Screen.storyBoard作為與圖片類型類似的二進(jìn)制文件,進(jìn)行加載,執(zhí)行是在main函數(shù)之前,所以不參與業(yè)務(wù)代碼控制。適配就不做多余的闡述了。

一、問題

在iOS應(yīng)用程序中修改了啟動(dòng)屏幕LaunchScreen.storyboad中的某些內(nèi)容時(shí),我都會(huì)遇到一個(gè)問題:系統(tǒng)會(huì)緩存啟動(dòng)圖像,即使刪除了該應(yīng)用程序,它實(shí)際上也很難清除原來的緩存,猜測(cè)會(huì)有多級(jí)緩存。

二、分析

我們可以改動(dòng)的緩存只有本地的沙盒目錄(/Library/SplashBoard的Snapshots),打印的日志:

2020-05-19 09:25:38.138233+0800 luanchTest[3892:1751265] cache Path == /var/mobile/Containers/Data/Application/BCA1FD18-2A24-43A9-B844-65A5D38A5B9D/Library/SplashBoard,subpath == (
Snapshots,
"Snapshots/wei.jiang.luanchTest - {DEFAULT GROUP}",
"Snapshots/wei.jiang.luanchTest - {DEFAULT GROUP}/B6C097B0-5F66-4740-A20A-5FBDAA2EE484@2x.ktx",
"Snapshots/wei.jiang.luanchTest - {DEFAULT GROUP}/83BE4383-44CF-41E0-9E3F-235E6D132B61@2x.ktx",
"Snapshots/wei.jiang.luanchTest - {DEFAULT GROUP}/D2761A77-E07C-4D3B-9551-C90F643218BF@2x.ktx",
"Snapshots/wei.jiang.luanchTest - {DEFAULT GROUP}/A3E1D437-7876-481C-A5AA-4940E69770A6@2x.ktx",
"Snapshots/sceneID:wei.jiang.luanchTest-default",
"Snapshots/sceneID:wei.jiang.luanchTest-default/1970FB13-BDA2-44F0-B998-ECFEDE1068A6@2x.ktx",
"Snapshots/sceneID:wei.jiang.luanchTest-default/2890B753-D9F5-4030-881A-FA35EF24D922@2x.ktx",
"Snapshots/sceneID:wei.jiang.luanchTest-default/downscaled",
"Snapshots/sceneID:wei.jiang.luanchTest-default/downscaled/BBF126DF-765D-4607-A052-02CA0426DAA5@2x.ktx",
"Snapshots/sceneID:wei.jiang.luanchTest-default/downscaled/4E66BDD7-D46C-4F16-917F-BE23E8E0F1B9@2x.ktx"
)

一目了然,Snapshots就是我們操作的文件夾,將ktx導(dǎo)出轉(zhuǎn)換后綴,可以看到就是我們要的啟動(dòng)圖,因?yàn)槲疫@里是用的多個(gè)模擬器所以會(huì)有多張,正常的會(huì)只有一張。

注:如果項(xiàng)目工程是以xcode11方式新建的話,就需要處理UIScene的截圖,我們的項(xiàng)目沒有用到UIScene方式,所以沒有做相應(yīng)處理。

三、解決

思路:

推測(cè)系統(tǒng)在沙盒目錄有圖的時(shí)候,會(huì)從沙盒拿圖。所以我們?cè)诒3衷心夸浀那闆r下,只做圖片內(nèi)容的替換(有坑,有同事之前一直用的主目錄方式&&有過沙盒目錄的刪除操作,替換到這種方式每次首次讀圖都會(huì)空白屏)。

1.取圖:

每次展示自定義啟動(dòng)頁時(shí),優(yōu)先從Snapshots里拿image進(jìn)行展示(無圖是從storyBoard拿圖),進(jìn)行無縫銜接;

每次啟動(dòng)時(shí),根據(jù)UI指定的布局,生成一張圖片,作為展位圖展示。(開屏啟動(dòng)時(shí),有系統(tǒng)狀態(tài)條變化,直接獲取luanchScreen.storyboard的圖片會(huì)有狀態(tài)條高度缺失引起圖片生成異常)

2.替換:

當(dāng)次展示完啟動(dòng)圖時(shí),進(jìn)行更新(將storyBoard的image同步到Snapshots)。避免重復(fù)無用操作做了版本控制。

當(dāng)次展示完啟動(dòng)圖時(shí),進(jìn)行更新(將我們生成的圖片存入SnapShots的沙盒目錄下)。避免重復(fù)無用操作做了版本控制。

不多說了,直接上代碼吧。

//
//  MJLaunchScreenTool.h
//  MojiWeather
//
//  Created by wei.jiang on 2020/5/14.
//  Copyright ? 2020 Moji Fengyun Technology Co., Ltd. All rights reserved.
//

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN
/*
* 配套LaunchScreen.storyBoard的啟動(dòng)加載方式
*/
@interfaceMJLaunchScreenTool : NSObject

/*
*  獲取沙盒/SplashBoard/Snapshots目錄下的啟動(dòng)圖(不推薦,有statusBar改動(dòng)會(huì)有異常)
*/
+ (UIView *)getCacheLaunchImageByLirbrary;

/*
*  獲取LaunchScreen.storyBoard對(duì)應(yīng)啟動(dòng)圖(推薦,忽略statusbar影響)
*/
+ (UIView *)getLaunchImageIngoreStatusBar:(BOOL)isHighQuailty;

/*
* 更替修正storyboard的緩存啟動(dòng)圖(storyboard作啟動(dòng)圖的情況下,不可刪除)
*/
+ (void)updateSplashBoardCache:(BOOL)fetImageFromStoryBoard;

@end
NS_ASSUME_NONNULL_END
//
//  MJLaunchScreenTool.m
//  MojiWeather
//
//  Created by wei.jiang on 2020/5/14.
//  Copyright ? 2020 Moji Fengyun Technology Co., Ltd. All rights reserved.
//

#import "MJLaunchScreenTool.h"
#import "UIView+ScreenShot.h"
#import "MJLottieAnimationManager.h"

#define kSplashBoard_Version @"kSplashBoard_Version"
#define kMJSplashBoardCopyImageName @"mj_cover_install_first_image.png"
@implementation MJLaunchScreenTool

//從storyboard獲取啟動(dòng)圖(不建議使用,如果外部有改動(dòng)statusbar獲取的圖片可能會(huì)出問題)
+ (UIView *)getLaunchImageByStoreBoard{
    UIStoryboard *launchScreenStoryBoard = [UIStoryboard storyboardWithName:@"LaunchScreen" bundle:[NSBundle mainBundle]];
    UIView *view = [launchScreenStoryBoard.instantiateInitialViewController view];
    return view;
}

+ (UIView *)getLaunchImageIngoreStatusBar:(BOOL)isHighQuailty{
    CGFloat scaleNumer = 1;
    if (isHighQuailty) {
        scaleNumer = 2;
    }
    CGFloat launchScreenWidth = SCREEN_WIDTH * scaleNumer;
    CGFloat launchScreenHeight = SCREEN_HEIGHT * scaleNumer;
    
    UIView *launchView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, launchScreenWidth, launchScreenHeight)];
    launchView.clipsToBounds = YES;
    launchView.backgroundColor = [UIColor whiteColor];
    //背景
    UIImageView *bgView = [[UIImageView alloc] initWithFrame:launchView.bounds];
    bgView.image = [UIImage imageNamed:@"splashBg_winter"];
    bgView.contentMode = UIViewContentModeScaleAspectFill;
    [launchView addSubview:bgView];
    //內(nèi)容區(qū)域
    UIImage *contentImage = [UIImage imageNamed:@"splashContent_winter"];
    UIImageView *contentView = [[UIImageView alloc] initWithImage:contentImage];
    CGFloat scale = contentImage.size.width/contentImage.size.height;
    CGFloat contentHeight = ((scale != 0)?(launchView.width/scale):contentImage.size.height);
    CGFloat contentTopInSafeArea = 18;//距離安全頂部的距離
    CGFloat contentTop = ([MojiGlobal getStatusBarHeightBySafeArea] + contentTopInSafeArea)*scaleNumer;
    contentView.frame = CGRectMake(0, contentTop, launchView.width, contentHeight);
    contentView.contentMode = UIViewContentModeScaleAspectFit;
    [launchView addSubview:contentView];
    //底部slogan
    //圖片高度124 ,底部34為安全區(qū)域適配,無安全區(qū)域的屏幕不展示底部34,只展示頂部90高度,
    //用容器containView去裁剪帶安全區(qū)域的圖片,否則iOS13以下系統(tǒng)的6p、7p、8p處理會(huì)有問題
    CGFloat bottomImageHeight = 90.0 * scaleNumer;
    CGFloat containViewHeight = bottomImageHeight + [MojiGlobal getBottomSafeHeight]*scaleNumer;
    UIView *bottomContainView = [[UIView alloc] initWithFrame:CGRectMake(0, launchScreenHeight - containViewHeight, launchScreenWidth, containViewHeight)];
    bottomContainView.clipsToBounds = YES;
    bottomContainView.backgroundColor = [UIColor clearColor];
    UIImageView *bottomImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"splashSloganMJ_winter"]];
    bottomImageView.contentMode = UIViewContentModeScaleAspectFill;
    bottomImageView.frame = CGRectMake(0, 0, launchScreenWidth, 124 * scaleNumer);
    [bottomContainView addSubview:bottomImageView];
    [launchView addSubview:bottomContainView];
    return launchView;
}

//從沙盒獲取啟動(dòng)圖
+ (UIView *)getCacheLaunchImageByLirbrary{
    if (![self isAvailable]) {
        return [self getLaunchImageIngoreStatusBar:NO];
    }
    NSString *cacheLaunchPath = [[self getCacheLaunchImageArrayPath] firstObject];
    UIImage *image = [[UIImage alloc] initWithContentsOfFile:cacheLaunchPath];
    UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
    imageView.frame = SCREEN_FRAME;
    return imageView;
}

#pragma mark - launchscreen.storyBoard 緩存清理、重置(版本更新時(shí)需調(diào)用)
//部分機(jī)型可能出現(xiàn)緩存同時(shí)用到2張截圖的情況
//所以在不改動(dòng)系統(tǒng)原有緩存數(shù)的情況下 僅做替換 防止出錯(cuò)
+ (void)updateSplashBoardCache:(BOOL)fetImageFromStoryBoard{
    if (![self isAvailable]) {
        NSString *cache = [NSString stringWithFormat:@"%@/Library/Caches/Snapshots/",NSHomeDirectory()];
        MJXLOG_INFO(@"Library/Caches/Snapshots 是否為可寫目錄 : %d",[[NSFileManager defaultManager] isWritableFileAtPath:cache]);
        MJXLOG_INFO(@"Library/Caches/Snapshots 是否為可讀目錄 : %d",[[NSFileManager defaultManager] isReadableFileAtPath:cache]);
        MJXLOG_INFO(@"Library/Caches/Snapshots 是否為可刪除目錄 : %d",[[NSFileManager defaultManager] isDeletableFileAtPath:cache]);
        return;
    }
    UIImage *mjLaunchImage = [self getMJPathSplashCacheImage];
    NSData *mjImageData = UIImagePNGRepresentation(mjLaunchImage);
    
    NSArray *cacheLauchPaths = [self getCacheLaunchImageArrayPath];
    if (mjImageData && cacheLauchPaths.count > 0) {
        // 校檢md5 不一致則替換
        for (NSString *path in cacheLauchPaths) {
            NSData *systemCacheData = [NSData dataWithContentsOfFile:path];
            
            if (![[systemCacheData mjl_MD5String] isEqualToString:[mjImageData mjl_MD5String]]) {
                if (![mjImageData writeToFile:path atomically:YES]) {
                    MJXLOG_INFO(@"storyBoard方式啟動(dòng)圖,寫入緩存失敗");
                }else{
                    MJXLOG_INFO(@"storyBoard方式啟動(dòng)圖,寫入緩存成功,files == %@",[[NSFileManager defaultManager] subpathsAtPath:[self splashShotCachePath]]);
                }
            }
        }
    }
}


#pragma mark - 路徑
+ (NSString *)splashShotCachePath{
    NSString *snapShotPath = nil;
    if ([UIDevice currentDevice].systemVersion.floatValue < 13.0) {
        snapShotPath = [NSString stringWithFormat:@"%@/Library/Caches/Snapshots/%@/",NSHomeDirectory(),[NSBundle mainBundle].bundleIdentifier];
    }else{
        //13.0以上系統(tǒng)
        snapShotPath = [NSString stringWithFormat:@"%@/Library/SplashBoard/Snapshots/%@ - {DEFAULT GROUP}/",NSHomeDirectory(),[NSBundle mainBundle].bundleIdentifier];
    }
    return snapShotPath;
}

// 獲取緩存的啟動(dòng)圖路徑多圖數(shù)組
+ (NSArray *)getCacheLaunchImageArrayPath{
    NSFileManager *defaultManager = [NSFileManager defaultManager];
    //splashBoard的緩存截圖路徑
    NSString * snapShotPath = [self splashShotCachePath];
    MJXLOG_INFO(@"library splashBoard path == %@, subFiles == %@",snapShotPath,[defaultManager subpathsAtPath:snapShotPath]);
    
    NSArray *snapShots = [defaultManager subpathsAtPath:snapShotPath];
    NSMutableArray *shotArray = [NSMutableArray array];
    for (NSString *shotNameStr in snapShots) {
        if ([shotNameStr hasSuffix:@".ktx"]) {
            //完整路徑數(shù)組
            [shotArray addObject: [NSString stringWithFormat:@"%@%@",snapShotPath,shotNameStr]];
        }
    }
    
    if (shotArray.count > 0) {
        return shotArray;
    }
    return nil;
}

#pragma mark - 備份路徑(業(yè)務(wù)需求)
+ (UIImage *)getMJPathSplashCacheImage{
    NSString *mjSplashCacheImagePath = [self getMJSplashBoardCacheImagePath];
    
    // 根據(jù)版本信息,判斷是否需要觸發(fā)更新操作
    BOOL hasUpdate = NO;
    NSUserDefaults *userDefault = [NSUserDefaults standardUserDefaults];
    NSString *splashVersion = [userDefault objectForKey:kSplashBoard_Version];
    // MOJI_VERSION為外部定義的版本號(hào) 也可以是bundleVersion
    if (![splashVersion isEqualToString:MOJI_VERSION] || !splashVersion) {
        hasUpdate = YES;
    }

    UIImage *image;
    if (hasUpdate) {
        //更新圖片
        image = [self.class transformLaunchViewToImageView];
        NSData *imageData = UIImagePNGRepresentation(image);
        if ([imageData writeToFile:mjSplashCacheImagePath atomically:YES]) {
            MJXLOG_INFO(@"splashBoard版本更新,自定義cache路徑更新成功,路徑:%@",mjSplashCacheImagePath);
            [userDefault setObject:MOJI_VERSION forKey:kSplashBoard_Version];
            [userDefault synchronize];
        }else{
            MJXLOG_INFO(@"splashBoard版本更新,自定義cache路徑更新失敗,路徑:%@",mjSplashCacheImagePath);
        }
    }else{
        NSFileManager *defaultManager = [NSFileManager defaultManager];
        if ([defaultManager fileExistsAtPath:mjSplashCacheImagePath]) {
            //路徑有圖片
            image = [UIImage imageWithContentsOfFile:mjSplashCacheImagePath];
        }else{
            //路徑無圖片
            image = [self.class transformLaunchViewToImageView];
            NSData *imageData = UIImagePNGRepresentation(image);
            if ([imageData writeToFile:mjSplashCacheImagePath atomically:YES]) {
                MJXLOG_INFO(@"splashBoard路徑無圖片,自定義cache路徑更新成功,路徑:%@",mjSplashCacheImagePath);
            }else{
                MJXLOG_INFO(@"splashBoard路徑無圖片,自定義cache路徑更新失敗,路徑:%@",mjSplashCacheImagePath);
                [defaultManager removeItemAtPath:mjSplashCacheImagePath error:nil];
            }
        }
    }
    return image;
}

+ (NSString *)getMJSplashBoardCacheImagePath{
    NSString *copyDirectoryPath = [NSString stringWithFormat:@"%@/Library/MJSplashBoard",NSHomeDirectory()];
    
    NSFileManager *defaultManager = [NSFileManager defaultManager];
    BOOL isDir = false;
    BOOL isDirExist = [defaultManager fileExistsAtPath:copyDirectoryPath
                                        isDirectory:&isDir];
    if (!isDirExist || !isDir) {
        [defaultManager createDirectoryAtPath:copyDirectoryPath
                  withIntermediateDirectories:YES
                                   attributes:nil
                                        error:nil];
    }
    NSString *copyFullPath = [NSString stringWithFormat:@"%@/%@",copyDirectoryPath,kMJSplashBoardCopyImageName];
    return copyFullPath;
}

+ (BOOL)isAvailable{
    if ([UIDevice currentDevice].systemVersion.floatValue >= 13.0) {
        return YES;
    }
    return NO;
}

#pragma mark - 生成image
+ (UIImage *)transformLaunchViewToImageView{
    UIView *launchView = [self getLaunchImageIngoreStatusBar:YES];
    UIGraphicsBeginImageContext(launchView.bounds.size);
    [launchView.layer renderInContext:UIGraphicsGetCurrentContext()];
    UIImage *launchImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return launchImage;
}

@end

四、用法

1. 版本更新邏輯

在開屏展示完成的時(shí)候,調(diào)用更新沙盒緩存的開屏截圖邏輯。

觸發(fā)更新的時(shí)機(jī):

  • 新用戶首次安裝
  • 版本更新后,首次啟動(dòng)
  • 非首次安裝,每次比對(duì)沙盒緩存圖片的md5和當(dāng)次開屏生成圖片的md5,若不一致時(shí),觸發(fā)更新;

2. 占位圖調(diào)用邏輯

UIImage *image = [MJLaunchScreenTool getLaunchImageIngoreStatusBar:YES];
_launchImageView.image = image;

3. 更新緩存(啟動(dòng)圖結(jié)束使用之后,調(diào)用)

[MJLaunchScreenTool updateSplashBoardCache:YES];

我們app開屏流程

  1. 系統(tǒng)storyBoard的開屏占位圖(系統(tǒng)launchSreen.storyboard)
  2. 開屏占位圖(代碼設(shè)置)
  3. 開屏廣告展示(代碼設(shè)置)
  4. 開屏結(jié)束,更新系統(tǒng)沙盒開屏占位圖(代碼設(shè)置)

更新緩存是在步驟4進(jìn)行操作的。

2020.06.04更新

測(cè)試中,我們發(fā)現(xiàn)現(xiàn)有的開屏流程存在問題。

問題1

在首次覆蓋安裝后,當(dāng)次的熱啟動(dòng)開屏?xí)霈F(xiàn),新老開屏閃變的問題。

流程1階段的時(shí)候,展示的是舊的開屏占位圖;
流程2階段的時(shí)候,展示的是新的開屏占位圖;
就出現(xiàn)了:
舊占位圖==>新占位圖==>開屏廣告圖片==>開屏消失的情況

猜想

1. 系統(tǒng)覆蓋安裝時(shí),由于上一個(gè)版本,系統(tǒng)沙盒(Library/SplashBoard)緩存了開屏。覆蓋安裝后,沙盒緩存還是上一次的開屏圖片;啟動(dòng)時(shí),系統(tǒng)依舊從沙盒拿取上一次開屏,加載到內(nèi)存,作為當(dāng)次開屏占位圖。
2. 但是殺死app后,系統(tǒng)重新從沙盒(Library/SplashBoard)拿圖,加載到內(nèi)存,這時(shí)候沙盒圖片已經(jīng)被我們更新。相當(dāng)于清理了緩存,相當(dāng)于更新了storyboard開屏占位圖。

思考

我們可以換個(gè)思路考慮,這個(gè)問題僅僅在 “首次覆蓋安裝” ,而且 “同為storyboard方式作為開屏方式” 的時(shí)候才會(huì)出現(xiàn)。
那我們是不是可以針對(duì) 首次覆蓋安裝 + 兩次皆為storyboard方式作為開屏 的情況做單獨(dú)處理呢?

思路

1. 首次覆蓋安裝的情況特殊處理:

版本號(hào)不一致時(shí),本次會(huì)copy系統(tǒng)沙盒(Library/SplashBoard)開屏圖至自定義沙盒路徑(我這里定義的是Library/JWSplashBoard)備份,且當(dāng)次開屏占位圖為該備份圖,在備份完之后,更新系統(tǒng)沙盒開屏圖片(刪除舊圖,添加新圖)。

2. 非首次啟動(dòng)時(shí),默認(rèn)之前處理方式

每次取系統(tǒng)沙盒路徑,并刪除備份占位圖(Library/JWSplashBoard路徑下)。

問題2

問題一的處理,只考慮到在系統(tǒng)沙盒目錄下,系統(tǒng)只為我們存了一張開屏圖。

但實(shí)際上,這個(gè)數(shù)目是不確定的,之后在我們其他項(xiàng)目團(tuán)隊(duì)的app上,復(fù)現(xiàn)多張開屏圖。且通過就widget方式打開app時(shí),系統(tǒng)就會(huì)在沙盒目錄下新增一張開屏圖(這張圖可能為正常,也可能為異常)。

解決:

我們引入新的解決方法,每次啟動(dòng)開屏展示完成后,都會(huì)對(duì)系統(tǒng)沙盒目錄的開屏圖進(jìn)行MD5比對(duì),若不一致,則更新。

這樣即使第一次打開app時(shí),出現(xiàn)了異常開屏圖,我們會(huì)在之后的代碼中將他進(jìn)行修復(fù)。

也就是說異常只會(huì)出現(xiàn)一次,至于這一次為什么會(huì)出現(xiàn)異常。因?yàn)檫@部分代碼蘋果未對(duì)我們開源,我們不清楚具體邏輯,無法修改,屬于蘋果公司內(nèi)部的問題。

但我們同事在跟蘋果團(tuán)隊(duì)的溝通中,對(duì)方表示這種問題只會(huì)出現(xiàn)在開發(fā)環(huán)境,線上無問題。

實(shí)際上,蘋果的線上環(huán)境也有問題,之前偶現(xiàn)過今日頭條的App Store版本會(huì)出現(xiàn)黑色圖塊。

測(cè)試點(diǎn):

區(qū)分版本

  • iOS12以下系統(tǒng),系統(tǒng)沙盒目錄相關(guān)權(quán)限未開放,完全由系統(tǒng)把控,我們無法對(duì)此版本做補(bǔ)丁。
  • iOS13系統(tǒng),系統(tǒng)開放了沙盒權(quán)限。我們可以每次比對(duì)系統(tǒng)沙盒的開屏圖和我們需要的開屏圖。不一致,就進(jìn)行替換,修復(fù)異常的問題。

--------------------------完結(jié)撒花-----------------------

適配參考:iOS13---LaunchScreen.storyboard 啟動(dòng)圖屏幕適配「一」

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容