从此
📄文章 #️⃣专题 🌐酷站 👨‍💻技术 📺 📱

Android OS应用开发、Android Studio IDE编程技术


综合

minSdk 首选 28+(Wi-Fi RTT); 自用支持MethodHandle的26+;targetSdk 首选 28+。
Android更多开发知识专题非技术性Android产品专题Google Play 应用分发
Android各版本统计(英文版数据较新)Google Play 政策讲解视频

Android Java 17 LTS新特性:HexFormat、record 和 多行文本块儿;
Android 14起访问或选取媒体文件时会多出一项“选择照片和视频”(READ_MEDIA_VISUAL_USER_SELECTED权限),避免只能全部允许的风险。
中国大陆手机不允许安装未备案app应用,可通过断网安装或第三方安装器解决。
Android Studio Koala起支持(新项目 -> Gemini API Starter)Gemini聊天、文生图: https://developer.android.com/studio/projects/templates#GeminiApiTemplate

Google Play Console 发布PC游戏(上架要求arm64 + x86-64) -> 设置 -> 高级设置 -> 设备规格 -> +添加“Google Play 游戏电脑版”;玩家客户端在设置中启用“Hyper-V Hypervisor”才不会卡顿。
Google Play Games 电脑版开发者模拟器 - PC端上架前测试用(打开即进入Android桌面/返回键为Ctrl+B,Home屏为Ctrl+H/用adb安装apk),右下角图标右键"Validate Game Readiness"。

Android程序构建

Android Studio IDE 官方下载、以及gradle等编程工具

Android Studio 最精简起步项目模板


Android核心服务、系统组件

谷歌三件套

Google Services Framework(com.google.android.gsf)

Google Play Services(com.google.android.gms)

Google Play Store


编程

死记:
  命名不符 - context.getPackageName() 其实返回的其实是AppId,并非Java类所在包名 String.class.getPackage().getName(),与未自定义的 Application.getProcessName() 同名;或直接用 BuildConfig.APPLICATION_ID。
  耗时 - StorageStatsManager#getFreeBytes(UUID) 无缓存时会阻塞1.5秒,应使其异步化。
  最佳实践 - service.stopSelf(service.startId); // 会等onStartCommand执行完;startId则能避免误关其他实例;若是前台服务可在 startForeground 的通知入参设置下关闭行为。

四大基本组件:Activity(必须静态注册至AndroidManifest.xml)、Service服务、BroadcastReceiver广播接收器(无法用bindService动态注册Service)、Content Provider内容提供者
  Service - 免审核用非长期驻留的android:foregroundServiceType="shortService";处于前台时可正常用无foregroundServiceType属性的普通Service,通过startService()来启动;高优先级FCM等同处于前台应用。

设置偏好组件(PreferenceFragmentCompat) - https://developer.android.google.cn/develop/ui/views/components/settings?hl=zh-cn
  getSupportFragmentManager().beginTransaction().replace(R.id.settings, new SettingsFragment()).commit();

生命周期:
    避免创建Application且启动较早做法 - Application attachBaseContext -> 【ContentProvider onCreate】 -> Application onCreate

上下文(android.content.Context):
  其子类ContextWrapper(包装了mBase)即Activity、Service、Application的基类;而BroadcastReceiver则只是继承于Object的回调函数而已。
  全局Application(周期最长)上下文 - context.getApplicationContext() == contextWrapper.getApplication();
  短生命周期上下文 - activity.getBaseContext()、(Activity)view.getContext() 或 MainActivity.this
  BroadcastReceiver继承于Object,其方法onReceive(context..)传入的Context类型不确定,需要转换为ContextWrapper后再调getApplication(),或用最宽泛的getApplicationContext()。
  由于全局上下文没有任务栈,故每次均要加上FLAG_ACTIVITY_NEW_TASK,所以跟Activity任务栈相关的,首推MainActivity.this.startActivity(intent)。
  访问已安装包的上下文:android:sharedUserId="com.example" 签名 createPackageContext("pkg.demo", Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY);  https://blog.csdn.net/u013398960/article/details/140178904

进程、线程:
  同步屏障 - 操作View必须在UI线程执行 getMainExecutor().execute(() -> { }); UI线程带延迟时间用 new Handler(getMainLooper()).postDelayed(() -> { /*...*/ }, 1000);
    若在非UI线程执行无参 Handler 会报 Can't toast on a thread that has not called Looper.prepare()  需启用下该线程的Looper对象 Looper.prepare(); var hWithLooper = new Handler(); Looper.loop();

  异步化 - 优选基于 ThreadPoolExecutor 的可重用线程池 Executors.newCachedThreadPool(); 底层时才用 new Thread()。


  系统版本查询:
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { /* 高版本API调用 */ }

    // 列出扩展支持版本 - adb shell getprop | grep build.version.extensions
    if (SdkExtensions.getExtensionVersion(VERSION_CODES.R) >= 3) { }

  pkg.R类导出前后均可见,包名由applicationId决定;而BuildConfig类则只存在于编译时,导出后所用之处会变为实际值,包名由namespace/package决定;
  启用构建配置常量:BuildConfig.VERSION_NAME; gradle.properties必须配上 - android.defaults.buildfeatures.buildconfig=true
    多变种时不能直接用R.string访问,应反射之:ViewCommon.getStringByResName(getApplicationContext(), "google_app_id");


  权限:
    静态权限 -
      <uses-permission android:name="android.permission.INTERNET" />
      <uses-permission android:name="com.google.android.gms.permission.AD_ID" />
      FOREGROUND_SERVICE(前台Service)、RECEIVE_BOOT_COMPLETED

    需要用户明确同意的权限 - "前台服务"的通知直接能弹,不受该权限影响。
      通知 - <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
        TargetSDK 为 Android 13 (33) + 不写上该权限,则 App Info 中启用通知的按钮处于禁点状态;早期版本则每次重启后,遇到nm.createNotificationChannel(nc)时,均有当次弹授权的机会。

    需要Google Play控制台审批的权限 - 
      USE_FULL_SCREEN_INTENT(闹钟、电话可在Play控制台申请默认启用)

  前台服务: // 短时服务用shortService,否则用未归类+持续执行的specialUse,Google Play上架时需要提供该服务使用录像。
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
    <service android:name="fooService" android:foregroundServiceType="specialUse">
      <property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE" android:value="Chat Service"/>
    </service>
    Service onCreate(): 未启用通知则不显示,但不影响前台服务正常执行;低于android 14则总是显示通知。
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
            startForeground(n.hashCode(), n, ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE);
        } else {
            startForeground(n.hashCode(), n);
        }

  共享存储:
    高于maxSdkVersion版本的则废弃此权限:

      <uses-permission
        android:name="android.permission.READ_EXTERNAL_STORAGE"
        android:maxSdkVersion="32" />
      <uses-permission
        android:name="android.permission.WRITE_EXTERNAL_STORAGE"
        android:maxSdkVersion="29" />

用以下细化权限项来替代: <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> 以及 READ_MEDIA_VIDEO READ_MEDIA_AUDIO Android 11起采用了包可见性机制,默认可见应用有:adb shell dumpsys package queries forceQueryable:标签下的apk 表示 可见所有已安装的apk queries via package name: 标签表示只有配置了包名 才对该app可见 queries via intent: 标签表示只有配置了intent 才对该app可见 queryable via uses-library: queryable via interaction: 应用包安装: 显式安装权限 - android.permission.REQUEST_INSTALL_PACKAGES 静默安装权限(只能用于系统应用) - android.permission.INSTALL_PACKAGES WebView载入目录索引页会报JavaScript脚本xxx未定义而页面空白 解决 AlertDialog+getApplicationContext() 报错 Unable to add window -- token null is not valid; 例外:Toast可用任何上下文及getApplicationContext() new AlertDialog.Builder(view.getContext()).setMessage("msg").create().show();// or MainActivity.this 解决横竖屏切换阻止构建 android:screenOrientation="portrait" tools:ignore="LockedOrientationActivity" tools:replace="android:screenOrientation" Gradle构建: android { buildFeatures { buildConfig = true } // 启用后才会生成BuildConfig.DEBUG defaultConfig { // x86_64、x86仅用于模拟器调试或ChromeOS系统 ndk.abiFilters 'arm64-v8a', 'x86_64','x86' // 'armeabi-v7a','arm64-v8a','x86','x86_64' } buildTypes { debug { ndk.abiFilters 'arm64-v8a', 'x86_64' } release { //noinspection ChromeOsAbiSupport ndk.abiFilters 'arm64-v8a' } // 只保留需要支持的系统so库 // 本地依赖库无名为other的buildType时进行降级 other { matchingFallbacks = ['debug', 'qa', 'release'] } } } Android插件Gradle国内仓库: [铁定被墙] google() 大陆镜像 https://maven.aliyun.com/repository/google 包含 com.android.application 等插件。 gradlePluginPortal() 即 https://plugins.gradle.org/m2/ 大陆镜像 https://maven.aliyun.com/repository/gradle-plugin 包含 gradle 插件(除 Android 外)。 gradlePluginPortal()改url用法 - GRADLE_OPTS环境变量值添加 -Dorg.gradle.internal.plugins.portal.url.override=https://maven.aliyun.com/repository/gradle-plugin mavenCentral() 大陆镜像 https://maven.aliyun.com/repository/central 开机解锁前应用执行 - android:directBootAware="true"只是标记下,并不会开机启动,还应标记下注册了LOCKED_BOOT_COMPLETED的receiver。 开机解锁后应用执行 - BOOT_COMPLETED 关闭代码分析提示: android { lint { abortOnError = false checkReleaseBuilds = false } // ... } 解决 Duplicate class kotlin... - implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.20"))

文件I/O、Uri

流读写操作:
  android.os.FileUtils(InputStream in, OutputStream out); // api29 - 带进度回调 FileUtils.ProgressListener
  inputStream.transferTo(OutputStream out); // api33

进程间文件共享:
  打开文件 - ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
  打开指向文件的Uri(content://x/path/path2/<_id字段值>或file://...,不能存在sub-section子部分?k=v或#、*通配符) - getContentResolver().openFileDescriptor(Uri uri, String mode, CancellationSignal cs);
  打开前缀必须为file://的Uri - getContentResolver().openFile(file://.../_id) - api29

其他

  非UI主线程报错:new Thread(() -> { Toast.makeText(context, "Can't toast on a thread that has not called Looper.prepare()", Toast.LENGTH_LONG).show(); }).start();
    解决 - context.getMainExecutor().execute(() -> { /* 等同 activity.runOnUiThread(() -> { }); */ });