欢迎使用 WordPress。这是您的第一篇文章。编辑或删除它,然后开始写作吧!
作者: zzh
-
部分省份无法访问在线服务的解决方案
周末被反应国内的服务福建省出现无法调用的情况,心中大概知道什么问题。这个问题经常出现,尤其是在福建,江苏,河南等省份。
下面是简单的解决方案,分别为:HTTPS(SSL),CDN,ipv6,组建集群,客户端指定DNS
HTTPS
大部分国内的服务加上证书可以解决很多问题,没错这是真实情况…
CDN
最优的解决方案,目前可以查询到上面三个省份均有节点的服务商:
- 阿里云
- 腾讯云
- 七牛云
其它服务商暂时没有查询,具体可以咨询官网客服。如果是前后端分离的项目,推荐购买接口加速的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
- 导入
implementation 'androidx.core:core-splashscreen:1.0.1'
- 继承主题
Theme.ScreenSplash
,继承的主题可以设置前景和背景以及时间(最大500),类似icon - 使用主题
在国产大厂的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 或借助工具。
- 格式: XML (
- Animation Drawable:
- 格式: XML (
.xml
),引用一系列帧图片。 - 优点: 简单易懂,适合简单的帧动画。
- 缺点: 每一帧都是一张图片,文件体积大,内存占用高,缩放可能失真。适用于帧数非常少且简单的动画。
- 格式: XML (
- 自适应图标 (Adaptive Icon):
- 格式: 通常是矢量图,由前景层 (
@mipmap/ic_launcher_foreground
) 和背景层 (@mipmap/ic_launcher_background
) 组成。 - 优点: 系统原生支持,自动适应不同形状的图标蒙版,无需额外动画即可平滑缩放。
- 最常用方案: 如果没有自定义动画需求,直接使用你的自适应图标前景层即可,例如:
<item name="windowSplashScreenAnimatedIcon">@mipmap/ic_launcher_foreground</item>
。
- 格式: 通常是矢量图,由前景层 (
- Animated Vector Drawable (AVD):
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)
文件中,确保compileSdk
和targetSdk
至少是 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 秒的加载时间 } }
- 导入
-
WordPress发不出去通知邮件的解决方案
TL;DR
wordpress国内服务器访问不到发件服务器,所以经过综合考虑之后,选择采用第三方插件和第三方邮箱通过SMTP发送邮件。
起因
我转移到wordpress之后,之前搭建在纽约的服务器上,邮件是没什么问题的。评论和被评论都会收到邮件通知。但是正常转移到杭州的服务器上之后发现收不到任何邮件了,包括不限于修改密码,邮箱,用户操作,评论和被评论的通知邮件。
分析
❌期初我以为是因为服务器商封禁了邮件发送端口。但是我在防火墙打开了SMTP端口(25,465 ,587)的出站之后发现还是不行。
❌那就要考虑邮件是不是被服务商拦截的,询问客服之后告知不是,同时被告知如果发送垃圾邮件会被封禁。
✔️最后我检查之前的邮件,发现是来自wp.com的邮件。我并未安装jetpack,但是邮件依旧不是来自我的站点而是通过wordpress的服务器发送(猜测是需要访问一下WP的接口而非使用PHP的
mail()
方法发送的邮件)。尝试解决
问题明确之后就很容易解决了。下面是几个直接方案:
- 通过一些方案访问WP.COM
- 代理
- HOSTS
- 更换服务器
- 修改DNS(或许可行?)
- 利用wordpress的第三方插件访问WP网络
- 修改发件邮箱
- 修改mail()方法
- 利用第三方插件
因为我不想改代码了,也不想被警告,经过我1/3秒的思考,我觉得利用第三方插件,即使他们就是一个SMTP发送邮件的方案却做得又大又重,我似乎没有更多其它选择。
我最后选择了Post SMTP,这是唯一一个SMTP插件,比较流行,看起来没那么重,且貌似不会打扰我的邮件插件。另外不推荐在 VPS 上运行本地 SMTP 服务器(配置复杂,送达率低)。
推荐使用QQ/foxmail邮箱做发件邮箱。所有的插件使用outlook都需要会员,outlook自带的SMTP貌似不可用了。我根据网上的教程设置了SMTP,打开了双重验证,然后在下方寻找应用密码。因为outlook在登录时似乎需要使用应用密码而不是账户密码。但是设置了双重验证,貌似又需要认证,永远提示认证失败。后来查了一下,说是之后会禁用第三方SMTP…奇奇怪怪的东西。另外我也不清楚这个是否会放到corn队列了,并且corn是否会堆积…对WP了解的不多。
所以,用FOXMAIL算啦!开通”POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务”之后,或许一个授权码用作登录即可。记得要勾选“SMTP 发信后保存到服务器”!
本身在传家宝上搭WP是为了方便,现在反而产生了更多的不方便。
其他小问题
- 插件提示存在屏蔽词或者被判断为垃圾内容,如下提示:
The mail may contain inappropriate words or content.
此时你应该在插件中找到发送方式(Mailer Type),修改为
PHPMailer
- 发送失败,提示信息中存在如下字样:
Mail from address must be same as authorization user.
此时你应该在插件中寻找发件地址(From Address)设置,然后修改文SMTP登录账号的邮箱。并且勾选“禁止其他插件/主题修改此内容”选项
- 通过一些方案访问WP.COM
-
Pocket给我的失望与启示
TL;DR
这似乎是一个经典的互联网产品悲剧循环:
其他产品同类出现> 导致用户流失 > 更新不积极,市场定位偏差,没有突出买点以及差异化feature,固收基础功能以及紧缩的免费用户权限> 加深市场竞争与用户流失>Mozilla 战略调整>宣布关停
现在,如果做不到样样精通,还没有极端差异化的宣传噱头,似乎只有这一个剧本了。
引言
当我听说 Mozilla 将在 2025 年 7 月 8 日关闭 Pocket 时,心里既不意外又有些唏嘘。作为一个曾经长期使用 Pocket 的免费用户,我一直觉得它的功能像个漂亮的“收藏夹”。Pocket 的关闭让我开始思考:是什么让这个“稍后阅读”工具走到尽头?
Pocket 停止运营的背景
2025 年 5 月 23 日,Mozilla 宣布 Pocket 将于 2025 年 7 月 8 日停止运营,同时关闭的还有其评论真伪分析工具 Fakespot(宣布今年7月1日关闭)。Mozilla 表示,关闭是为了集中资源发展 Firefox 浏览器。具体的安排如下:
- 服务终止:2025 年 7 月 8 日,Pocket 的 Web 端、移动端应用和浏览器扩展将全部停止。从 5 月 22 日起,用户无法下载应用或新购 Premium 订阅。
- 数据导出:我需要在 2025 年 10 月 8 日前通过 Pocket 的导出页面备份文章,包括链接、标题、笔记和高亮,否则数据将永久丢失。
- 连带关闭:Fakespot 的关闭表明 Mozilla 正在收缩非核心业务。
Pocket 为何让我失望
作为 Pocket 的免费用户,我一直觉得它的功能非常有限。Pocket 允许我保存网页和文章(或者说是链接)以便稍后阅读,但缺乏离线保存和下载网页内容的功能。这意味着,一旦网站失效或链接更改,我保存的内容就成了死链接,毫无用处。我曾保存过一篇教程,但网站下线后,Pocket 只能显示一个无用的标题,形同“残废收藏夹”。
相比之下,我用 Notion 搭配 Notion Web Clipper 就能轻松保存网页的完整内容,包括图片和格式化文本,还能添加笔记、标签,甚至整合到我的知识库中。不仅Notion,其他各种剪藏工具(Web Clipper不错)+同步工具都不错,你可以使用juplin(同样提供剪藏工具)+teracloud(我的邀请码 NKM4G )的搭配。为什么我推荐使用笔记工具?就像我之前在博客《如何记笔记》 中有写:记笔记的目的并非单纯为了记录,而是为了复用。
或许你可以反驳,我需要的并不是一款”稍后读“软件而是笔记软件。或许是这样的,但是这两个需求并不冲突。如果你确实”稍后读“了收藏的内容,单纯地欣赏便罢了。若是还想记录下来,岂不是又要重复操作。所以带有我还是推荐带有全文搜索功能的笔记软件。
导致 Pocket 关闭的连带效应
Pocket 的关闭不是偶然,而是由一系列连带效应导致的,从竞品崛起到 Mozilla 的战略调整,每一步都让 Pocket 走向终结。
竞品崛起引发用户流失
Notion、Obsidian、Raindrop.io 和 Instapaper 等工具的出现让我和许多用户看到了更好的选择。Notion 提供了强大的笔记和协作功能,Obsidian 的双向链接适合知识管理,Raindrop.io 则在书签管理上更直观。这些工具的功能全面且不断更新,吸引了大量 Pocket 用户。X 平台上,不少用户表示因为竞品更强大而放弃了 Pocket。
更新滞后与市场定位偏差
Pocket 的功能更新停滞不前,博客上最后一次提到新功能是在 2023 年。它的核心功能仅限于保存链接和阅读,缺乏 AI 摘要、深度笔记整合等差异化特性。免费用户像我一样,权限受限,无法使用标签管理或离线阅读,体验大打折扣。Pocket 的定位也模糊不清,既不如浏览器书签简单,也不如笔记工具强大,形同“半吊子”产品。
加剧的市场竞争与用户流失
功能停滞和定位偏差让 Pocket 在竞争中节节败退。竞品不断推出新功能,比如 Notion 的 AI 工具和 Obsidian 的插件生态,而 Pocket 却毫无进展。X 平台上的用户反馈也反映了类似的不满:界面陈旧、功能单一、标签管理繁琐。这些问题导致用户流失加剧,Pocket 的商业价值进一步下降。
Mozilla 战略调整
面对用户流失,Mozilla 的资源分配向 Firefox 倾斜。Firefox 需应对 Chrome 的竞争,迫使 Mozilla 放弃非核心项目。Pocket 与 Mozilla 的核心使命(隐私保护、开放互联网)关联度低,财务压力下,关闭 Pocket 和 Fakespot 成为必然选择。
最终关闭决定
竞品崛起引发用户流失,功能滞后和定位偏差加剧竞争劣势,最终迫使 Mozilla 宣布关闭 Pocket。这个连带效应清晰地解释了 Pocket 从流行工具到被淘汰的轨迹。
所以
无论什么软件,数据可以无限制导出是最重要滴。Pocket 的关闭源于竞品崛起、用户流失、功能滞后、定位偏差和 Mozilla 战略调整的连带效应。作为免费用户,我早已因 Pocket 的局限性转向 Notion 等更强大的工具。这次经历让我意识到,选择数字工具时必须注重其功能完整性和长期稳定性。Pocket 的“稍后阅读”时代结束了,但它提醒我和所有用户:数据备份和工具选择需要更加谨慎。未来,工具开发者也应吸取教训,通过持续创新和清晰定位赢得用户信任。
-
三十而立,JAVA生日快乐!
当一款一出生就颇具竞争力,甚至在每个榜单都名列前茅的编程语言持续更新到30周年,它已经不只是一门编程语言,一个学科,一项技术了。它是一个时代的标志。
Java 技术演进之路与时代回响
1995 年 5 月 23 日,Sun Microsystems 发布了 Java 语言,谁也没想到这个诞生于小型家电控制项目的编程语言,会在接下来的三十年里重塑整个软件开发生态。从 Web 时代的 Servlet 到移动互联网的 Android,从大数据领域的 Hadoop 到云原生时代的 Kubernetes,Java 始终站在技术变革的潮头。今天,在 Java 迎来三十周年生日之际,让我们回溯那些改变技术史的里程碑时刻。
一、“一次编写,到处运行” 的传奇起点
Java 的诞生源于 Sun 公司 “Green Team” 的一个冷门项目 —— 为智能电视、机顶盒等设备开发编程语言。詹姆斯・高斯林(James Gosling)带领团队设计出 Oak 语言(Java 前身),却因市场时机未到陷入沉寂。直到互联网浪潮袭来,Oak 凭借 “Write Once, Run Anywhere”(WORA)的跨平台特性,在浏览器插件领域找到突破口。1995 年发布的 Java 1.0 定义了核心特性:字节码、虚拟机(JVM)、垃圾回收机制,这些设计让 Java 程序能够在不同操作系统上以相同行为运行,彻底改变了软件开发的部署模式。
1996 年,第一个 Java 开发工具包(JDK 1.0)正式发布,包含 AWT 图形库与 Applet 技术。Applet 允许 Java 程序嵌入网页运行,引发了第一次 Java 热潮。尽管后来因安全问题逐渐被淘汰,但它验证了 Java 在网络应用领域的潜力。
二、企业级开发的统治时代
1998 年,Java 2 平台的发布标志着 Java 正式进军企业级应用领域。Java 2 平台分为标准版(J2SE)、企业版(J2EE)和微型版(J2ME),其中 J2EE 通过 EJB(Enterprise JavaBeans)、Servlet、JSP 等技术,为分布式应用开发提供了完整解决方案。IBM、Oracle 等巨头纷纷加入 Java 阵营,Java EE 成为构建银行核心系统、电商平台的事实标准,支撑起全球 80% 以上的企业级应用开发。
2004 年,Java 5(代号 Tiger)的发布堪称 Java 史上的重大飞跃。泛型、自动装箱 / 拆箱、枚举类型、可变参数等特性的引入,让 Java 从单纯的面向对象语言进化为更现代化的编程语言。同年,Spring 框架 1.0 发布,以轻量级容器和依赖注入理念革新了企业级开发模式,与 Java EE 形成互补,共同奠定了 Java 在企业级市场的统治地位。
三、移动革命与开源觉醒
2007 年,苹果发布 iPhone,智能手机时代拉开帷幕。而 Java 早已提前布局,J2ME 曾是功能机时代的主流开发平台。2008 年,谷歌推出基于 Linux 内核和 Java 的 Android 系统,将 Java 推向移动开发的巅峰。虽然 Android 后来改用 Kotlin 作为官方推荐语言,但 Java 至今仍是安卓开发者的重要工具,全球数十亿台设备上运行着 Java 编写的应用。
2006 年,Sun 宣布 Java 开源,成立 OpenJDK 社区。这一决策打破了商业闭源的限制,激发了全球开发者的创造力。Apache Harmony、IBM J9 等替代 JVM 项目涌现,OpenJDK 逐步成为 Java 的参考实现。2010 年 Oracle 收购 Sun 后,围绕 Java 版权与开源协议的争议不断,但社区力量最终推动 Java 走向更开放的生态。
四、云原生时代的涅槃重生
2011 年起,Java 进入快速迭代周期,每半年发布一个新版本。Java 8 引入的 Lambda 表达式与 Stream API,让函数式编程范式融入传统 OOP 体系,极大提升了代码简洁性与并行处理能力;Java 11 成为首个 LTS(长期支持)版本,精简模块化系统并移除过时特性,标志着 Java 向轻量化转型。
随着容器化与微服务兴起,Java 凭借 Spring Cloud、Quarkus 等框架,以及 GraalVM 即时编译技术,在云原生领域焕发新生。Kubernetes 项目的核心组件 Kubelet、kube-apiserver 均采用 Java 开发,印证了其在分布式系统领域的卓越性能与稳定性。
五、下一个三十年:Java 的无限可能
站在三十周年的节点,Java 生态早已超越编程语言本身,成为覆盖开发、部署、运维全生命周期的技术体系。如今,Java 正积极拥抱 AI 与 Web3 领域,通过 Project Loom 虚拟线程优化高并发场景,探索与机器学习框架的深度集成。OpenJDK 社区持续推进 JVM 性能优化,让 Java 在边缘计算、Serverless 等新兴领域保持竞争力。
从智能家电到数据中心,从桌面应用到云端服务,Java 用三十年时间证明了 “技术生命力在于持续进化”。对于全球 1000 万 Java 开发者而言,这不仅是一门编程语言的生日,更是一个时代技术精神的缩影。未来,随着 Java 22 计划引入结构化并发、虚拟线程成熟化等重磅特性,这个 “永远年轻” 的语言仍将书写新的传奇。
Java重要版本更新列表
由GROK.COM生成
1. Java 1.0(1996 年 1 月)
- 定义核心特性,如字节码、虚拟机(JVM)、垃圾回收机制,实现 “Write Once, Run Anywhere” 跨平台运行。
- 发布第一个 Java 开发工具包(JDK 1.0),包含 AWT 图形库与 Applet 技术。
2. Java 1.1(1997 年 2 月)
- 引入内部类,增强代码封装与组织性。
- 加入反射机制,支持运行时检查和操作类、方法、字段。
- 诞生 JDBC,方便 Java 程序与数据库交互。
3. Java 2 平台(1998 年)
- 分为标准版(J2SE)、企业版(J2EE)和微型版(J2ME)。其中 J2EE 通过 EJB、Servlet、JSP 等技术,为分布式应用开发提供完整解决方案。
4. Java 5(2004 年 9 月)
- 引入泛型,提升类型安全性与代码复用性。
- 实现自动装箱 / 拆箱,简化基本数据类型与包装类的转换。
- 新增枚举类型,方便定义常量集合。
- 支持可变参数,使方法参数数量更灵活。
5. Java 7(2011 年 7 月)
- 引入 try – with – resources 语句,简化资源管理。
- 钻石操作符
(<>)
自动推断泛型类型参数。 - Fork/Join 框架提升并行处理大量数据的效率。
- NIO 升级到 NIO.2,新增异步 I/O 和文件系统 API。
6. Java 8(2014 年 3 月)
- 引入 Lambda 表达式,支持函数式编程范式。
- 推出 Stream API,提升代码简洁性与并行处理能力。
- 允许为接口方法添加默认方法体。
- 引入日期时间应用程序接口(date time API)。
7. Java 9(2017 年 9 月)
- 引入模块化系统(Project Jigsaw),解决类路径问题,提高代码可维护性。
- 提供 Javadoc,支持在 API 文档中进行搜索,输出兼容 HTML5 标准。
- 在 List、Set、Map 接口中,静态工厂方法可创建不可变实例。
- 内置轻量级 JSON API。
8. Java 10(2018 年 3 月)
- 带来局部变量类型推断(var 关键字),简化代码声明。
- 提升线程间通信效率的线程本地握手机制。
- 将 JDK 多个代码仓库合并到一个储存库。
9. Java 11(2018 年 9 月)
- 成为首个 LTS(长期支持)版本。
- 精简模块化系统,移除过时特性。
- 引入 NestMembers 属性和 NestHost 属性。
- 支持 TLS 1.3 协议。
10. Java 17(2021 年 9 月)
- 引入密封类,限制类的继承层次,增强代码安全性。
- 恢复浮点语义,为伪随机数发生器(PRNG)提供新接口类型。
- 增加 macOS AArch64 端口,删除 Applet API、远程方法调用(RMI)激活机制,强封装 JDK 内部。
-
基于Nextjs的说说程序
TL;DR
点这里看效果: https://ss.banzhuanriji.com
发布,查看
一代人有一代人的QQ空间要踩 XD ,可是QQ空间怎么少的了说说呢。而且发长文会带动一切RSS订阅用户,偶尔想当个动态/朋友圈发的话,太打扰人了。
所以我想到了两种方案:
- RSS避免某个分类的文章,把这个分类当做说说来发。但我想了想,似乎在function里加一些方法直接控制数据更好用一点,而且样式制作可以直接在CSS里修改,不影响其他文章样式内容。所以wordpress的方案又分为两种
- 不经过RSS的分类
- 在wordpress的function中创建独立的方法内容
- 创建一个独立的说说页面
但是目前wordpress的数据库已经够复杂,冗余过于夸张了。并且之后如果我不希望使用wordpress的话,如何迁移?所以决定使用独立的方案。
第一想到的就是目前已经存在的有没有可用的。第一个想到的就是面条大佬的BroadcastChannel ,把TG频道做数据来源。实话实说挺好用的。不过样式不可控,可能需要自己修改,且一些TG的内容样式不支持。所以我就采用了Nextjs+neon数据库的方案写了这个程序。
功能
- 浏览内容,分页
- 鉴权登录
- 发布内容
忽略了内容修改的功能,因为要做修改的话可能需要在首页做登录判断,然后在布局上做一些按钮预留。想了想暂时不做这个了
特色
如果可以成为特色的话…
- 全部白嫖,白嫖!
- 结构超级简单
- Markdown支持
预览
下载
如果你有兴趣可以查看这里: https://ss.banzhuanriji.com
暂时没有提供下载,等我完善之后在github上发出来XD。比较感兴趣的话也可以联系我,我会发给你一份源码: m#hoytzhang.com 或者在文章下面留言 ;D
- RSS避免某个分类的文章,把这个分类当做说说来发。但我想了想,似乎在function里加一些方法直接控制数据更好用一点,而且样式制作可以直接在CSS里修改,不影响其他文章样式内容。所以wordpress的方案又分为两种
-
如何为wordpress的文章添加阅读量
要在 WordPress 上实现文章阅读统计(跟踪每篇文章的阅读次数或页面浏览量),可以借助插件或自定义代码。以下是几种常见的方法和工具,如果使用代码请注意文章内容。
方法一:使用插件实现文章阅读统计
- MonsterInsights
- MonsterInsights 是最受欢迎的 Google Analytics 插件之一,可以直接在 WordPress 仪表板中查看每篇文章的页面浏览量、访客行为、跳出率等数据。通过其 Page Insights 扩展,还可以查看单篇文章的详细统计(如页面浏览量、停留时间等)。必须绑定GA。
- WP Statistics
- WP Statistics 是一个隐私友好的分析插件,符合 GDPR 要求,无需外部账户,直接在 WordPress 数据库中存储数据。它可以跟踪每篇文章的访问量,并提供详细的图表和分类分析。不过最顶上那个评论似乎说明了一些问题,我没尝试这个插件,观望一下。
- Post Views Counter
- 一个轻量级插件,专门用于显示文章、页面或自定义内容的浏览次数。它支持通过 PHP、JavaScript 或 REST API 跟踪数据,并允许自定义计数器的显示位置和样式。
- Jetpack Stats
- Jetpack 的统计模块提供文章的浏览量、热门内容、流量来源等数据,适合 WordPress.com 用户或安装了 Jetpack 的自托管站点。免费用户可查看过去 7 天的统计,付费计划解锁更多功能。
- Independent Analytics
- 一个专为 WordPress 设计的免费分析插件,加载速度快,符合 GDPR。它可以自动记录文章的访问量,并按类别显示统计数据。
- 如果你恰好财力雄厚,你可以使用一些主题的pro版本自带的功能
- 这里就不对赘述了,例如我使用的blocksy主题,在设置页面左侧点击blocksy,设置中文章元数据打开显示即可。
方法二:插件之短码插件(推荐)
- Post Views for Jetpack
- 提供短码,可以在文章、页面或小工具中显示各种统计数据。以下部分代码复制于这个插件的讨论区: https://wordpress.org/support/topic/way-to-display-views-in-blocksy-post-meta/
这个插件如何在blocksy中使用?请在你的
function.php
中添加以下代码 (注意把[sbs_views]
修改为你上面短码插件中约定的短码):add_filter( 'blocksy:archive:render-card-layer', function ( $output, $single_component) { if ( 'post_meta' !== $single_component['id'] ) { return $output; } $post_views = do_shortcode( '[sbs_views]' ); $output = str_replace( '</li></ul>', '</li><li class="post-views">' . $post_views . '</li></ul>', $output ); return $output; }, 11, 2 );
方法三:自定义代码实现阅读统计
在你的主题的
function.php
中添加以下代码:// 记录文章浏览量 function set_post_views($postID) { $count_key = 'post_views_count'; $count = get_post_meta($postID, $count_key, true); if ($count == '') { $count = 0; delete_post_meta($postID, $count_key); add_post_meta($postID, $count_key, '0'); } else { $count++; update_post_meta($postID, $count_key, $count); } } // 在单篇文章页面记录浏览量 function track_post_views($postID) { if (!is_single()) return; if (empty($postID)) { global $post; $postID = $post->ID; } set_post_views($postID); } add_action('wp_head', 'track_post_views'); // 显示文章浏览量 function get_post_views($postID) { $count_key = 'post_views_count'; $count = get_post_meta($postID, $count_key, true); if ($count == '') { delete_post_meta($postID, $count_key); add_post_meta($postID, $count_key, '0'); return "0 Views"; } return $count . ' Views'; }
在需要使用的地方添加:
echo get_post_views(get_the_ID());
如果需要在blocksy主题中使用:
add_filter( 'blocksy:archive:render-card-layer', function ( $output, $single_component ) { if ( 'post_meta' !== $single_component['id'] ) { return $output; } $output = str_replace( '</li></ul>', '</li><li class="post-views">' . get_post_views(get_the_ID()) . '</li></ul>', $output ); return $output; }, 10, 2 );
注意上面所有的代码部分,可能会造成不可回溯的内容。
部分可能会在你数据库的
前缀_postmeta
表中创建很多key为post_views_count
的数据,介意的话可以使用第三方插件或者自己手动修改。另外注意,有的主题不允许修改functions.php
的代码,你复制代码保存失败时,可以按ctrl
+shift
+v
粘贴。 - MonsterInsights
-
家里种的月季花
红月季与白月季XD 单株异色 紧蹙 家里养的月季开的非常漂亮,发了朋友圈之后被问怎么种的。就是家里空地(农村小院)上种的,没有特别照料和施肥。不仅种在土地上的花,花盆里的花也开的很漂亮。总结了一下,大概是这几点会注意:
光照,绝对充足的光照
如果你没有额外施肥,那么光照就不应该吝啬。太阳大的晴天可以放到室外阳光直射的地方。有的花甚至光照够一定的时间才会开花,才会有花芽!除了一些特别的植物,绝大部分都喜欢被阳光直射。
按照习性浇水
这些月季是三五天浇一次的,但是没有特别照顾。旁边就是菜地,基本都是浇完菜顺带水管冲一下。这里需要注意:温度高的时候不要浇花或者作物,会导致根系烫伤,或者温差也会对植物造成伤害。
在过强的光照或者温度较高环境下,植物会蔫蔫的,就像下图,这是一株种在花盆里的蓝花草,此时请勿直接浇水,可以等太阳落山或者吃完晚饭后适当浇水或者浸泡花盆。
适当修剪
还是小学学过的那些,平常可以修剪,留老桩(如果不会可以去抖音搜索一下)。生长期可以按照你的想法去顶枝,让他更茂盛,因为顶枝非常吃养料。
最后附上高清图片 ;D
-
从Typecho安全平移到WordPress
这里不比较Typecho与Wordpress的好坏。实际上优缺点都很明显。但如果你没有其他想法,只是想单纯写写内容,Notebook才是最棒的选择。我只是想把加的各位朋友的友链放在除了友链页以外的其他页面,那貌似只能放在sidebar了,所以选择了一个双栏的杂志主题。后来想了想,直接换wordpress得了。
1. 导出数据
数据导出当然使用的是ByTyp,但是我记得之前第一次安装是有什么问题的,貌似是PHP版本不对还是什么东西,忘记了,不过我记得我修改了一些东西。如果你会有这个问题并且你发现搜索不到解决方案,你可以点下面的按钮下载我的备份:
2. 备份
备份你需要做以下备份
- 下载Typecho的所有文件内容
- 下载Typecho的数据库备份文件
- 尝试新建数据库并导入数据库备份文件查看是否可以导入
- 使用Typecho的备份功能下载备份文件
- 检查以上备份
3. 安装Wordpress和导入数据
因为我后端使用的是宝塔,所以有一键部署。我的域名是banzhuanriji.com,我新建的网站绑定的域名为abc(这里你自己设置).banzhuanriji.com,然后所有的操作都是在这个网站上进行的。
工具-导入-立即安装,安装完成后会显示【运行导入器】,然后选择上面ByTyp导出的文件即可,选择接受用户即可。
4. 修改域名
- 检查上面的备份内容
- wordpress后台,设置-常规:WordPress 地址(URL)和 站点地址(URL)修改为你的地址
- 修改网站文件夹名称,也就是删除原来的 banzhuanriji.com,把abc.banzhuangriji.com,修改为banzhuanriji.com
- 重启Nginx
5. 安装需要的插件
我是所有都设置好之后才修改域名的,现在我点击主题的自定义,就会先显示几条错误提示,再显示自定义网页页面。错误内容如下:
Warning: Undefined array key "mp_featuredimg_1" in /www/wwwroot/banzhuanriji.com/wp-includes/class-wp-customize-widgets.php on line 1130 Warning: Trying to access array offset on value of type null in /www/wwwroot/banzhuanriji.com/wp-includes/class-wp-customize-widgets.php on line 1130 Warning: Undefined array key "mp_featuredimg_1" in /www/wwwroot/banzhuanriji.com/wp-includes/class-wp-customize-widgets.php on line 1131 Warning: Trying to access array offset on value of type null in /www/wwwroot/banzhuanriji.com/wp-includes/class-wp-customize-widgets.php on line 1131 Warning: Undefined array key "mp_featuredimg_1" in /www/wwwroot/banzhuanriji.com/wp-includes/class-wp-customize-widgets.php on line 603 Warning: Trying to access array offset on value of type null in /www/wwwroot/banzhuanriji.com/wp-includes/class-wp-customize-widgets.php on line 603
貌似是Blocky插件的问题,到现在我都不知道怎么解决…
6. 其他
关于安装的插件和主题,有兴趣的话可以查看关于页面。
如果你没有想使用Wordpress,那么我推荐你使用Tp2MD,可以导出到Markdown文件,你经过简单的修改可以直接在一些hexo,hugo,nextjs或者astro等可以使用Markdown文件的博客系统。