2019-1-7

Android

官方文档
中文文档
w3cschool 的教程

Android 布局&UI Android 多媒体&群控 Android 混合开发 Android 四大组件 广播接收器 Android 四大组件 内容提供器 Android 四大组件 Activity Android 四大组件 Service Android Adapter MVC模型 Android Bluetooth Android CameraX Android NDK Android Zxing N_Android Hook 逆向 ArcGIS Runtime for Android

四大组件

组件是一个Android程序至关重要的构建模块.每一个组件都是系统进入你的应用的不同途径.但并不是所有的组件都是用户进入程序的真实入口, 其中一些要依赖于其它组件, 但是每一个组件都以自己独有的形式存在, 并发挥特殊的作用; 每一个组件都是一个唯一的模块, 帮助你实现程序的各种行为.

应用组件是 Android 应用的基本构建块.每个组件都是一个入口点, 系统或用户可通过该入口点进入您的应用.有些组件会依赖于其他组件.

共有四种不同的应用组件类型:

  1. Activity
  2. 服务
  3. 广播接收器
  4. 内容提供程序

当系统启动某个组件时,它会启动该应用的进程 (如果尚未运行) , 并实例化该组件所需的类. 例如, 如果您的应用启动相机应用中拍摄照片的 Activity, 则该 Activity 会在属于相机应用的进程 (而非您的应用进程) 中运行.因此, 与大多数其他系统上的应用不同, Android 应用并没有单个入口点 (即没有 main() 函数) .

由于系统在单独的进程中运行每个应用, 且其文件权限会限制对其他应用的访问, 因此您的应用无法直接启动其他应用中的组件, 但 Android 系统可以.如要启动其他应用中的组件, 请向系统传递一条消息,说明启动特定组件的 Intent.系统随后便会为您启动该组件.

启动组件 在四种组件类型中, 有三种 (Activity 服务和广播接收器) 均通过异步消息Intent 进行启动.Intent 会在运行时对各个组件进行互相绑定.您可以将 Intent 视为从其他组件 (无论该组件是属于您的应用还是其他应用) 请求操作的信使.

与 Activity 服务和广播接收器不同,内容提供程序并非由 Intent 启动.相反, 它们会在成为 ContentResolver 的请求目标时启动.内容解析程序会通过内容提供程序处理所有直接事务, 因此通过提供程序执行事务的组件便无需执行事务, 而是改为在 ContentResolver 对象上调用方法.这会在内容提供程序与请求信息的组件之间留出一个抽象层 (以确保安全) .

每种组件都有不同的启动方法:

  • Activity 如要启动 Activity, 您可以向 startActivity() 或 startActivityForResult() 传递 Intent (当您想让 Activity 返回结果时) , 或者为其安排新任务.

  • Service 在 Android 5.0 (API 级别 21) 及更高版本中, 您可以使用 JobScheduler 类来调度操作.对于早期 Android 版本, 您可以通过向 startService() 传递 Intent 来启动服务 (或对执行中的服务下达新指令) .您也可通过向将 bindService() 传递 Intent 来绑定到该服务.

  • Broadcast 您可以通过向 sendBroadcast() sendOrderedBroadcast() 或 sendStickyBroadcast() 等方法传递 Intent 来发起广播.

  • ContentResolver 您可以通过在 ContentResolver 上调用 query(), 对内容提供程序执行查询.

Android 开发指南 >应用组件

1.Activity

Android 四大组件 Activity

2.Service

Android 四大组件 Service

3.广播接收器

Android 四大组件 广播接收器

4.内容提供者

内容提供程序有助于应用管理其自身和其他应用所存储数据的访问,并提供与其他应用共享数据的方法。它们会封装数据,并提供用于定义数据安全性的机制。内容提供程序是一种标准接口,可将一个进程中的数据与另一个进程中运行的代码进行连。实现内容提供程序大有好处。最重要的是,通过配置内容提供程序,您可以使其他应用安全地访问和修改您的应用数据

.其他应用可通过内容提供程序查询或修改数据 (如果内容提供程序允许).

Android 开发指南 >内容提供程序

Intent 和 Intent 过滤器

Intent 是一个消息传递对象, 您可以用来从其他应用组件请求操作.尽管 Intent 可以通过多种方式促进组件之间的通信, 但其基本用例主要包括以下三个:

1.启动 Activity

Activity 表示应用中的一个屏幕.通过将 Intent 传递给 startActivity(), 您可以启动新的 Activity 实例.Intent 用于描述要启动的 Activity, 并携带任何必要的数据.

如果您希望在 Activity 完成后收到结果, 请调用 startActivityForResult().在Activity 的 onActivityResult() 回调中, 您的 Activity 将结果作为单独的 Intent 对象接收.

2.启动服务

Service 是一个不使用用户界面而在后台执行操作的组件.使用 Android 5.0 (API 级别 21) 及更高版本, 您可以启动包含 JobScheduler 的服务.如需了解有关 JobScheduler 的详细信息, 请参阅其 API-reference documentation.

对于 Android 5.0 (API 级别 21) 之前的版本, 您可以使用 Service 类的方法来启动服务.通过将 Intent 传递给 startService(), 您可以启动服务执行一次性操作 (例如, 下载文件) .Intent 用于描述要启动的服务, 并携带任何必要的数据.

如果服务旨在使用客户端-服务器接口, 则通过将 Intent 传递给 bindService(), 您可以从其他组件绑定到此服务.如需了解详细信息, 请参阅服务指南.

3.传递广播

广播是任何应用均可接收的消息.系统将针对系统事件 (例如: 系统启动或设备开始充电时) 传递各种广播.通过将 Intent 传递给 sendBroadcast() 或 sendOrderedBroadcast(), 您可以将广播传递给其他应用.

Intent 分为两种类型:

显式 Intent

通过提供目标应用的软件包名称或完全限定的组件类名来指定可处理 Intent 的应用.通常, 您会在自己的应用中使用显式 Intent 来启动组件, 这是因为您知道要启动的 Activity 或服务的类名.例如, 您可能会启动您应用内的新 Activity 以响应用户操作, 或者启动服务以在后台下载文件.

隐式 Intent

不会指定特定的组件, 而是声明要执行的常规操作, 从而允许其他应用中的组件来处理.例如, 如需在地图上向用户显示位置, 则可以使用隐式 Intent, 请求另一具有此功能的应用在地图上显示指定的位置.

Intent 和 Intent过滤器

Android Jetpack

https://toeii.github.io/2019/07/09/Android-Jetpack%E5%85%A8%E5%AE%B6%E6%A1%B6(%E4%B8%80)%E4%B9%8BJetpack%E4%BB%8B%E7%BB%8D/

Android 对进程的优先级

进程和应用生命周期

应用进程的生命周期并不由应用本身直接控制, 而是由系统综合多种因素来确定的, 比如系统所知道的正在运行的应用部分 这些内容对用户的重要程度, 以及系统中可用的总内存量.这是 Android 非常独特的一个基本功能.

应用开发者必须了解不同的应用组件 (特别是 Activity Service 和 BroadcastReceiver) 对应用进程的生命周期有何影响.这些组件使用不当会导致系统在应用进程正执行重要任务时将它终止.

为了确定在内存不足时应该终止哪些进程, Android 会根据每个进程中运行的组件以及这些组件的状态, 将它们放入”重要性层次结构”.这些进程类型包括 (按重要性排序):

1.前台进程

前台进程是用户目前执行操作所需的进程.在不同的情况下, 进程可能会因为其所包含的各种应用组件而被视为前台进程.如果以下任一条件成立, 则进程会被认为位于前台:

  • 它正在用户的互动屏幕上运行一个 Activity (其 onResume() 方法已被调用) .

  • 它有一个 BroadcastReceiver 目前正在运行 (其 BroadcastReceiver.onReceive() 方法正在执行) .

  • 它有一个 Service 目前正在执行其某个回调 (Service.onCreate() Service.onStart()Service.onDestroy()) 中的代码.

系统中只有少数此类进程, 而且除非内存过低, 导致连这些进程都无法继续运行, 才会在最后一步终止这些进程.通常, 此时设备已达到内存分页状态, 因此必须执行此操作才能使用户界面保持响应.

2.可见进程

可见进程正在进行用户当前知晓的任务, 因此终止该进程会对用户体验造成明显的负面影响.在以下条件下, 进程将被视为可见:

  • 它正在运行的 Activity 在屏幕上对用户可见, 但不在前台 (其 onPause() 方法已被调用) .举例来说, 如果前台 Activity 显示为一个对话框, 而这个对话框允许在其后面看到上一个 Activity, 则可能会出现这种情况.

  • 它有一个 Service 正在通过 Service.startForeground() (要求系统将该服务视为用户知晓或基本上对用户可见的服务) 作为前台服务运行.

  • 系统正在使用其托管的服务实现用户知晓的特定功能, 例如动态壁纸 输入法服务等. 相比前台进程, 系统中运行的这些进程数量较不受限制, 但仍相对受控.这些进程被认为非常重要, 除非系统为了使所有前台进程保持运行而需要终止它们, 否则不会这么做.

3.Service

包含一个已使用 startService() 方法启动的 Service.虽然用户无法直接看到这些进程, 但它们通常正在执行用户关心的任务 (例如后台网络数据上传或下载) , 因此系统会始终使此类进程保持运行, 除非没有足够的内存来保留所有前台和可见进程.

已经运行了很长时间 (例如 30 分钟或更长时间) 的服务的重要性可能会降位, 以使其进程降至下文所述的缓存 LRU 列表.这有助于避免超长时间运行的服务因内存泄露或其他问题占用大量内存, 进而妨碍系统有效利用缓存进程.

4.缓存进程

缓存进程是目前不需要的进程, 因此, 如果其他地方需要内存, 系统可以根据需要自由地终止该进程.在正常运行的系统中, 这些是内存管理中涉及的唯一进程: 运行良好的系统将始终有多个缓存进程可用 (为了更高效地切换应用) , 并根据需要定期终止最早的进程.只有在非常危急 (且具有不良影响) 的情况下, 系统中的所有缓存进程才会被终止, 此时系统必须开始终止服务进程. 这些进程通常包含用户当前不可见的一个或多个 Activity 实例 (onStop() 方法已被调用并返回) .只要它们正确实现其 Activity 生命周期 (详情请见 Activity) , 那么当系统终止此类流程时, 就不会影响用户返回该应用时的体验, 因为当关联的 Activity 在新的进程中重新创建时, 它可以恢复之前保存的状态.

这些进程保存在伪 LRU 列表中, 列表中的最后一个进程是为了回收内存而终止的第一个进程.此列表的确切排序政策是平台的实现细节, 但它通常会先尝试保留更多有用的进程 (比如托管用户的主屏幕应用 用户最后看到的 Activity 的进程等) , 再保留其他类型的进程.还可以针对终止进程应用其他政策: 比如对允许的进程数量的硬限制, 对进程可持续保持缓存状态的时间长短的限制等.

多网卡路由

路由表

ip route命令的输出显示了当前 Android 设备的路由表,每条路由条目表示网络流量的转发规则`

 
default via 192.168.144.1 dev ar_net0
192.168.20.0/24 dev wlan0 proto kernel scope link src 192.168.20.99
192.168.144.0/24 dev ar_net0 proto kernel scope link src 192.168.144.11
  • default via 192.168.144.1 dev ar_net0 默认路由(所有未明确指定目标的流量)通过网关 192.168.144.1,使用网络接口 ar_net0发送。
  • 192.168.20.0/24 dev wlan0 proto kernel scope link src 192.168.20.99 目标网络 192.168.20.0/24的流量直接通过 wlan0(Wi-Fi接口)发送,无需网关。

修改默认路由

​需要 root 设备

#  **删除现有的默认路由​**
ip route delete default
 
 
# **添加新的默认路由​**
ip route add default via 192.168.20.1 dev wlan0

指定网卡

NetworkCapabilities中定义的所有传输类型常量。你可以使用 hasTransport(int transportType)方法来检查一个网络是否属于特定类型。

常量描述
TRANSPORT_WIFI1设备通过 ​​Wi-Fi​​ 无线网卡连接到网络。
TRANSPORT_CELLULAR0设备通过 ​​蜂窝移动数据​​(如 4G, 5G)网卡连接到网络。这是最常用的两种类型。
TRANSPORT_BLUETOOTH2设备通过 ​​蓝牙​​ 网络共享(例如,通过另一台手机蓝牙共享网络)连接到网络。
TRANSPORT_ETHERNET3设备通过 ​​有线以太网​​ 网卡(通常需要 USB 转换器)连接到网络。常见于电视、平板等设备。
TRANSPORT_VPN4设备的所有网络流量都通过一个 ​​VPN​​ 隧道进行传输。这是一个逻辑传输类型,底层物理传输可能是 WiFi 或蜂窝。​​注意:​​ 一个网络可以同时拥有 TRANSPORT_VPN和 TRANSPORT_WIFI,表示这是一个基于 WiFi 的 VPN 连接。
TRANSPORT_WIFI_AWARE5设备通过 ​​Wi-Fi Aware​​(邻居感知网络,NAN)连接。用于设备间近距离高速通信。
TRANSPORT_LOWPAN6设备通过 ​​低功耗无线个域网​​(Thread, ZigBee 等)连接。常用于 IoT 设备。
TRANSPORT_TEST7一个用于 ​​测试​​ 的虚拟传输类型。应用通常不会遇到。
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
 
 
//  ConnectivityManager.NetworkCallback 通过回调的 android.net.Network#getSocketFactory 拿到SocketFactory
public static void requestNetwork(Context context, ConnectivityManager.NetworkCallback callback){
	ConnectivityManager connectivityManager = context.getSystemService(ConnectivityManager.class);
	connectivityManager.requestNetwork(
			new NetworkRequest.Builder().addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR).build(),callback
	);
}
    
 

日志框架

XLog

https://github.com/elvishew/XLog/blob/master/README_ZH.md

implementation("com.elvishew:xlog:1.10.1")

LogConfiguration config = new LogConfiguration.Builder()
    .logLevel(BuildConfig.DEBUG ? LogLevel.ALL             // 指定日志级别,低于该级别的日志将不会被打印,默认为 LogLevel.ALL
        : LogLevel.NONE)
    .tag("MY_TAG")                                         // 指定 TAG,默认为 "X-LOG"
    .enableThreadInfo()                                    // 允许打印线程信息,默认禁止
    .enableStackTrace(2)                                   // 允许打印深度为 2 的调用栈信息,默认禁止
    .enableBorder()                                        // 允许打印日志边框,默认禁止
    .jsonFormatter(new MyJsonFormatter())                  // 指定 JSON 格式化器,默认为 DefaultJsonFormatter
    .xmlFormatter(new MyXmlFormatter())                    // 指定 XML 格式化器,默认为 DefaultXmlFormatter
    .throwableFormatter(new MyThrowableFormatter())        // 指定可抛出异常格式化器,默认为 DefaultThrowableFormatter
    .threadFormatter(new MyThreadFormatter())              // 指定线程信息格式化器,默认为 DefaultThreadFormatter
    .stackTraceFormatter(new MyStackTraceFormatter())      // 指定调用栈信息格式化器,默认为 DefaultStackTraceFormatter
    .borderFormatter(new MyBoardFormatter())               // 指定边框格式化器,默认为 DefaultBorderFormatter
    .addObjectFormatter(AnyClass.class,                    // 为指定类型添加对象格式化器
        new AnyClassObjectFormatter())                     // 默认使用 Object.toString()
    .addInterceptor(new BlacklistTagsFilterInterceptor(    // 添加黑名单 TAG 过滤器
        "blacklist1", "blacklist2", "blacklist3"))
    .addInterceptor(new MyInterceptor())                   // 添加一个日志拦截器
    .build();
 
Printer androidPrinter = new AndroidPrinter(true);         // 通过 android.util.Log 打印日志的打印器
Printer consolePrinter = new ConsolePrinter();             // 通过 System.out 打印日志到控制台的打印器
Printer filePrinter = new FilePrinter                      // 打印日志到文件的打印器
    .Builder("<日志目录全路径>")                             // 指定保存日志文件的路径
    .fileNameGenerator(new DateFileNameGenerator())        // 指定日志文件名生成器,默认为 ChangelessFileNameGenerator("log")
    .backupStrategy(new NeverBackupStrategy())             // 指定日志文件备份策略,默认为 FileSizeBackupStrategy(1024 * 1024)
    .cleanStrategy(new FileLastModifiedCleanStrategy(MAX_TIME))     // 指定日志文件清除策略,默认为 NeverCleanStrategy()
    .flattener(new MyFlattener())                          // 指定日志平铺器,默认为 DefaultFlattener
    .writer(new MyWriter())                                // 指定日志写入器,默认为 SimpleWriter
    .build();
 
XLog.init(                                                 // 初始化 XLog
    config,                                                // 指定日志配置,如果不指定,会默认使用 new LogConfiguration.Builder().build()
    androidPrinter,                                        // 添加任意多的打印器。如果没有添加任何打印器,会默认使用 AndroidPrinter(Android)/ConsolePrinter(java)
    consolePrinter,
    filePrinter);
 
///////////////////
 
 
 
public static synchronized void init(Application application) {
        if (isInitialized) {
            return;
        }
        UncaughtHandler.getInstance().init(application);
        LogConfiguration config = new LogConfiguration.Builder()
                .logLevel( (BuildConfig.DEBUG) ? com.elvishew.xlog.LogLevel.ALL:com.elvishew.xlog.LogLevel.INFO)// 指定日志级别,低于该级别的日志将不会被打印,默认为 LogLevel.ALL
                .tag("X-LOG") // 指定 TAG,默认为 "X-LOG"
                .enableThreadInfo() // 允许打印线程信息,默认禁止
                .addInterceptor(new LogMiddleTruncateInterceptor(10000))
                .enableStackTrace(1) // 允许打印深度为 2 的调用栈信息,默认禁止
//                .enableBorder()// 允许打印日志边框,默认禁止
                .build();
        Printer androidPrinter = new AndroidPrinter(true);
        File logDir = new File(application.getExternalFilesDir(null), "xlogs");
        FilePrinter filePrinter = new FilePrinter.Builder(logDir.getAbsolutePath())
                .fileNameGenerator(new DateFileNameGenerator(){
                    public String generateFileName(int logLevel, long timestamp){
                        return super.generateFileName(logLevel,timestamp)+".log";
                    }
                })
                .flattener(new PatternFlattener("{d yyyy-MM-dd HH:mm:ss.SSS} {l}/{t}: {m}\t"))
                .cleanStrategy(new FileLastModifiedCleanStrategy(60 * 24 * 3600L))
                .build();
        XLog.init(config,androidPrinter, filePrinter);
        XLog.w("初始化于 "+ DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss.SSS"));
        isInitialized = true;
    }
 

记录未捕获异常 (秒退)

/**
 * Created by yang on 2019/6/12.
 */
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
 
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
 
public class UncaughtHandler implements Thread.UncaughtExceptionHandler {
    private static final String TAG = "UncaughtHandler";
 
    @SuppressLint("StaticFieldLeak")
    private static final UncaughtHandler crashHandler = new UncaughtHandler();
    private Context mContext;
    private Thread.UncaughtExceptionHandler mDefaultCaughtExceptionHandler;
 
    public static UncaughtHandler getInstance() {
        return crashHandler;
    }
 
    public void init(Context context) {
        mDefaultCaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
        Thread.setDefaultUncaughtExceptionHandler(this);
        mContext = context.getApplicationContext();
    }
    @SuppressLint("SimpleDateFormat")
    @Override
    public void uncaughtException(Thread thread, Throwable ex) {
        XLog.e(TAG+ "版本: "+getVersionName()+ " 未处理的异常:\r\n"+Log.getStackTraceString(ex));
        File extDirectory = mContext.getExternalFilesDir(null);
        String path = extDirectory.getPath();
        Date date = new Date();
        String yyyyMMddHHmmss = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(date);
        String content = "\r\n未处理的异常,时间:"+yyyyMMddHHmmss+"; \r\n"+Log.getStackTraceString(ex);
        FileUtil.appendUtf8String(content, path
                + File.separator +"logs"
                + File.separator + "exception_"+yyyyMMddHHmmss+".log");
    }
 
    private String getVersionName() {
        try {
            PackageInfo packageInfo = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0);
            return packageInfo.versionName;
        } catch (PackageManager.NameNotFoundException e) {
            XLog.e(TAG+ "Error getting version code: " + e.getMessage());
            return "";
        }
    }
}
  • 在 MainActivity 初始化 UncaughtHandler.getInstance().init(this);

动态权限

官方文档

 
class CamActivity : AppCompatActivity(),LocationListener, SensorEventListener {
	
	override fun onCreate(savedInstanceState: Bundle?) {
		super.onCreate(savedInstanceState)
		setTheme(android.R.style.Theme_Light_NoTitleBar_Fullscreen)//全屏
		if (allPermissionsGranted()) {
			initOnPermissionsGranted()//有权限了 继续
		} else {
			//申请权限, 它会回调 onRequestPermissionsResult
			ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
		}
	}
 
	// 申请权限的回调
	 override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode == PrintActivity.REQUEST_CODE_PERMISSIONS) {
            if (allPermissionsGranted()) {
	            initOnPermissionsGranted();//有权限了 继续
            } else {
                Toast.makeText(this, "未获得权限, 该功能不可用 0xff", Toast.LENGTH_LONG).show()
                finish();
            }
        }
    }
 
	// 判断是否有, REQUIRED_PERMISSIONS 列表的权限	
	private fun allPermissionsGranted(): Boolean {
        for (permission in PrintActivity.REQUIRED_PERMISSIONS) {
            if (ContextCompat.checkSelfPermission(
                    this,
                    permission
                ) != PackageManager.PERMISSION_GRANTED
            ) {
                return false
            }
        }
        return true
    }
	// kt 的静态常量 (用组合对象)
    companion object {
	    //Application specific request code to match with a result reported 
	    //to ActivityCompat.OnRequestPermissionsResultCallback.onRequestPermissionsResult(int, String[], int[]). Should be >= 0. 
        private const val REQUEST_CODE_PERMISSIONS = 101
        //The requested permissions. Must be non-null and not empty.
        private val REQUIRED_PERMISSIONS =//权限列表
            arrayOf(Manifest.permission.CAMERA, Manifest.permission.ACCESS_FINE_LOCATION)
    }
}

easypermissions 库

使用方式

val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
				arrayOf(Manifest.permission.FOREGROUND_SERVICE, Manifest.permission.POST_NOTIFICATIONS)
			} else {
				arrayOf(Manifest.permission.FOREGROUND_SERVICE)
			}
val request = PermissionRequest.Builder(this, 101, *permissions)
		.setRationale("授予前台通知的权限以尽量避免设备连接被系统回收?")
		.setPositiveButtonText("前往授权")
		.setNegativeButtonText("以后再说")
		.build()
// Context
if (EasyPermissions.hasPermissions(this, *permissions) ){
  //已有权限
}else{
	//请求, 这里请求会回调到请求者 Activity 的 onRequestPermissionsResult中
	EasyPermissions.requestPermissions( request);
}

回调过程

权限请求的结果会回调到你传递给 requestPermissions方法的 Activity或 Fragment中的 onRequestPermissionsResult方法。

所以,EasyPermissions 要求你在 Activity或 Fragment中重写 onRequestPermissionsResult方法,并在其中调用 EasyPermissions.onRequestPermissionsResult()方法,以便 EasyPermissions 能够处理结果并传递给你之前设置的 PermissionCallbacks接口的实现。

override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
	super.onRequestPermissionsResult(requestCode, permissions, grantResults)
	//将结果转发到 EasyPermissions; this 即需要 实现 PermissionCallbacks 
	EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this)
}

EasyPermissions 还会将结果路由到 pub.devrel.easypermissions.EasyPermissions.PermissionCallbacks接口的方法, 所以 Activity 实现此接口.

class WebActivity: AppCompatActivity(), EasyPermissions.PermissionCallbacks, WebContext {
 
 
	...........
 
 
   override fun onPermissionsGranted(requestCode: Int, perms: MutableList<String>) {
    }
    override fun onPermissionsDenied(requestCode: Int, perms: MutableList<String>) {
    }
    
}

常用权限

官方文档

<!-- 文件IO -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
 
<!-- 网络访问 -->
<uses-permission android:name="android.permission.INTERNET"/>
<!-- 读取帧缓存用于屏幕截图, 只有系统APP? -->
<uses-permission android:name="android.permission.READ_FRAME_BUFFER"/>
 
 
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="hongge.yang.com.scs">
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.INTERNET"/>
 
...
<manifest/>

Room ORM

https://developer.android.google.cn/jetpack/androidx/releases/room?hl=zh-cn https://www.cnblogs.com/renhui/p/10966560.html https://developer.android.google.cn/codelabs/basic-android-kotlin-training-intro-room-flow?hl=zh-cn#0

Primary components

There are three major components in Room:

  • The database class that holds the database and serves as the main access point for the underlying connection to your app’s persisted data.(数据库类,用于存储数据库并作为对应用程序持久化数据底层连接的主要访问点。)
  • Data entities that represent tables in your app’s database.(数据实体表示您应用程序数据库中的表。)
  • Data access objects (DAOs) that provide methods that your app can use to query, update, insert, and delete data in the database.(数据访问对象(DAOs),提供您的应用程序可以用来查询、更新、插入和删除数据库中的数据的方法。)

依赖

https://developer.android.google.cn/jetpack/androidx/releases/room?hl=zh-cn#kts

  • 顶级 build 添加
plugins {  
	id("com.android.application") version "8.2.0" apply false  
	id("org.jetbrains.kotlin.android") version "1.9.0" apply false  
 
	id("androidx.room") version "2.6.1" apply false
	//添加ksp
	id("com.google.devtools.ksp") version "1.9.20-1.0.13" apply false
 
}
 
plugins {  
id("com.android.application")  
id("org.jetbrains.kotlin.android")  
id("com.google.devtools.ksp")  //启用ksp
}
 
dependencies {
    val room_version = "2.6.1"
	// If this project only uses Java source, use the Java annotationProcessor
    // No additional plugins are necessary
    // 对于 java
    annotationProcessor("androidx.room:room-compiler:$room_version")
	// 对于 kotlin
	ksp("androidx.room:room-compiler:$room_version")
	implementation("androidx.room:room-paging:$room_version")// 分页支持
	implementation("androidx.room:room-ktx:$room_version")// 编译增强的库
	implementation("androidx.core:core-ktx:1.13.1")    // 编译增强的库
 
...
 

自定义类型转换

RoomDatabase 除了必须 添加@Database注解 也可以添加@TypeConverter注解。用于提供一个把自定义类转化为一个Room能够持久化的已知类型的,比如我们想持久化日期的实例,我们可以用如下代码写一个TypeConverter去存储相等的Unix时间戳在数据库中。

public class Converters {
 
    @TypeConverter
    public static Date fromTimestamp(Long value) {
        return value == null ? null : new Date(value);
    }
 
    @TypeConverter
    public static Long dateToTimestamp(Date date) {
        return date == null ? null : date.getTime();
    }
/*******
	// 转换 JsonElement 为字符串并且压缩 保存
	@TypeConverter
    fun jsonElementToString(json: JsonElement?): ByteArray? {
        if (json == null) return null
 
        return try {
            val jsonString = json.toString()
            ByteArrayOutputStream().use { bos ->
                GZIPOutputStream(bos).use { gzip ->
                    gzip.write(jsonString.toByteArray(Charsets.UTF_8))
                    gzip.finish() // 必须调用以确保数据完整
                }
                bos.toByteArray()
            }
        } catch (e: Exception) {
            throw IllegalArgumentException("JSON 压缩失败", e)
        }
    }
 
    @TypeConverter
    fun stringToJsonElement(blob: ByteArray?): JsonElement? {
        if (blob == null) return null
        return try {
            val decompressed = ByteArrayOutputStream().use { bos ->
                ByteArrayInputStream(blob).use { bis ->
                    GZIPInputStream(bis).use { gzip ->
                        gzip.copyTo(bos)
                    }
                }
                bos.toByteArray()
            }
 
            JsonParser.parseString(String(decompressed, Charsets.UTF_8))
        } catch (e: Exception) {
            throw IllegalArgumentException("Blob 解压失败", e)
        }
    }
*********/
}
@Database(entities = {User.class}, version = 1)
//注入转换类
@TypeConverters(Converters::class)
public abstract class AppDatabase extends RoomDatabase {
    public abstract UserDao userDao();
}

版本更新

@Entity
public class Book {
    @PrimaryKey(autoGenerate = true)
    private Long   uid;
    private String name;
    private Date   time;
    private Long   userId;
}
 
// version 指定当前版本
@Database(entities = {User.class, Book.class}, version = 3)
@TypeConverters({Converters.class})
public abstract class AppDatabase extends RoomDatabase {
    public abstract UserDao userDao();
    public abstract BookDao bookDao();
}
 
public class AppApplication extends Application {
 
    private AppDatabase mAppDatabase;
 
    @Override
    public void onCreate() {
        super.onCreate();
        // addMigrations 添加 升级规则
        mAppDatabase = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "android_room_dev.db")
                           .allowMainThreadQueries()
                           .addMigrations(MIGRATION_1_2, MIGRATION_2_3)
                           .build();
    }
 
    public AppDatabase getAppDatabase() {
        return mAppDatabase;
    }
 
 
/////////////// 更新规则
    /**
     * 数据库版本 1->2 user表格新增了age列
     */
    static final Migration MIGRATION_1_2 = new Migration(1, 2) {
        @Override
        public void migrate(SupportSQLiteDatabase database) {
            database.execSQL("ALTER TABLE User ADD COLUMN age integer");
        }
    };
 
    /**
     * 数据库版本 2->3 新增book表格
     */
    static final Migration MIGRATION_2_3 = new Migration(2, 3) {
        @Override
        public void migrate(SupportSQLiteDatabase database) {
            database.execSQL(
                "CREATE TABLE IF NOT EXISTS `book` (`uid` INTEGER PRIMARY KEY autoincrement, `name` TEXT , `userId` INTEGER, 'time' INTEGER)");
        }
    };
}

删除数据库

// 删除数据库文件
context.deleteDatabase("app_database")
// 重新创建数据库
val database = Room.databaseBuilder(
    context,
    AppDatabase::class.java, "app_database"
).build()

AAR 项目

AAR 是用于共享和重用代码的库项目,而不是独立运行的应用。因此,在设计 AAR 的 AndroidManifest.xml 时,要确保只包含与库功能相关的声明。

AAR 文件的文件扩展名为 .aar,文件本身是一个 zip 文件,以下内容是必须包含的:

  • /AndroidManifest.xml
  • /classes.jar
  • /res/
  • /R.txt

不包含第三方依赖

不包含aar项目的依赖, 主项目需要引入arr依赖的第三方库

AndroidManifest 配置文件

使用 AAR 的应用项目将继承这些声明,并在自己的 AndroidManifest.xml 中添加必要的配置。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.library">
 
    <!-- 库项目的 Application 信息,通常为空 -->
    <application>
        <!-- 库项目的组件声明,例如 Activities、Services、BroadcastReceivers 等 -->
        <activity android:name="com.example.library.LibraryActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
 
        <!-- 其他库项目组件声明... -->
    </application>
 
    <!-- 库项目可能需要的权限声明 -->
    <uses-permission android:name="android.permission.INTERNET" />
 
    <!-- 库项目的依赖库声明,例如支持某些功能所需的依赖库 -->
    <uses-library android:name="some.library" />
 
</manifest>
 

项目源码

美菱安卓端

Transclude of Application.zip

三普信息化

kotlin + java 源码

Transclude of spxxf_20250424.zip

java API 26 版源码(市场通兼容)

Transclude of spxxf_20240206.rar

水网信息化

Transclude of swxxf_20250424.zip

农机智驾

Transclude of njzj-app_20250424.zip

Android Studio

项目的主要目录

  • assets assets 文件夹: 原始资源文件夹, 对应着Android工程的assets文件夹, 一般用于存放原始的图片 txt css等资源文件.

  • res src/main/res 这个目录下的内容就有点多了.简单点说, 就是你在项目中使用到的所有图片, 布局, 字符串等资源都要存放在这个目录下. 当然这个目录下还有很多子目录, 图片放在drawable目录下, 布局放在layout目录下, 字符串放在values目录下

drawable开头的文件夹都是用来放图片的, 注意后缀是根据不同分辨率的手机读取资源的; 以mipmap开头的文件夹都是用来放应用图标的; 以values开头的文件夹都是用来放字符串 样式 颜色等配置的; layout文件夹是用来放布局文件的.

  • main/AndroidManifest.xml 整个Android项目的配置文件, 在程序中定义的所以四大组件都需要在这个文件里注册, 另外还可以在这个文件中给应用程序添加权限声明.

  • build.gradle 这是app模块的gradle构建脚本, 这个文件中会指定很多项目构建相关的配置.

lambda 支持

  1. 在project build 引入 classpath 'me.tatarka:gradle-retrolambda:3.2.0'
buildscript {
    repositories {
        google()
        jcenter()
        maven { url "https://repo.spring.io/release" }
        maven { url "https://repo.spring.io/milestone" }
        maven { url "https://repo.spring.io/snapshot" }
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.0.1'
        classpath 'me.tatarka:gradle-retrolambda:3.2.0'//lambda
        //implementation(files("libs\\LPAPI-R.jar")) //本地jar
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}
  1. 在module build 增加任务apply plugin: 'me.tatarka.retrolambda'(3.0之后版本 去掉这一步)
apply plugin: 'com.android.application'
apply plugin: 'me.tatarka.retrolambda'
 
  1. 在module build ‘android’指定版本 targetCompatibility 1.8
android {
    compileSdkVersion 27
    defaultConfig {
        applicationId "com.joinken.application"
        minSdkVersion 21
        targetSdkVersion 27
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
 
    compileOptions {
        targetCompatibility 1.8
        sourceCompatibility 1.8
    }
}
  1. rebuild

gradle

参考 N_Gradle.md

修改为本地 gradle

File -> Settings -> Builder, Excution, Dep... -> Gradle

Gradle user home

当我们在 setting 下 gradle下设置 gradle 选择 ‘use defalut gradle wrapper(recommended)’ 时, as就会根据{project.dir}\gradle\wrapper\gradle-wrapper.properties 文件中的配置去gradle

踩坑

不识别真机

主界面显示 No devices;

无外乎:

  1. 驱动. 注意电脑的设备管理器
  2. SDK版本, 一般studio会提示
  3. 考虑下 ADB 能不能看到设备.
  4. 以上不是? Android UI src 文件夹都不是源码图标, 明显IDE 没识别到项目

修改 \settings.gradle 删除文件内容 File -> Sync project with gradle Files , 在改回来 sync !