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

Android(AOSP)编程开发


综合/最新

Android开发Android UIAndroid API LevelsAndroid玩机

Android系统开放源码(页首代码搜索/墙外)
新:
  通过NsdManager/UPNP+TCP/UDP暴漏一个Android Service,自家App共享其已授权的能力。

综合:
  Jetpack Compose 和 SwiftUI 延续了 React 框架的声明式和虚拟DOM(Diff算法/状态驱动)两大特性。
  Android Studio 安装后首次启动应指定下 Custom -> Android SDK Location: F:\big\ide-as\Sdk\,可节约 15GiB 空间。
  Android Studio 分为基础版和 Feature Drop 版,前者只升级IDEA底层,而后者则会在前者Beta后启动开发,并添加 Android 新特性,后者正式发布后,前者结束更新。
  Android SDK Manager 已不再提供 SDK Docs 了; curl.exe https://dl.google.com/android/repository/repository2-3.xml -I
  Android SDK 版本:
    minSdk 指应用不能安装在低于该版本的Android OS中,与限制策略无关:
      首选28+,依赖少则26+(支持MethodHandle),最小24+(因Android 8的26启动服务需要通知),或19+(PlayServices最低支持)
      忽略库较高minSdk - <uses-sdk tools:overrideLibrary="com.lib1, com.x"/>
      Android 9(28)+系统支持v3签名;图片动画和ImageDecoder取代BitmapFactory、BiometricPrompt生物识别框、Wi-Fi RTT室内定位、类似安全芯片TPM的StrongBox Keymaster技术

      Google Play services只支持Android 4.4+/19+系统
      Android 11+/30+系统支持v4签名、ControlsProviderService

    targetSdk - 首选28+;29+(演示/后台无法启动Activity/但支持WorkManager调试REQUEST_DIAGNOSTICS),Google Play要求31+(后台禁止启动窗体)
      指即使运行在高于其指定版本的Android OS中,依然维持老版本的调用行为,不执行高版本的限制策略,例如6.0起的显式权限申请。另外,如果想用新的特性,提升targetSdk即可。

    compileSdk(仅编译期检查用)和buildToolsVersion(可忽略)对运行时没有任何影响,故取最高即可。

死记、偏门:
  图标:Android 图标尺寸最低为 256x256 像素;Google Play商店网页展示图标最低要求为 32 位 PNG(含 alpha 透明层)、尺寸 512x512 像素、文件大小上限 1024 KB。
  可触摸 UI 元件的标准为 48dp=72px=9mm,即一个手指能够准确舒适触摸的最小尺寸,最小可点击区域。
  暂时禁用日志刷屏的应用:./adb shell pm disable-user com.google.android.apps.youtube.music
  约定:java包nonui下放BroadcastReceiver和Service,ui包则放Activity。
  授权通知才能Toast(前台Activity则永远能弹Toast) - NotificationService Error: Suppressing toast from package app.name by user request.
  录屏排除悬浮窗 - 
    Android应用层方案:layoutParams.setTitle("已豁免的厂商录屏App悬浮窗口名")。
    Android系统层方案:设置 LayoutParams.FLAG_SECURE 后系统默认会用黑色填充,直接将宽高归零不就录不上了;
      surfaceflinger/LayerFE.cpp 中 if(blackOutLayer) 的 prepareClearClientComposition(...) 增加 layerSettings.geometry.boundaries = FloatRect(0.f, 0.f, 0.f, 0.f);
    或 surfaceflinger/CompositionEngine/src/Output.cpp rebuildLayerStacks(...)
  全局异常处理:
    Application onCreate() - Thread.setDefaultUncaughtExceptionHandler(new UEH());

    public class UEH implements Thread.UncaughtExceptionHandler {
      Thread.UncaughtExceptionHandler defaultHandler;
      public UEH() { defaultHandler = Thread.getDefaultUncaughtExceptionHandler();}

      @Override public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) {
        if (defaultHandler != null) { defaultHandler.uncaughtException(t, e); }
      } // 也可交回默认异常处理器
    }

  进程生命周期:
    ⬇前台进程(foreground process/oom_adj=0) - 有Activity界面直接交互的情况。
    ⬇可见进程(visible process) - 存在执行了Service.startForeground()的Service,则处于该状态;同时,还可通过startService()嵌套调startService()。
    ⬇服务进程(service process) - 如果有通过startService()启动的Service,则处于该状态,但在“无可见UI+无前台服务”数分钟后会强制降级为“缓存进程”,若在该Service里嵌套调startService()会报“Not allowed to start service Intent”,在降级前嵌套调startForegroundService()则不会报错;bindService()方式则不受影响;SYSTEM_ALERT_WINDOW+可见悬浮窗也可后台调startForegroundService()。
    ⬇缓存进程(cached process) - 降级为该状态后,系统会主动触发stopService();内存不足后会先拿其开刀,但若被其他app调起,则会提升至上方进程状态。
    无进程 - 即强制停止后的IMPORTANCE_GONE状态。

  查看PID: ./adb shell pidof com.openle.v1app.social.fullchannel
  查看优先级: ./adb shell cat /proc/[PID]/oom_adj

  前台服务(Foreground Services/FGS)任务管理器:下拉顶部通知栏,再二阶段下拉就能看到左下角的前台服务列表了,若用户在此点了“Stop”会连带整个应用关闭,但与“强制停止”不同的是,闹钟和WorkManager不会失效。
  [似乎已废弃]前台服务在24小时内连续运行20小时以上,会弹通知引起用户注意(“system-notification-long-running-fgs”),抑制1像素保活方案。
  系统源码添加白名单 - com.android.server.am.OomAdjuster、ProcessList.setOomAdj?

  开发者选项的“不保留活动”开启后,会导致进入二级页面时,按Home键就会销毁MainActivity了,二级页面再finish()就找不到MainActivity了,故时间充裕的话,应测测看:
    判断后提示用户关掉:Settings.Global.getInt(getContentResolver(), Settings.Global.ALWAYS_FINISH_ACTIVITIES, 0);

Android虚拟机:
  Android Runtime(ART)虚拟机取代了早期的Dalvik虚拟机,但均使用由d8转换后的DEX字节码格式(*.dex或*.aar),无法识别纯Java编译打包的*.class或*.jar文件。
  Java转Dex命令:d8 MyProject/app/build/intermediates/classes/debug/*/*.class 或 d8 java.jar --output out.dex
  动态载入及反射调用:DexClassLoader对象。

内容

Android BroadcastReceiver:
  开机广播:
    说明 - [targetSdk = 34 亲测]能启动部分前台服务,但只有授权通知后,才允许Toast和显示“前台服务运行通知”;[未测]起个悬浮窗是否能Toast?
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
    <receiver android:name=".nonui.BootReceiver" android:enabled="true" android:exported="true">
      <intent-filter><action android:name="android.intent.action.BOOT_COMPLETED" /></intent-filter>
    </receiver>

Android Service:
  前台服务:
    说明 - Activity启动的前台服务会弹出“前台服务运行通知”,但BOOT_COMPLETED则必须再授予通知权限后才会通知,不授权则只是不通知,而不会影响该服务运行。
    短期服务(android:foregroundServiceType="shortService") - 3分钟后系统会触发onTimeout(int startId)来stopSelf(),优势是支持在 BOOT_COMPLETED 内启动,且不需要上架Google Play的人工审核。

调起Android系统内置邮箱应用: startActivity(new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_APP_EMAIL).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));

Android网络广播默认禁用,需要WifiManager.createMulticastLock(String tag)来启用。

存储:
  常用目录 - getCacheDir()、getFilesDir()。
  临时写入文件:
    try (var fos = new FileOutputStream(File.createTempFile("x".repeat(3), ".txt",
            new File(System.getProperty("java.io.tmpdir")))); // 即 getCacheDir(),不提供则由系统决定。
            var ch = fos.getChannel()) { // 取代 InputStream/OutputStream;返回写入字节数。
        ch.write(ByteBuffer.wrap("Hi".getBytes()));
    } catch (IOException e) { }

  通过 Environment.DIRECTORY_PICTURES 可获取用户更改的默认图片存储位置,实际路径通常是: /storage/emulated/0/Pictures/;但应首选非写死的 ContentUri。
  contentValues.put(MediaStore.Images.ImageColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES); // 指定约定目录或子目录;或 put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/subDir")


保活:
  Android一像素Activity进程保活
  15分钟区间保活:WorkManager+闹钟

  ROOT保活:
    ./adb root
    ./adb remount
    ./adb push system/priv-app/folder_name/ apk_path
    ./adb shell sync
    ./adb reboot

  进程活跃度受限:
    var usm = (UsageStatsManager) getSystemService(Context.USAGE_STATS_SERVICE);
    var asb = usm.getAppStandbyBucket(); // ./adb shell am get-standby-bucket PACKAGE_NAME
    if (asb > UsageStatsManager.STANDBY_BUCKET_ACTIVE) {
        //应用可能受限
        System.out.println(asb);
    }


FGS 任务管理器 向上滑动 强行停止
立即从内存中移除应用
停止媒体播放
停止 FGS/移除关联的通知
移除 activity 返回堆栈
从历史记录中移除应用
取消预定作业
取消闹钟

UI

资源对象:
  格式 - PixelFormat.RGBA_8888(系统默认) 比 RGB_888 多一个Alpha透明通道位;RGBA_8888 bytes排列顺序为byte[0]=R、byte[1]=G、byte[2]=B、byte[3]=A、byte[4]=R...。
  android.media.Image - 图像原始数据和格式信息,通常为 YUV, PNG, RAW, RGBA; RGBA_8888 只用到了首个[0]平面 xImage.getPlanes()[0].getBuffer();
    BMP格式规定按行进行四字节对齐 - 补byte255:逐像素(RGBA) plane.getPixelStride(); 整行(width即宽度) plane.getRowStride(); 32 位图像(pixelStride=4)可以忽略 stride;后续研究 如果指定了与RowStride不能整除的width,是否要额外处理!?
  android.graphics.Bitmap - 已解码图像,通常为 RGB/PixelFormat.RGBA_8888 格式,无法区分PNG or WEBP格式了;比Image更适用于资源的设置。
  android.graphics.drawable.Drawable - 位图、矢量图容器
  图片存储:
    var file = File.createTempFile(UUID.randomUUID().toString(), ".png", getCacheDir());
    try (var fos = new FileOutputStream(file)) {
        bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos);
        bitmap.recycle();
    }

可视化:
  Spinner:
    Containers -> Spinner 拖拽至布局中,然后 Attributes -> entries -> 选择现有数组名 或 ➕Array Resource File -> File name:arr-res
      <?xml version="1.0" encoding="utf-8"?>
      <resources>
          <string-array name="str_arr"><item>1</item><item>2</item></string-array>
      </resources>
      取选中值: s.getSelectedItem().toString();

其他

华为鸿蒙系统(基于AOSP)

mDNS:
  根据树莓派服务类型检索网络地址:nsdManager.discoverServices("_workstation._tcp.",...);
    触发onServiceFound入参判断nsdServiceInfo.getServiceName().startsWith("raspberrypi")

  树莓派注册的服务类型:nsdServiceInfo.setServiceType("_workstation._tcp.");

解决录屏1分钟左右报: [ImageReader-102x223f1m60-3963-0](id:f7b00000003,api:1,p:1052,c:3963) dequeueBuffer: BufferQueue has been abandoned
  将 ImageReader 或 SurfaceTexture 变量从方法内提升为类级别,就不会被GC很快回收了。