标签: APP

  • 部分省份无法访问在线服务的解决方案

    周末被反应国内的服务福建省出现无法调用的情况,心中大概知道什么问题。这个问题经常出现,尤其是在福建,江苏,河南等省份。

    下面是简单的解决方案,分别为:HTTPS(SSL),CDN,ipv6,组建集群,客户端指定DNS

    HTTPS

    大部分国内的服务加上证书可以解决很多问题,没错这是真实情况…

    CDN

    最优的解决方案,目前可以查询到上面三个省份均有节点的服务商:

    1. 阿里云
    2. 腾讯云
    3. 七牛云

    其它服务商暂时没有查询,具体可以咨询官网客服。如果是前后端分离的项目,推荐购买接口加速的CDN就可以了。至此这个方案基本可以解决所有国内地区的访问难题!

    ipv6

    现在国内云厂商是提供免费ipv6的,且绝大部分用户实际上设备和网络状况均支持ipv6,如果可以使用ipv6进行访问也可以解决很大程度的DNS污染问题。

    后端集群

    在访问不到的地区购置服务器,组建集群,亦可解决问题,但是依然需要考虑被重复封禁的问题以及客户端因为地理位置解析不精准或者缓存等原因,并未选择最优线路的方案;亦或者选择的最优路线根本就是访问不到的路线。所以这个方案被排除在最后!(这里只是以解决访问问题为例,而非其他原因)

    客户端方案:指定可访问的DNS

    部分运营商会存在白名单/黑名单的情况,当然我们的备案服务和国内IP暂时可以排除黑名单的问题。但是不排除运营商DNS存在污染情况。如果定位不到问题,不妨让客户端多加一点逻辑。首先是检测哪个DNS服务访问最快,以Android端为例,以下用到第三方库版本分别为:OKHttp3(4.13.2),dnsJava(3.5.2),下面为具体逻辑代码:

    
    object DnsLatencyTester {
        // DNS服务器列表
        private val dnsServers = listOf(
            //并非越多越好,下面只是国内流行且高可用的DNS地址,另外还有360的DNS地址,具体情况请按需添加
            "223.5.5.5",    // 阿里公共DNS
            "223.6.6.6",    // 阿里公共DNS
            "119.29.29.29", // 腾讯公共DNS
            "119.28.28.28", // 腾讯公共DNS
            "180.76.76.76", // 百度公共DNS
            "114.114.114.114", // 114DNS纯净版
            "114.114.115.115", // 114DNS纯净版
        )
    
        // 测试API地址,协商你们的任意可访问服务地址
        private const val TEST_URL = ""
        private const val TIMEOUT_MS = 5000L
    
        /**
         * 测试所有DNS服务器的延迟并返回最快的一个
         */
        suspend fun findFastestDns(context: Context): String? = withContext(Dispatchers.IO) {
            // 为每个DNS创建异步测试任务
            val deferredResults = dnsServers.map { dns ->
                async { testDnsLatency(dns) to dns }
            }
    
            // 等待所有测试完成并过滤掉超时的结果
            val results = deferredResults.awaitAll()
                .filter { it.first != -1L }
                .sortedBy { it.first }
    
            // 返回延迟最低的DNS
            results.firstOrNull()?.second
        }
    
        /**
         * 测试单个DNS服务器的延迟
         * @return 延迟时间(毫秒),-1表示超时或失败
         */
        private suspend fun testDnsLatency(dnsServer: String): Long = withContext(Dispatchers.IO) {
            return@withContext try {
                // 创建自定义DNS
                val customDns = object : Dns {
                    override fun lookup(hostname: String): List<InetAddress> {
                        try {
                            // 使用指定DNS服务器解析域名
                            val addresses = InetAddress.getAllByName(hostname)
                            return addresses.toList()
                        } catch (e: UnknownHostException) {
                            // 如果解析失败,尝试直接使用IP连接
                            return listOf(InetAddress.getByName(dnsServer))
                        }
                    }
                }
    
                // 配置OkHttpClient
                val client = OkHttpClient.Builder()
                    .dns(customDns)
                    .connectTimeout(TIMEOUT_MS, TimeUnit.MILLISECONDS)
                    .readTimeout(TIMEOUT_MS, TimeUnit.MILLISECONDS)
                    .writeTimeout(TIMEOUT_MS, TimeUnit.MILLISECONDS)
                    .build()
    
                // 创建请求
                val request = Request.Builder()
                    .url(TEST_URL)
                    .head() // 使用HEAD请求减少数据传输
                    .build()
    
                // 记录开始时间
                val startTime = System.currentTimeMillis()
    
                // 发送请求
                client.newCall(request).execute().close()
    
                // 计算延迟时间
                System.currentTimeMillis() - startTime
            } catch (e: Exception) {
                // 发生异常时返回-1
                -1L
            }
        }
    }

    可以在闪屏页调用(注意实际用户体验):

            lifecycleScope.launch {
                val fastestDns = DnsLatencyTester.findFastestDns(binding.imageViewcontent.context)
                if (fastestDns != null) {
                    Log.i("DNS_TEST", "最快的DNS服务器: $fastestDns")
                    SPUtils.getInstance().put("fastestDns", fastestDns)
                    getInApp()
                } else {
                    Log.e("DNS_TEST", "所有DNS服务器测试失败")
                    SPUtils.getInstance().put("fastestDns", "")
                    getInApp()
                }
            }

    OKHttp3的使用示例:

            var customDns: Dns = Dns.SYSTEM // 默认使用系统 DNS
    
            val fastestDns = SPUtils.getInstance().getString("fastestDns")
    
            if (fastestDns.isNotEmpty()) {
                try {
                    customDns = CustomDns(fastestDns)
                } catch (e: UnknownHostException) {
                    e.printStackTrace()
                    customDns = Dns.SYSTEM
                }
            }
            val okHttpClientBuilder = OkHttpClient().newBuilder().apply {
                connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS)
                readTimeout(READ_TIMEOUT, TimeUnit.SECONDS)
                writeTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS)
                dns(customDns)
                addInterceptor(MInterceptor())
                addInterceptor(DataEncryptInterceptor())

    中间存在的CustomDNS代码如下:

    
    class CustomDns(dnsServer: String) : Dns {
        private var resolver: SimpleResolver? = null
    
        init {
            try {
                // 使用自定义的DNS服务器地址
                this.resolver = SimpleResolver(dnsServer)
            } catch (e: UnknownHostException) {
                throw IllegalArgumentException("Invalid DNS server address: $dnsServer", e)
            }
        }
    
        @Throws(UnknownHostException::class)
        override fun lookup(hostname: String): List<InetAddress> {
            try {
                // 使用dnsjava进行解析
                val name = Name("$hostname.")
                val record: Record = Record.newRecord(name, Type.A, DClass.IN)
                val query = Message.newQuery(record)
    
                val response = resolver!!.send(query)
    
                val addresses: MutableList<InetAddress> = ArrayList()
                if (response.rcode == Rcode.NOERROR) {
                    val records: Array<Record> = response.getSectionArray(Section.ANSWER)
                    for (r in records) {
                        if (r is ARecord) {
                            val a = r as ARecord
                            addresses.add(a.address)
                        }
                    }
                }
    
                if (addresses.isEmpty()) {
                    throw UnknownHostException("No address found for $hostname")
                }
    
                return addresses
            } catch (e: Exception) {
                // 捕获所有dnsjava的异常,并转换为UnknownHostException
                throw UnknownHostException("DNS lookup failed for " + hostname + ": " + e.message)
            }
        }
    }

    以上就是我目前可以想到的全部解决方案,综合之后我个人建议的排序为:

    CDN>DNS>集群,(ipv6以及HTTPS为默认配置😄)

  • Android 12的启动白屏简单适配方案

    TL;DR

    1. 导入implementation 'androidx.core:core-splashscreen:1.0.1'
    2. 继承主题 Theme.ScreenSplash,继承的主题可以设置前景和背景以及时间(最大500),类似icon
    3. 使用主题

    在国产大厂的APP上,每一个Feature,每一个模块,每一个按钮,甚至每一个不可交互的地方,它都有出生的意义:广告位。

    闪屏页的战略地位不言而喻😄

    在 Android 12(SDK 31)之前,应用的启动画面(Splash Screen)一直是个令人头疼的问题。开发者们不得不绞尽脑汁,通过自定义 Activity 主题、设置 windowBackground 或者创建一个独立的 Splash Activity 来模拟启动效果。

    不仅需要作为天然的广告位,这个一闪而过页面承载太多东西了。你的所有需要注册的内容,所有内容的完整性检查,服务器判断,DNS解析。当然还有广告的加载以及用户的手有没有抖动…

    虽然目前看来体验不尽人意,适配寥寥无几,不过当初Android12似乎真的想改变这些(当然不止这些,更多的可能是体验统一的问题),所以带来一个原生,更高效的解决方案。抛开这些开发决定不了的内容,下面我简单记录一下自己适配这个SplashScreen,当然这也是我第一次使用这个特性。

    核心概念与 Theme.SplashScreen 属性详解

    Android 12 的启动画面不再是应用自身绘制的 View,而是由系统根据主题配置生成并管理。你的启动 Activity 会首先应用一个继承自 Theme.SplashScreen 的主题,系统会根据该主题的属性来渲染启动画面,并在应用准备就绪后平滑过渡到应用真正的界面。下面是我在网上找到的VerseAPP的动画闪屏页(你也可以下载查看teelgram的动画效果):

    以下是 Theme.SplashScreen 主题及其关键属性的详细解析:

    1. 父主题:Theme.SplashScreen

    • 作用: 这是 Android 12+ 系统提供的启动画面基础主题。你的自定义启动主题必须继承这个主题,才能享受到系统级别的启动画面管理和动画效果。

    2. 背景属性:windowSplashScreenBackground

    类型: Drawable 引用(@drawable/@color/

    作用: 定义启动画面的背景。这是替换传统“白屏”的关键。

    自定义方案:

    • 纯色: 最简单也是最高效的方式,直接引用一个颜色资源,如 <item name="windowSplashScreenBackground">@color/your_brand_color</item>
    • 渐变色: 创建一个 shape Drawable,并在其中定义 gradient 标签,实现平滑的颜色过渡。
    • 图片作为背景(慎用,推荐 LayerList): 虽然可以直接引用一张图片 (@drawable/your_image),但通常不推荐直接将一张大图作为背景,因为它可能导致屏幕适配问题、文件尺寸增大和内存开销。
    • LayerList 组合背景

    3. 图标属性:windowSplashScreenAnimatedIcon

    • 类型: Drawable 引用(@drawable/@mipmap/
    • 作用: 定义在启动画面中心显示的图标或动画。
    • 可选动画类型:
      • Animated Vector Drawable (AVD)
        • 格式: XML (.xml),定义了矢量图的动画。
        • 优点: 官方推荐,文件小巧,可缩放不失真,性能优秀,可以实现复杂的矢量动画。
        • 创建: 复杂动画通常需要手动编写 XML 或借助工具。
      • Animation Drawable:
        • 格式: XML (.xml),引用一系列帧图片。
        • 优点: 简单易懂,适合简单的帧动画。
        • 缺点: 每一帧都是一张图片,文件体积大,内存占用高,缩放可能失真。适用于帧数非常少且简单的动画。
      • 自适应图标 (Adaptive Icon):
        • 格式: 通常是矢量图,由前景层 (@mipmap/ic_launcher_foreground) 和背景层 (@mipmap/ic_launcher_background) 组成。
        • 优点: 系统原生支持,自动适应不同形状的图标蒙版,无需额外动画即可平滑缩放。
        • 最常用方案: 如果没有自定义动画需求,直接使用你的自适应图标前景层即可,例如:<item name="windowSplashScreenAnimatedIcon">@mipmap/ic_launcher_foreground</item>

    4. 动画持续时间:windowSplashScreenAnimationDuration

    • 类型: 整数(毫秒)
    • 作用: 定义 windowSplashScreenAnimatedIcon 中图标动画的持续时间。系统会在这个时间结束后开始淡出启动画面。
    • 注意: 你的动画设计应尽量在这个持续时间内完成,以避免动画被截断或过早结束。建议值为 200ms 到 1000ms 之间,保持快速且流畅。

    5. 品牌 Logo:windowSplashScreenBrandDrawable (可选)

    • 类型: Drawable 引用
    • 作用: 在启动画面底部显示一个可选的品牌 Logo。
    • 位置: 这个 Logo 会固定显示在启动画面的底部,不会随图标一起动画。
    • 用处: 适合展示公司或产品的额外品牌标识。

    6. 核心属性:postSplashScreenTheme

    • 类型: Style 引用(@style/
    • 作用: 这是最重要的属性!它指定了启动画面结束后,Activity 应该切换到哪个主题来渲染你的应用界面。
    • 重要性: 如果没有正确设置这个属性,你的 Activity 在启动画面消失后,可能会显示错误的样式,甚至出现界面空白或闪烁。务必将其指向你应用正常运行时所使用的主要主题。

    二、应用开发实践:从零开始配置

    让我们通过一个完整的示例,一步步将 Android 12 的启动画面集成到你的应用中。

    1: 确保你的项目兼容 Android 12 (SDK 31+)

    首先,在你的 build.gradle (Module: app) 文件中,确保 compileSdktargetSdk 至少是 31:

    android {
        compileSdk 31 // 或更高版本
        defaultConfig {
            targetSdk 31 // 或更高版本
            // ...
        }
        // ...
    }

    2. 添加 Splash Screen 库依赖

    dependencies {
        // ... 其他依赖
        implementation 'androidx.core:core-splashscreen:1.0.1' 
    }

    3. 定义你的应用主题

    res/values/themes.xml (和 res/values-night/themes.xml,用于深色模式) 中定义应用主主题。这将是 postSplashScreenTheme 指向的主题。

    <resources>
        <style name="Theme.MyAwesomeApp" parent="Theme.MaterialComponents.DayNight.NoActionBar">
            <item name="colorPrimary">#6200EE</item>
            <item name="colorPrimaryVariant">#3700B3</item>
            <item name="colorOnPrimary">#FFFFFF</item>
            <item name="colorSecondary">#03DAC6</item>
            <item name="colorSecondaryVariant">#018786</item>
            <item name="colorOnSecondary">#000000</item>
            <item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
            <item name="android:navigationBarColor">@color/black</item>
            <item name="android:windowBackground">@color/white</item>
            </style>
    </resources>
    
    <resources>
        <style name="Theme.MyAwesomeApp" parent="Theme.MaterialComponents.DayNight.NoActionBar">
            <item name="colorPrimary">#BB86FC</item>
            <item name="colorPrimaryVariant">#3700B3</item>
            <item name="colorOnPrimary">#000000</item>
            <item name="colorSecondary">#03DAC6</item>
            <item name="colorSecondaryVariant">#03DAC6</item>
            <item name="colorOnSecondary">#000000</item>
            <item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
            <item name="android:navigationBarColor">@color/black</item>
            <item name="android:windowBackground">@color/dark_gray</item>
        </style>
    </resources>
    

    4. 准备启动画面背景(渐变色和Logo)

    我们创建一个 LayerList 来实现一个包含渐变背景和底部品牌Logo的启动画面。(实际上不推荐 用渐变色,我更推荐telegram或者X的方案,纯色背景+AVD/静态LOGO)

    <?xml version="1.0" encoding="utf-8"?>
    <layer-list xmlns:android="[http://schemas.android.com/apk/res/android](http://schemas.android.com/apk/res/android)">
        <item>
            <shape android:shape="rectangle">
                <gradient
                    android:angle="270"
                    android:startColor="#6200EE"
                    android:endColor="#03DAC6"
                    android:type="linear" />
            </shape>
        </item>
    
        <item android:bottom="32dp"> <bitmap
                android:src="@drawable/ic_my_company_logo"
                android:gravity="bottom|center_horizontal" />
        </item>
    </layer-list>
    

    注意:

    • @drawable/ic_my_company_logo 应该是你的 Logo 图片(Vector Drawable 或 Bitmap)。
    • android:bottom 可以调整 Logo 的位置。
    • android:gravity="bottom|center_horizontal" 将 Logo 放置在底部中央。

    5. 准备启动画面动画图标(AVD 示例)

    假设你已经有一个名为 ic_animated_logo.xml 的 Animated Vector Drawable。

    <?xml version="1.0" encoding="utf-8"?>
    <animated-vector xmlns:android="[http://schemas.android.com/apk/res/android](http://schemas.android.com/apk/res/android)"
        android:drawable="@drawable/ic_static_logo"> <target android:name="group_name_in_static_logo"> <propertyValuesHolder
                android:propertyName="rotation"
                android:valueFrom="0"
                android:valueTo="360"
                android:valueType="floatType"
                android:duration="1000"
                android:interpolator="@android:interpolator/fast_out_slow_in" />
        </target>
    </animated-vector>
    

    如果不想使用自定义动画,直接使用自适应图标前景部分:@mipmap/ic_launcher_foreground

    6. 定义启动画面主题

    现在,在 res/values/themes.xml (和 res/values-night/themes.xml) 中定义你的启动主题,并引用我们准备好的 Drawable。

    <resources>
        <style name="Theme.MyAwesomeApp.SplashScreen" parent="Theme.SplashScreen">
            <item name="windowSplashScreenBackground">@drawable/splash_layer_bg</item>
    
            <item name="windowSplashScreenAnimatedIcon">@drawable/ic_animated_logo</item>
            <item name="windowSplashScreenAnimationDuration">1000</item> <item name="postSplashScreenTheme">@style/Theme.MyAwesomeApp</item>
        </style>
    </resources>
    
    <resources>
        <style name="Theme.MyAwesomeApp.SplashScreen" parent="Theme.SplashScreen">
            <item name="windowSplashScreenBackground">@color/dark_splash_bg_color</item>
            <item name="windowSplashScreenAnimatedIcon">@drawable/ic_animated_logo_dark</item>
            <item name="windowSplashScreenAnimationDuration">1000</item>
            <item name="postSplashScreenTheme">@style/Theme.MyAwesomeApp</item>
        </style>
    </resources>
    

    7. 在 AndroidManifest.xml 中应用主题

    AndroidManifest.xml 中,将 Theme.MyAwesomeApp.SplashScreen 主题应用到启动 Activity(通常是 MainActivity,也有可能是SplashActivity之类的)。

    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="[http://schemas.android.com/apk/res/android](http://schemas.android.com/apk/res/android)"
        xmlns:tools="[http://schemas.android.com/tools](http://schemas.android.com/tools)">
    
        <application
            android:allowBackup="true"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:roundIcon="@mipmap/ic_launcher_round"
            android:supportsRtl="true"
            android:theme="@style/Theme.MyAwesomeApp"> <activity
                android:name=".MainActivity"
                android:exported="true"
                android:theme="@style/Theme.MyAwesomeApp.SplashScreen"> <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>
    
            <activity android:name=".OtherActivity"
                android:theme="@style/Theme.MyAwesomeApp"/>
    
        </application>
    </manifest>
    

    8. 在 MainActivity 中安装启动画面

    最后,在启动 Activity 的 onCreate() 方法中调用 installSplashScreen()这是启用系统启动画面 API 的核心步骤。

    // MainActivity.kt
    import android.os.Bundle
    import androidx.appcompat.app.AppCompatActivity
    import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
    import kotlinx.coroutines.Dispatchers
    import kotlinx.coroutines.delay
    import kotlinx.coroutines.withContext
    import androidx.lifecycle.lifecycleScope
    import kotlinx.coroutines.launch
    
    class MainActivity : AppCompatActivity() {
    
        private var isContentReady = false // 用于控制启动画面何时消失
    
        override fun onCreate(savedInstanceState: Bundle?) {
            // 1. 调用 installSplashScreen() 必须在 super.onCreate() 之前
            val splashScreen = installSplashScreen()
    
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            // 2. 延迟启动画面消失,直到内容准备就绪
            splashScreen.setKeepOnScreenCondition {
                !isContentReady // 当 isContentReady 为 false 时,启动画面会一直显示
            }
    
            // 3. 模拟数据加载或应用初始化
            loadAppContent()
        }
    
        private fun loadAppContent() {
            lifecycleScope.launch(Dispatchers.IO) {
                // 模拟耗时的初始化操作,例如网络请求、数据库加载等
                delay(3000) // 模拟 3 秒的加载时间
    
                // 数据加载完成
                isContentReady = true
    
                withContext(Dispatchers.Main) {
                    // 在这里可以执行数据加载完成后的 UI 更新或跳转操作
                    // 例如:navigateToHome()
                }
            }
        }
    }
    

    Java 示例(AI转换的代码,应该咩问题):

    // MainActivity.java
    import android.os.Bundle;
    import androidx.appcompat.app.AppCompatActivity;
    import androidx.core.splashscreen.SplashScreen;
    import android.os.Handler;
    import android.os.Looper;
    import java.util.concurrent.Executors;
    import java.util.concurrent.ScheduledExecutorService;
    import java.util.concurrent.TimeUnit;
    
    public class MainActivity extends AppCompatActivity {
    
        private boolean isContentReady = false; // 用于控制启动画面何时消失
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            // 1. 调用 installSplashScreen() 必须在 super.onCreate() 之前
            SplashScreen splashScreen = SplashScreen.Companion.installSplashScreen(this);
    
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            // 2. 延迟启动画面消失,直到内容准备就绪
            splashScreen.setKeepOnScreenCondition(() -> !isContentReady);
    
            // 3. 模拟数据加载或应用初始化
            loadAppContent();
        }
    
        private void loadAppContent() {
            ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
            executor.schedule(() -> {
                // 模拟耗时的初始化操作
                isContentReady = true; // 数据加载完成
    
                new Handler(Looper.getMainLooper()).post(() -> {
                    // 在主线程执行数据加载完成后的 UI 更新或跳转操作
                });
            }, 3, TimeUnit.SECONDS); // 模拟 3 秒的加载时间
        }
    }