翻自己硬盘里的 snippets 文件夹最早的几个文件是七年前建的里面躺着一段 SharedPreferences 的封装、一段判断网络是否可用的代码、还有一段到现在都没改过的双击退出。做 Android 的人大概都有这么一本自己的“常用代码合集”它谈不上多高深但每次开新项目第一件事就是把这些片段从旧工程里刨出来、改改包名、粘进去。这篇文章就把这些年攒下的 Android 常用经典代码段做一次系统整理从基础工具层到 UI 交互、异步与数据、权限兼容一段一段拆开讲既给能直接抄的代码也说清楚为什么要这么写、哪里容易埋雷。刚上手的同学可以当成模板库照着搭做了几年的人可以拿来对照看看自己的写法还有没有能再收紧的地方。1. 代码片段库的整体设计与组织思路1.1 为什么“能跑”的代码不等于“能用”的片段先说一个很常见的现象从网上搜到一段代码粘进项目编译过了跑起来也没崩于是就留在那儿了。半年后再看这段代码用的是已经被标记废弃的 API或者它默认了某个上下文是 Activity结果你在 Service 里调用时拿到的是 Application 上下文弹窗直接抛异常。能跑只是最低标准能用意味着它在你不知道的调用场景下也不会出问题。我判断一段代码值不值得进片段库一般看三条第一它对传入的上下文有没有明确要求如果有是不是在方法名或者注释里写清楚了第二它有没有隐藏的线程假设比如必须在主线程调用或者内部偷偷开了子线程第三它在异常路径上是什么行为是抛出去还是吞掉。这三条里任何一条说不清楚我就不会把它收进公共库宁可每次现写。举个具体例子。判断网络是否可用这段代码网上流传最广的版本大概是拿ConnectivityManager的activeNetworkInfo判一下isConnected。这段代码在多数机器上跑得通但它有两个坑一是activeNetworkInfo在某些版本上可能返回 null不做判空就直接.isConnected会空指针二是它只告诉你“有网”不告诉你“网能不能真的通到目标服务器”。前者是写法问题后者是语义问题。所以我在自己的合集里把它拆成了两层hasNetwork()回答“有没有可用的网络接口”canReach(host)才回答“通不通”。1.2 目录结构与命名约定片段库最怕的不是少是乱。我的做法是按“层”分目录而不是按“功能”分。因为按功能分时间一长就会出现utils/下面塞了两百个文件谁也不知道哪个是哪个。按层分的话结构大概是这样的core/上下文持有、线程调度、日志、全局配置ui/View 扩展、适配器骨架、对话框、状态栏data/存储封装、缓存、序列化net/网络状态、请求拦截、错误映射compat/版本判断、权限、设备信息命名上我有个硬性习惯扩展函数一律用动词开头工具类一律用名词加 Kit 或 Helper 后缀。Context.screenWidth()而不是Context.screenWidthPx因为后者看起来像属性实际每次调用都要走一遍系统服务查询容易被误以为开销很低。这种细节看起来吹毛求疵但当片段库长到几百个函数的时候命名的一致性直接决定了你还敢不敢用自己写的东西。1.3 版本与依赖的克制原则还有一个容易被忽略的点片段库里尽量不要引入第三方库。我见过不少人的合集里躺着一段“优雅的链式调用”点进去发现依赖了一个几千 star 的第三方工具库为了一个字符串判空拖进来一整个依赖树这在多模块工程里是灾难。我的原则是片段库本体只依赖 AndroidX 基础库和 Kotlin 标准库任何需要额外依赖的东西单独放一个可选模块。比如协程相关的片段我就单独放在一个标注了“需要 kotlinx-coroutines”的子目录里用不到的人直接不拷。这样做的代价是有些代码写起来会啰嗦一点但换来的是拷进任何工程都不会因为缺依赖而编译不过。2. 基础工具层每开一个新项目都要重写的那几段2.1 上下文持有与屏幕参数获取先说上下文。很多工具函数需要 Context但又不该持有 Activity 引用标准做法是搞一个全局的 Application 上下文持有者。这段代码短但写法上有讲究object AppCtx { private var app: Application? null fun init(application: Application) { app application } fun get(): Context app ?: error(AppCtx 未初始化请在 Application.onCreate 中调用 init) }关键在最后那个error()。早期我写的是app!!结果某次在 ContentProvider 初始化阶段调用直接一个空指针堆栈还指不到源头。改成显式抛错之后报错信息一眼就能看出是初始化顺序问题。片段库里凡是依赖初始化的地方都应该给出人能读懂的报错而不是让!!去撞运气。屏幕参数这块Android 11 之后Display.getRealMetrics已经被标记废弃新写法走WindowManager.currentWindowMetrics。为了兼容老版本封装里必须做分支fun Context.realScreenSize(): PairInt, Int { val wm getSystemService(Context.WINDOW_SERVICE) as WindowManager return if (Build.VERSION.SDK_INT Build.VERSION_CODES.R) { val b wm.currentWindowMetrics.bounds b.width() to b.height() } else { Suppress(DEPRECATION) val dm DisplayMetrics().also { wm.defaultDisplay.getRealMetrics(it) } dm.widthPixels to dm.heightPixels } }这里有个实操细节值得说currentWindowMetrics返回的是包含系统栏的整个窗口区域而在分屏或者自由窗口模式下这个值和全屏时是不一样的。如果你的布局计算依赖这个数值记得在onConfigurationChanged里重新取别在onCreate里算一次就存成全局变量。我踩过一次分屏状态下界面比例全乱排查了半天才发现是缓存了屏幕尺寸。2.2 SharedPreferences 的薄封装SharedPreferences 的原生 API 用起来啰嗦edit()、put、apply三行才存一个值。多数人第一反应是封装成putString(key, value)这种形式。但我建议再往前走一步用泛型 委托属性把读写做成属性访问。object SpKit { private const val FILE app_prefs private val sp by lazy { AppCtx.get().getSharedPreferences(FILE, Context.MODE_PRIVATE) } fun put(key: String, value: Any?) { sp.edit().apply { when (value) { null - remove(key) is String - putString(key, value) is Int - putInt(key, value) is Long - putLong(key, value) is Boolean - putBoolean(key, value) is Float - putFloat(key, value) else - error(不支持的类型: ${value::class.java}) } }.apply() } inline fun reified T get(key: String, def: T): T when (T::class) { String::class - sp.getString(key, def as String) as T Int::class - sp.getInt(key, def as Int) as T Long::class - sp.getLong(key, def as Long) as T Boolean::class - sp.getBoolean(key, def as Boolean) as T Float::class - sp.getFloat(key, def as Float) as T else - error(不支持的类型: ${T::class.java}) } }之所以用apply()而不是commit()是因为commit()是同步写磁盘在主线程调用会阻塞。apply()会先更新内存再异步落盘。但这里有个坑必须提醒apply()不保证在进程被杀之前一定写完如果你存的是那种“丢了会导致用户重新登录”的关键数据要么用commit()放到子线程要么换别的存储方案。我一般在登录 token 这种场景上会单独用同步写 子线程的组合。另外一个细节SharedPreferences在 Android 里首次getSharedPreferences会触发一次磁盘读取如果在主线程对一个大文件做这件事会有可见的卡顿。所以我在by lazy里做但调用方要保证第一次访问不在启动关键路径上或者提前在 Application 里预热一次。2.3 日志与调试开关日志这块最忌讳的是上线包里还在打Log.d。我的做法是把日志收拢到一个门面里用BuildConfig.DEBUG控制object L { private const val TAG App var enabled: Boolean BuildConfig.DEBUG fun d(msg: String, tag: String TAG) { if (enabled) Log.d(tag, msg) } fun e(msg: String, t: Throwable? null, tag: String TAG) { if (enabled) Log.e(tag, msg, t) } inline fun t(block: () - String) { if (enabled) Log.d(TAG, block()) } }那个t()的写法值得展开说说。日志里拼接字符串是有开销的尤其是那种list size ${list.size}, first ${list.firstOrNull()}的写法即使日志被关掉字符串拼接也照样执行。用inline加 lambda编译器会把整块代码内联进调用点enabled为 false 时连 lambda 对象都不会创建。这个小技巧在列表滚动、频繁回调的场景里能省下不少开销实测下来一个长列表的滚动帧率能差出几帧。还要注意一点日志里绝不能打用户隐私数据。手机号、身份证、token、设备唯一标识这些哪怕在 debug 包里也应该脱敏。我见过一个真实案例某项目 debug 包流到外部日志文件里带着完整手机号后续处理起来非常麻烦。所以我在L里加了个mask()扩展字符串超过一定长度就只保留首尾。2.4 软键盘、Toast 与状态栏软键盘的收起这个需求看起来简单实际上不同场景写法不同。如果当前焦点在某个 EditText 上用InputMethodManager.hideSoftInputFromWindow(view.windowToken, 0)就够了但如果焦点不在输入框上这个方法可能无效得先清焦点再收键盘fun Activity.hideKeyboard() { val imm getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager val v currentFocus ?: window.decorView imm.hideSoftInputFromWindow(v.windowToken, 0) v.clearFocus() }顺序很关键。先hide再clearFocus反过来有时候键盘会先收起来又因为焦点变化弹回来表现为闪一下。这个现象在带搜索框的页面上特别常见。Toast 在 Android 11 之后对自定义 View 做了限制背景是纯文本的 Toast 在后台也没法弹了。所以合集里我把 Toast 统一换成 Snackbar 或者自己画一个浮层。如果非要用 Toast记得做版本判断Toast.makeText在低版本上连续弹会排队用户点十次按钮就要等十次提示走完体验很差用一个静态 Toast 实例复用它比较好。3. UI 与交互层的高频片段3.1 RecyclerView 通用适配器的骨架RecyclerView 的适配器是重复代码的重灾区。每次新建一个列表都要写ViewHolder、onCreateViewHolder、onBindViewHolder、getItemCount四个方法而且写法几乎一模一样。抽一个泛型基类能省下大量样板abstract class BaseAdapterT( private val layoutId: Int ) : RecyclerView.AdapterBaseAdapter.VH() { private val items mutableListOfT() fun submit(list: ListT?) { items.clear() if (!list.isNullOrEmpty()) items.addAll(list) notifyDataSetChanged() } abstract fun bind(holder: VH, item: T, position: Int) override fun getItemCount() items.size override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH { val v LayoutInflater.from(parent.context).inflate(layoutId, parent, false) return VH(v) } override fun onBindViewHolder(holder: VH, position: Int) { bind(holder, items[position], position) } class VH(val itemView: View) : RecyclerView.ViewHolder(itemView) { Suppress(UNCHECKED_CAST) fun V : View find(id: Int): V itemView.findViewById(id) as V } }用的时候只需要继承并实现bindholder.findTextView(R.id.tv_title).text item.title一行搞定。这里find用了泛型强转省掉每次手动 cast代价是如果 id 和类型对不上会在运行时崩所以命名上我会保持约定比如tv_前缀一定是 TextView。要特别说明的是notifyDataSetChanged()。这个方法会触发全量重绘和动画丢失列表大了以后滑动会卡。生产环境应该换成DiffUtil或者ListAdapter我在合集里把它单独作为一个“进阶版本”放着基础版本只用于数据量小、刷新不频繁的页面。这个取舍要写清楚否则新人拿去就用在长列表上回头还要问为什么滑动掉帧。3.2 倒计时按钮与双击退出获取验证码的倒计时按钮几乎是每个带登录功能的 App 都要写。核心逻辑不复杂但边界条件容易漏fun TextView.startCountdown(total: Int 60, normal: String 获取验证码) { isEnabled false var left total val handler object : Handler(Looper.getMainLooper()) { override fun handleMessage(msg: Message) { if (left 0) { text ${left}s 后重发 left-- sendEmptyMessageDelayed(0, 1000) } else { isEnabled true text normal } } } handler.sendEmptyMessage(0) }坑在哪第一Activity 销毁时 handler 还在跑会持有 View 引用造成泄漏所以要么在onDestroy里removeCallbacksAndMessages(null)要么用lifecycleScopedelay重写。第二倒计时期间用户切到后台Handler 的消息仍然按时间走但界面没刷新回来时可能已经数完了这其实是正确行为但要注意别在onResume里重新启动一次计时。第三按钮isEnabled false之后视觉上要有变化否则用户以为没点到会连续猛戳。双击返回退出同理核心是记录时间戳private var lastBack 0L override fun onBackPressed() { val now System.currentTimeMillis() if (now - lastBack 2000) { lastBack now toast(再按一次退出) } else { super.onBackPressed() } }2000这个值不是随便定的。太短用户来不及按第二下太长又失去了防误触的意义。我试过 1500 和 2500最后选 2000配合提示文案用户接受度最好。另外注意 Android 13 之后onBackPressed被废弃新工程应该用OnBackPressedDispatcher注册回调这段老代码放在合集里主要是给存量项目参考。3.3 圆角、阴影与切图的代码替代早期做圆角背景习惯让设计切一张 png现在基本都是用shape或者MaterialCardView。合集里我常备一个通用的圆角背景生成函数fun roundRectDrawable( color: Int, radius: Float, strokeWidth: Int 0, strokeColor: Int Color.TRANSPARENT ): GradientDrawable GradientDrawable().apply { shape GradientDrawable.RECTANGLE cornerRadius radius setColor(color) if (strokeWidth 0) setStroke(strokeWidth, strokeColor) }用代码生成的好处是颜色能跟着主题走夜间模式切换时不用维护两套资源。但要注意cornerRadius是 px 不是 dp直接写数字在不同密度屏幕上效果差别很大所以我会再包一层dp2px转换。阴影这块elevation在 Android 5.0 以下不生效如果最低版本还压到 4.x就要用layer-list画假阴影。不过现在最低版本普遍 21 往上这个坑基本不用管了。真正容易出问题的是给elevation加了之后 View 的绘制顺序变化某些情况下会被父容器裁剪表现是阴影只出来一半这时候要在父布局加android:clipChildrenfalse和clipToPaddingfalse。3.4 沉浸式布局的适配写法沉浸式这块WindowCompat出来之后写法统一了很多fun Activity.applyEdgeToEdge() { WindowCompat.setDecorFitsSystemWindows(window, false) ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { v, insets - val bars insets.getInsets(WindowInsetsCompat.Type.systemBars()) v.setPadding(bars.left, bars.top, bars.right, bars.bottom) insets } }注意最后必须把insets返回回去否则内层 View 收不到分发。我见过有人写成WindowInsetsCompat.CONSUMED结果内部的 EditText 在键盘弹出时不会被顶起来表现为输入框被键盘挡住。还有一个实际问题是状态栏图标颜色。浅色背景要配深色图标深色背景要配浅色图标这个要用WindowInsetsControllerCompat设置isAppearanceLightStatusBars。如果页面上有图片轮播背景色会变那就得动态改。我一般会留一个setStatusBarStyle(isLight: Boolean)的扩展在页面切换时调。4. 异步、网络与数据的常用写法4.1 网络状态判断与请求兜底前面提过网络判断要分两层。第一层看有没有可用网络fun Context.hasNetwork(): Boolean { val cm getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager return if (Build.VERSION.SDK_INT Build.VERSION_CODES.M) { cm.activeNetwork?.let { cm.getNetworkCapabilities(it) }?.let { it.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) } ?: false } else { Suppress(DEPRECATION) cm.activeNetworkInfo?.isConnected true } }注意这里用的是NET_CAPABILITY_INTERNET而不是NET_CAPABILITY_VALIDATED。前者只表示“这个网络声称能上网”后者表示“系统实际探测过确实能上网”。用前者会漏掉那种连上了 WiFi 但需要网页认证的场景。所以更靠谱的做法是结合一个轻量的探测请求比如请求自己的接口返回一个极小的响应体成功才算真通。第二层是请求兜底。我习惯给所有网络请求统一包一层重试suspend fun T retry(times: Int 2, block: suspend () - T): T { var last: Throwable? null repeat(times 1) { try { return block() } catch (e: Throwable) { last e if (e !is IOException) throw e } } throw last!! }这里只对IOException重试因为超时、连接失败这类才值得重试业务错误码重试没有意义。另外要注意重试之间最好加个退避直接连着发三次反而更容易把服务端打满。4.2 协程作用域与 Handler 的防泄漏写法现在新代码基本都用协程了但存量项目里 Handler 还大量存在。Handler 的经典写法是用静态内部类加弱引用class SafeHandler(looper: Looper) : Handler(looper) { private val ref WeakReference(this) override fun handleMessage(msg: Message) { ref.get()?.onHandle(msg) ?: return } open fun onHandle(msg: Message) {} }不过说实话改成协程之后这类问题少了很多。用lifecycleScope.launch启动的任务会跟随 Activity 生命周期自动取消不需要手动管理。但要注意一个反例在lifecycleScope里做耗时操作用户退到后台不会取消只是界面不再更新。如果这个操作有关键副作用比如上传文件那要考虑用repeatOnLifecycle或者放到 ViewModel 的viewModelScope里。还有个细节viewModelScope在onCleared时取消但配置变更比如旋转屏幕不会触发这是它的优势。我一般把“跟业务数据相关的异步”放viewModelScope“跟界面动画相关的”放lifecycleScope这个分工在多数项目里都适用。4.3 文件与图片的本地缓存缓存这块合集里常备两个函数一个算缓存目录大小一个清理缓存。这两个功能任何 App 的“设置”页都要有fun Context.cacheSize(): Long { val dir cacheDir return dir.walkTopDown().filter { it.isFile }.sumOf { it.length() } } fun Context.clearCache(): Boolean try { cacheDir.listFiles()?.forEach { it.deleteRecursively() } true } catch (e: Exception) { false }注意walkTopDown在大目录下会遍历所有子文件如果缓存文件有几万个这个计算会明显卡顿务必要放到子线程。我在合集里标注了“必须在 IO 线程调用”并且给了一个带格式化输出的扩展把字节数转成 MB、GB 显示。图片压缩这块很多人会直接拿Bitmap.compress按质量压缩但质量参数对 PNG 是无效的只有 JPEG 和 WEBP 才起作用。所以更稳妥的做法是先按尺寸缩放再按质量压缩fun Bitmap.scaleTo(maxSide: Int): Bitmap { val w width; val h height val ratio maxSide.toFloat() / maxOf(w, h) if (ratio 1f) return this return Bitmap.createScaledBitmap(this, (w * ratio).toInt(), (h * ratio).toInt(), true) }缩放时用filter true会走双线性插值效果比默认好但内存峰值也高一点。对于大图建议用inSampleSize在解码阶段就降采样而不是先完整解码再缩放否则内存峰值可能直接 OOM。4.4 数据库升级与迁移片段如果项目用了 Room版本升级一定要配 Migration否则用户升级 App 后一打开就崩。合集里我放了一个模板val MIGRATION_1_2 object : Migration(1, 2) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL(ALTER TABLE note ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0) } }有两个坑必须说。第一ALTER TABLE加非空列时必须给DEFAULT否则已有数据行会违反约束。第二迁移里改过的表结构实体类里的字段声明必须完全一致包括NOT NULL和默认值。我见过实体类写了ColumnInfo(defaultValue 0)但迁移语句没写编译期看不出来运行到迁移时直接崩。还有一个更隐蔽的问题Room 的exportSchema默认是 true会生成 json 文件。如果团队多人协作这个文件一定要提交到版本库否则合并时容易丢字段。我一般还会在 CI 里加一步校验确保所有迁移路径都有对应测试。5. 权限、设备信息与兼容性适配5.1 动态权限申请的封装Android 6.0 之后权限要动态申请ActivityResultContracts出来之后流程清爽多了class PermRequester( private val host: ComponentActivity, private val onResult: (MapString, Boolean) - Unit ) { private val launcher host.registerForActivityResult( ActivityResultContracts.RequestMultiplePermissions() ) { onResult(it) } fun request(vararg perms: String) launcher.launch(perms) }这里的关键是registerForActivityResult必须在 Activity 的STARTED之前调用也就是写在字段初始化里。如果在点击事件里才注册会抛IllegalStateException。这一点很多新人会踩写成fun onClick() { registerForActivityResult(...).launch(...) }就崩了。权限还有几个容易忽视的细节。一是永久拒绝的状态判断用户勾了“不再询问”之后再次申请系统会直接返回拒绝且不弹窗这时候要引导用户去设置页。判断方法是用shouldShowRequestPermissionRationale返回 false 且权限未授予通常就意味着永久拒绝。二是 Android 11 的“仅本次允许”和“仅使用时允许”位置权限拿到的精度可能不一样业务上要做好降级。三是 Android 13 的分媒体权限读图片要用READ_MEDIA_IMAGES不再走READ_EXTERNAL_STORAGE。5.2 设备与版本信息的采集做问题排查时用户反馈的“闪退”往往需要设备信息。合集里常备一段采集fun deviceSummary(): String buildString { append(BRAND).append(Build.BRAND) append(, MODEL).append(Build.MODEL) append(, SDK).append(Build.VERSION.SDK_INT) append(, ABI).append(Build.SUPPORTED_ABIS.joinToString(/)) append(, LOCALE).append(Locale.getDefault()) }注意这里没有采集 IMEI、Android ID 这类唯一标识一方面是隐私合规要求另一方面这些值在系统升级、恢复出厂后可能变化做用户追踪并不可靠。真要区分设备用自己生成的随机 UUID 存在应用私有目录里更合适。Build.MODEL有时会带渠道后缀比如同一款机型在不同批次显示的型号字符串不一样做统计时最好做一次归一化。我一般会取Build.BRAND Build.MODEL再去掉空格和特殊字符这样至少能把同一品牌聚到一起。5.3 厂商差异的兜底处理国内厂商系统在后台限制、自启管理、通知权限上的策略差异很大这块没法用一段代码解决但可以做一些兜底。比如通知权限Android 13 之前areNotificationsEnabled()就够用之后还要检查POST_NOTIFICATIONS运行时权限。再比如后台启动 ActivityAndroid 10 之后默认被禁需要走全屏 Intent 或者通知这段逻辑如果不做兜底跳转就会静默失败。我的做法是在合集里放一个compat/目录每个厂商一个小的判断函数只做“能不能”的判断不做具体的引导跳转。原因是引导跳转的设置页名称各版本都在变写死了反而容易失效。更稳妥的方式是引导用户自己去系统设置里找配合一张截图说明比调 Activity 来得可靠。6. 常见问题与排查技巧实录6.1 崩溃与内存问题的排查表把常用片段用起来之后最常见的问题其实集中在几个固定位置。下面这张表是我这些年整理出来的高频对照遇到问题先扫一眼能省掉大量试错现象大概率的根因排查动作弹窗时崩溃日志提示 WindowManager$BadTokenException用了 Application 上下文弹窗或 Activity 已销毁检查 Dialog 的 Context 来源加isFinishing判断列表滑动掉帧notifyDataSetChanged全量刷新或onBindViewHolder里做耗时操作换DiffUtil把图片加载和格式化移出绑定逻辑内存持续上涨不回落静态变量持有 Activity或监听器没反注册用内存分析工具抓堆转储重点看 Activity 实例数量收不到广播或回调注册在错误的生命周期或 Android 版本限制隐式广播检查注册时机是否在onStart之后确认是否受版本限制界面在分屏下错乱缓存了屏幕尺寸未响应配置变更在onConfigurationChanged重新计算布局参数安装后首次启动白屏时间长Application 初始化做了重活把非关键初始化延后或用启动任务框架分级加载表格里最后一条值得多说两句。启动优化最容易犯的错误是把所有 SDK 初始化都堆在Application.onCreate里尤其是那些“可能用得上”的功能。我的做法是分三级必须同步初始化的崩溃上报、配置读取放主线程次要的统计、推送放到首帧之后边缘的分享、地图按需懒加载。这么改完冷启动时间通常能压下去三到四成。6.2 踩坑记录与独家心得有几条经验是文档里不会写、但实际会反复遇到的。第一条关于代码粘贴。从别处拷来的代码包名和资源 id 一定要全局搜一遍替换尤其是R.id.xxx这种粘过来编译不报错但运行时找不到 View返回 null 再一调用就空指针。我现在养成习惯粘完代码先搜一遍R.逐个确认。第二条关于版本判断。Build.VERSION.SDK_INT 某版本这个判断很多人会写成。差一个符号在刚好等于那个版本的机器上行为就完全不同。而且这种 bug 极难在开发机上复现因为开发机通常是新系统。所以我在合集里约定所有版本判断统一写成并且常量用Build.VERSION_CODES.XXX而不是数字可读性更好。第三条关于资源释放。Cursor、InputStream、Bitmap这些用完必须关。use {}扩展能自动处理但要注意Bitmap.recycle()在 Android 3.0 之后其实不强制调用了过早 recycle 反而可能让仍在显示的 ImageView 崩掉。所以图片这块我不做手动回收靠内存缓存和软引用策略来控制。第四条是关于代码注释。片段库里的每个函数我都会写一行“什么情况下不要用”。这比写“这个函数是干什么的”有用得多。比如hasNetwork()上面我写的是“不要用它判断能否真正访问服务端只用于快速失败”就是这么一句避免了好几次误用。6.3 常用片段速查表最后把合集里最高频的几个片段按用途归一下类方便按需取用。这张表我一般贴在团队文档首页新人入职先看这个用途片段名关键注意点全局上下文AppCtx.get()必须在 Application 中初始化否则抛错圆角背景roundRectDrawable()半径单位是 px调用前先做 dp 转换权限申请PermRequester必须在字段初始化时注册 launcher网络判断hasNetwork()只判断接口可用性不代表能连通图片降采样Bitmap.scaleTo()优先用inSampleSize在解码阶段处理键盘收起Activity.hideKeyboard()先 hide 再 clearFocus沉浸式applyEdgeToEdge()记得返回 insets 继续分发缓存清理clearCache()必须放子线程大目录会卡我个人在实际操作中的体会是代码合集这个东西真正的价值不在于“省了多少行”而在于它把一些隐性的经验固化了下来。一段代码写得漂亮不难难的是把“什么时候不该用”也一并写进去。我现在的习惯是每次踩完一个坑如果这个坑能用一段代码或者一句注释防住就立刻补进合集并且写清楚触发条件。时间长了这本合集就不只是模板库了更像是一本自己写给自己的故障手册翻起来比搜任何资料都快。
企业数字化 ERP 产品动态
相关推荐
C语言程序只能单文件?解析翻译单元与多文件工程化 这道选择题在网络题库里被转发了不知道多少遍,但真正把它当回事的人不多——大部分人对着答案点一下"下一篇"就过去了。前阵子有个刚入行的朋友拿着这道题来问我,说他选 A 选得很确定,理由是"我写了这么久 C,都是在… · 2026/11/3 4:37:50
牛过桥面试题背后的资源受限决策与方案拆解 “一头牛重800公斤,一座桥承重700公斤,牛怎么过桥?”这道题在网上流传了很多年,隔一阵子就被翻出来讨论一轮,常被冠上某知名科技企业的面试题名头,也被无数人当成段子转发。我第一次听到它的时候࿰… · 2026/11/3 4:37:50
Fast-LIO实时SLAM原理与GPS融合坐标系对齐避坑指南 简介:本资源是一份面向自动驾驶与机器人领域研究人员及工程师的Fast-LIO算法深度解析与工程改进文档,聚焦多传感器融合建图与定位中的关键瓶颈——点云特征缺失导致的建图失败问题。作者在原始Fast-LIO(融合IMU与激光雷达)基础上&… · 2026/11/3 5:06:54
Fast-LIO+GPS紧耦合融合:状态扩展与鲁棒观测建模实战 简介:本资源是一份面向机器人与自动驾驶领域研究人员及工程师的Fast-LIO算法深度解析与工程改进文档,聚焦多传感器融合建图与定位中的关键瓶颈——点云特征缺失导致的建图失败问题。作者在原始Fast-LIO(IMU激光雷达紧耦合)基础上&… · 2026/11/3 5:06:54
深入理解switch-case:语法、优化与多语言实践 1. 理解switch-case的基本概念switch-case是一种多分支选择结构,它允许程序根据表达式的值从多个代码块中选择一个执行。这种结构在几乎所有主流编程语言中都存在,包括C、C、Java、JavaScript等。switch语句的基本工作原理是:首先计算一个表达… · 2026/11/3 5:06:54
SE注意力机制原理与PyTorch实现详解 1. 通道注意力机制概述通道注意力(Channel Attention)是计算机视觉领域中一种重要的注意力机制,它通过学习不同特征通道的重要性权重,来增强有用特征并抑制无用特征。SE(Squeeze-and-Excitation)注意力是最… · 2026/11/3 5:06:54
2025届AI科研平台实测:五大工具深度对比与选型指南 1. 2025届AI科研平台实测背景与选型标准在2023-2024年全球AI技术爆发式增长的背景下,科研领域的工具生态发生了革命性变化。根据斯坦福大学《AI Index 2024》报告显示,学术工作者使用AI工具的比例从2021年的17%激增至2024年的89%,其中专门针对… · 2026/11/3 5:06:54
基于STM32与AD5542的微伏级脑电信号模拟发生系统设计 简介:面向脑机接口测试场景的高精度脑电信号模拟发生系统设计资料,适合医疗设备制造商、科研工作者及脑机接口开发团队参考。针对传统信号发生器系统复杂、体积庞大、精度不足且难以生成真实脑电模型的问题,资料围绕数模转换、同相比例放大与… · 2026/11/3 5:05:54
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 4:42:45
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 5:17:18
STM32F411RE与MAX14431实现SPI隔离:工业现场抗共模干扰实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
ESP32+大模型AI硬件开发:从Demo到产品的8个工程问题 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
基于MAX14431BAEE+与PIC18F85J10的SPI隔离方案设计与实践 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 4:42:45
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 5:17:18