APP 启动页开发与白屏优化

1,020 阅读8分钟

一、背景

技术栈前提——原生 Webview + h5 实现方案, 启动页的开发是APP区分h5很重要的东西,也是很有必要的, 能有效减少用户的等待时间, 因为套壳Webview的APP,不仅要花时间在壳的初始化启动上,还有一部分时间花在加载网页上,所以不可避免的会打开比较慢, 容易产生白屏, 对于用户来说是非常不友好的,下面将对安卓和ios上的启动页开发以及其中遇到的问题进行梳理整理,主要解决的问题有端的启动页开发, webview加载完成取消启动页的,国行ios在首次打开时需要用户网络授权问题,ios 长时间后台打开产生白屏的问题

初版启动页开发及问题

juejin.cn/post/738463…

第一版开发主要是实现功能, 启动页的出现时间设置死了,问题有两个, 无法在启动页展示的同时加载内容也就是webview的内容, 当启动页关闭后, 还要花些时间渲染,这段时间容易产生白屏问题, 尤其是ios 第一次用户下载时, 第二个问题是,时间设置死无法动态调节, 有时候白屏,有时候浪费时间等待

二、ios 启动页开发

1、新建启动页视图

创建和初始化一个新的视图对象,加载一张启动页图片

image.png

SplashView.m 文件

#import "SplashView.h"
@interface SplashView()
@property(nonatomic, strong, readwrite)UIButton *button;

@end

@implementation SplashView

-(instancetype)initWithFrame:(CGRect)frame{
    self = [super initWithFrame:frame];
    if(self) {
        self.image = [UIImage imageNamed:@"icon.bundle/splash.png"];
        // 视图进行交互
        self.userInteractionEnabled = YES;
    }
    return self;
}

#pragma mark -
-(void)_removeSplashView{
    [self removeFromSuperview];
}

@end

2、管理启动页的展示销毁逻辑

新版本的OC项目已经将AppDelegate中的一部分视图功能分离到了SceneDelegate中了,所以一些查到的资料可能已经不适用了

SceneDelegate.m 文件

// 引入SplashView.h 文件
#import "SplashView.h"
// 添加 SplashView 属性
@property (strong, nonatomic) SplashView *splashView;

在 willConnectToSession 生命周期中加载ViewController主视图(其中处理加载webview的逻辑), 然后同步添加 SplashView 启动页,在增加一个消息监听,当webview加载完成,通知关闭启动页

- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions {
    [self initWindowScreen: scene];
};

-(void)initWindowScreen :(UIScene *)scene {
    UIWindowScene *windowScene = (UIWindowScene *)scene;
    self.window = [[UIWindow alloc] initWithWindowScene:windowScene];
    ViewController *launchVC = [[ViewController alloc] init];
    self.window.rootViewController    = launchVC;
    self.window.backgroundColor       = [UIColor whiteColor];
    [self.window makeKeyAndVisible];
    // 添加一个闪屏页面
    [self showSplashView];
    // 添加消息 注意在 dealloc 销毁时移除掉消息的监听
    [[NSNotificationCenter defaultCenter] addObserver:self selector: @selector(handleRemoveSplashView) name:@"RemoveSplashViewNotification" object:nil];
}

处理showSplashView 闪屏页方法,初始化执行代码添加到 window 上, 设置 10s 后关闭(防止通知异常导致启动页无法关闭的情况)

- (void)showSplashView  {
    self.splashView = [[SplashView alloc] initWithFrame:self.window.bounds];
    [self.window addSubview:self.splashView];
    // 10秒后关闭这个页面(如果需要)
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [self handleRemoveSplashView];
    });
}

移除启动页视图的方法, sceneDidDisconnect 断开链接的场景也移除启动页

- (void)handleRemoveSplashView {
    // 从父视图中移除闪屏页面
    [self.splashView removeFromSuperview];
    self.splashView = nil; // 避免野指针,将引用置为 nil
};
- (void)sceneDidDisconnect:(UIScene *)scene {
    [self handleRemoveSplashView];
}

在webView 的加载完成的方法中,通知移除启动页

- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation{
   [[NSNotificationCenter defaultCenter] postNotificationName:@"RemoveSplashViewNotification" object:nil];
}

以上便可实现加载启动页,和webview加载完成取消启动页的逻辑,用户也不会感知白屏,下面是开发中遇到的一些问题的修复与记录

3、国行iphone 首次安装需要授权网络,否则无法加载webview,出现白屏

image.png

解决思路: 1、判断是否是首次启动 2、首次启动检测当前网络状态 3、有网络并加载webview 4、无网络,等待2秒后继续请求网络,等待用户同意授权

- (void)viewDidLoad {
    [super viewDidLoad];
    // 检查是否是首次安装
    if ([self isFirstLaunch]) {
        // 检查网络状态
        [self checkNetworkStatus];
        return;
    }
    [self setNotFirstLaunch];
}

3-1、 isFirstLaunch 方法判断是否是首次安装

通过检查用户默认设置中的一个标记来确定是否是第一次启动应用,并确保这个检查只在应用的生命周期中执行一次

- (BOOL)isFirstLaunch {
    static BOOL hasLaunchedBefore = NO;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        hasLaunchedBefore = [[NSUserDefaults standardUserDefaults] boolForKey:@"hasLaunchedBefore"];
    });
    return !hasLaunchedBefore;
}

3-2、checkNetworkStatus 方法检查当前网络状态

创建SCNetworkReachabilityRef 对象,通过baidu域名检查当前网络可达性,网络可达并且不需要链接则加载 webview 否则,每两秒检测网络状态

// 判断网络状态
- (void)checkNetworkStatus {
    SCNetworkReachabilityRef reachabilityRef = SCNetworkReachabilityCreateWithName(NULL, "www.baidu.com");
    if (reachabilityRef != NULL) {
        SCNetworkReachabilityFlags flags;
        // 变量用于表示是否成功获取了网络状态标志
        BOOL didReach = SCNetworkReachabilityGetFlags(reachabilityRef, &flags);
        // 释放 reachabilityRef 对象
        CFRelease(reachabilityRef);
        reachabilityRef = NULL;
        if (didReach) {
            // 网络是否可达
            BOOL isReachable = ((flags & kSCNetworkFlagsReachable) != 0);
            // 网络需要链接
            BOOL needsConnection = ((flags & kSCNetworkFlagsConnectionRequired) != 0);
            // 这行代码检查网络是否可达且不需要建立连接,如果是,则表示设备有有效的网络连接
            if (isReachable && !needsConnection) {
                // 有网络,加载WebView
                [self loadWebView];
            } else {
                // 无网络,等待2秒后继续请求网络
                [NSTimer scheduledTimerWithTimeInterval:2.0
                                              target:self
                                            selector: @selector(checkNetworkStatus)
                                            userInfo:nil
                                             repeats:NO];
            }
        } else {
            NSLog(@"无法判断网络状态,可能是权限问题或其他原因");
            // 无法判断网络状态,可能是权限问题或其他原因
            exit(0);
        }
    }
}

3-3、setNotFirstLaunch 当不是首次时更新标志加载webview

- (void)setNotFirstLaunch {
    [self loadWebView];
    // 用户偏好设置 hasLaunchedBefore 为true 表示已经加载过
    [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"hasLaunchedBefore"];
    // 调用 synchronize 方法来同步用户偏好设置的存储 将状态同步到磁盘
    [[NSUserDefaults standardUserDefaults] synchronize];
}

3-4、当webview加载出现异常,继续进入网络状态的检查中,重新加载webview

- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error {
    // 页面加载失败时调用
    NSLog(@"页面加载失败: %@", error.localizedDescription);
    if ([error.domain isEqualToString:NSURLErrorDomain] && error.code == -1009) {
           [self checkNetworkStatus];
    }
}

4、部分机型长时间后台唤起白屏

4-1、由于内存回收,webview 崩溃

// 内存占用过大,页面即将白屏
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView API_AVAILABLE(macosx(10.11), ios(9.0)){
     [self.webView reload];
};

4-2、当webview加载出现异常,继续进入网络状态的检查中,重新加载webview

- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error {
    // 页面加载失败时调用
    NSLog(@"页面加载失败: %@", error.localizedDescription);
    if ([error.domain isEqualToString:NSURLErrorDomain] && error.code == -1009) {
           [self checkNetworkStatus];
    }
}

4-3、 通过后台时间手动重新重启启动页和webview

1、即将从前台进入后台时记录当前时间
- (void)sceneWillResignActive:(UIScene *)scene {
    // 记录应用即将进入后台的时间
    self.backgroundTime = [NSDate date];
}
2、即将从后台进入前台时检查是否超过了规定的时间
- (void)sceneWillEnterForeground:(UIScene *)scene {
   NSTimeInterval timeInBackground = [[NSDate date] timeIntervalSinceDate:self.backgroundTime];
   BOOL shouldShowSplash = timeInBackground > 600.0; // 10分钟
   if (shouldShowSplash && !self.splashView.superview) {
       // 重新初始化视图
       [self initWindowScreen: scene];
   }
}

三、android 启动页开发

实现思路, android 的启动页逻辑和 ios 的设计思路有些不一样, android 上新建了启动页的视图 SpashActivity, 然后在启动页视图上加载了一个activity_splash 的 layout 布局设置了当前的闪屏页的图片和文字, 开始由于是写死的时间在打开 MainActivity ,于是加载webview的时候会出现很长时间白屏, 并且不是同时加载的, 为了解决这个问题,于是在 SpashActivity 上就开始加载 webview, 当webview 执行完成,导航到 MainActivity 上

1、启动页视图中加载webview成功后跳转到 MainActivity中

SplashActivity.kt 文件

// 创建单例
import com.kalodata.kalodata_android.utils.WebViewSingleton

//...
@SuppressLint("CustomSplashScreen")
class SplashActivity : ComponentActivity() {
    private lateinit var webView: WebView
    private var hasFinished = false
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_splash)
        webView = WebViewSingleton.getInstance(this).webView
webView.loadUrl("https://www.baidu.com/")
        webView?.webViewClient = object : WebViewClient() {
            override fun onPageFinished(view: WebView, url: String) {
                super.onPageFinished(view, url)
                Handler().postDelayed( {
// 检查标志位,如果已经执行过跳转,则不执行 保证执行一次
                    if (!hasFinished) {
                        hasFinished = true // 设置标志位为已执行
                        startActivity(Intent(this@SplashActivity, MainActivity::class.java))
                    }
                    finish()
                } ,1000)
            }
        }
    }
}

通过单例模式 设计 WebViewSingleton.java

package com.kalodata.kalodata_android.utils;
import android.content.Context;
import android.webkit.WebView;

public class WebViewSingleton {
    private static WebViewSingleton instance;
    private WebView webView;
    // 存储Context,这里使用Application Context作为示例
    private Context context;

    // 私有构造函数,接收一个Context参数
    private WebViewSingleton(Context ctx) {
        this.context = ctx; // 保存Context供后续使用
        initializeWebView();
    }

    // 初始化WebView的方法
    private void initializeWebView() {
        webView = new WebView(context); // 使用提供的Context创建WebView
        webView.getSettings().setJavaScriptEnabled(true); // 根据需要设置WebView
        webView.getSettings().setDomStorageEnabled(true);   // 支持 JavaScript
    }

    // 静态方法,用于获取单例对象
    public static synchronized WebViewSingleton getInstance(Context ctx) {
        if (instance == null) {
            instance = new WebViewSingleton(ctx); // 使用传入的Context创建实例
        }
        return instance;
    }

    // 获取WebView的公共方法
    public WebView getWebView() {
        return webView;
    }
}

AndroidManifest.xml

<!--   添加网络权限-->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

// ...

<application
        android:networkSecurityConfig="@xml/network_security_config"
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.xxx_android"
        tools:targetApi="31" >
        <activity android:name=".SplashActivity"
            android:exported="true"
            android:launchMode="standard"
            >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name=".MainActivity"
            android:exported="true"
            android:launchMode="standard">
            <intent-filter>
                <action android:name="android.intent.action.VIEW"/>
                <category android:name="android.intent.category.DEFAULT"/>
                <category android:name="android.intent.category.BROWSABLE"/>
                <data android:scheme="kalodata"/>
            </intent-filter>
        </activity>
    </application>

2、MainActivity中加载要真正要展示的webview

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // webview
    setContentView(R.layout.activity_main)
    webView = findViewById(R.id.webview)
    webView.settings?.javaScriptEnabled = true   // 支持 JavaScript
    // 设置是否启用 DOM 存储
    // DOM 存储是一种在 Web 应用程序中存储数据的机制,它使用 JavaScript 对象和属性来存储和检索数据
    webView.settings?.domStorageEnabled = true
    // 设置 WebView 是否可以获取焦点 ( 自选 非必要 )
    webView?.isFocusable = true
    // 设置 WebView 中的滚动条样式 ( 自选 非必要 )
    // SCROLLBARS_INSIDE_OVERLAY - 在内容上覆盖滚动条 ( 默认 )
    webView?.scrollBarStyle = View.SCROLLBARS_INSIDE_OVERLAY
    // 设置页面自适应
    // Viewport 元标记是指在 HTML 页面中的 <meta> 标签 , 可以设置网页在移动端设备上的显示方式和缩放比例
    // 设置是否支持 Viewport 元标记的宽度
    webView.settings?.useWideViewPort = true
    // WebViewClient 是一个用于处理 WebView 页面加载事件的类
    webView?.webViewClient = object : WebViewClient() {
        override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
            // 检查链接是否是 mailto 类型
            if (url.startsWith("mailto:")) {
                return true; // 返回 true 以拦截链接,不让它打开浏览器
            }
            // 其他链接可以正常处理
            return super.shouldOverrideUrlLoading(view, url);
        }
        override fun onPageFinished(view: WebView, url: String) {
            super.onPageFinished(view, url)
            // 页面加载完成后可以在这里执行一些操作,例如调用JavaScript函数
        }
    }
}

Webview 视图容器

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <WebView
        android:id="@+id/webview"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

启动页视图编辑

activity_splash.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ImageView
        android:id="@+id/imageView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:src="@drawable/splash_image"
        android:scaleType="centerCrop" />
</LinearLayout>