Android运行时权限 Runtime Permissions

运行时权限

Api23开始,Android权限机制更改,有一部分权限不再是简单的在AndroidManifest.xml中声明即可。而是需要在运行时让用户去选择是否允许该项权限的操作。

那么哪些权限需要在运行时申请呢?危险权限需要这么做,而普通权限仍然和以前一样。具体的分类可以看之前的文章Runtime Permissions

普通权限一半时不会威胁安全、隐私;而危险权限一般涉及用户隐私,设备安全问题。

AndroidManifest声明权限

无论是危险权限还是普通权限都必须在AndroidManifest.xml中声明,这一步的作用是什么?主要有两方面:

  1. 程序安装时告知用户需要的权限,由用户决定是否安装。
  2. 在设置的应用选项中,点击应用查看信息可以看到应用获取的权限。由用户决定是否保留应用。

在build.gradle(app)中targetSdkVersion的值低于23时,应用运行在Android6.0及以上系统时,会默认打开在AndroidManifest.xml声明的权限。

如果升级应用,修改了targetSdkVersion为23及以上,再次运行时,依旧默认允许所有AndroidManifest.xml中所用声明的权限。

危险权限导致Crash

如果在Android6.0及以上运行程序执行危险权限相关操作,没有运行时检查权限获取情况,如果没有获取会导致程序crash。例如Console输出:Caused by: java.lang.SecurityException: Permission Denial: starting Intent { act=android.intent.action.CALL dat=tel:xxxxxxxxxxxx cmp=com.android.server.telecom/.components.UserCallActivity } from ProcessRecord{cae02cf 30814:android.example.com.permissionusage/u0a154} (pid=30814, uid=10154) with revoked permission android.permission.CALL_PHONE

有时候需要对执行危险权限操作进行封装,例如打电话操作:

  1. Intent intent = new Intent(Intent.ACTION_CALL);
  2. intent.setData(Uri.parse(“tel://1234567890”));
  3. startActivity(intent);

直接写成一个方法编译器会报错,即便不去处理也可以运行。可以通过捕获上面Console输出的异常来解决编译器报错:

  1. private void makeCall() {
  2. try{
  3. Intent intent = new Intent(Intent.ACTION_CALL);
  4. intent.setData(Uri.parse(“tel://1234567890”));
  5. startActivity(intent);
  6. }catch (SecurityException e) {
  7. e.printStackTrace();
  8. }
  9. }

运行时权限基础写法

单个运行时权限申请

  1. /**
  2. * 单个权限授权
  3. * @param view
  4. */
  5. public void btnClick(View view) {
  6. if(ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE)
  7. != PackageManager.PERMISSION_GRANTED) {
  8. ActivityCompat.requestPermissions(
  9. this, new String[]{Manifest.permission.CALL_PHONE}, CALL_REQUEST);
  10. }else {
  11. makeCall();
  12. }
  13. }

权限申请回调

  1. public void onRequestPermissionsResult(
  2. int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
  3. super.onRequestPermissionsResult(requestCode, permissions, grantResults);
  4. switch(requestCode) {
  5. case CALL_REQUEST:
  6. if(grantResults.length > 0
  7. && grantResults[0] == PackageManager.PERMISSION_GRANTED){
  8. makeCall();
  9. }else {
  10. Snackbar.make(mContainer, “权限被拒*了”, Snackbar.LENGTH_SHORT).show();
  11. }
  12. break;
  13. default:
  14. break;
  15. }
  16. }

其实当grantResults数组长度为0时,程序某个地方一定出现问题。

多个运行时权限申请

  1. /**
  2. * 多个权限同时授权
  3. * @param v
  4. */
  5. public void btnMorePermissions(View v) {
  6. List<String> permissions = new ArrayList<>();
  7. //安全权限,无需运行时检查
  8. if(ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_NETWORK_STATE)
  9. != PackageManager.PERMISSION_GRANTED) {
  10. permissions.add(Manifest.permission.ACCESS_NETWORK_STATE);
  11. }
  12. if(ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE)
  13. != PackageManager.PERMISSION_GRANTED) {
  14. permissions.add(Manifest.permission.CALL_PHONE);
  15. }
  16. if(ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
  17. != PackageManager.PERMISSION_GRANTED) {
  18. permissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);
  19. }
  20. if(!permissions.isEmpty()) {
  21. ActivityCompat.requestPermissions(
  22. this,
  23. permissions.toArray(new String[permissions.size()]),
  24. MORE_PERMISSIONS_REQUEST);
  25. }else {
  26. doSomething();
  27. }
  28. }

权限申请回调

  1. public void onRequestPermissionsResult(
  2. int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
  3. super.onRequestPermissionsResult(requestCode, permissions, grantResults);
  4. switch(requestCode) {
  5. case MORE_PERMISSIONS_REQUEST:
  6. if(grantResults.length > 0) {
  7. for(int i : grantResults) {
  8. if(i != PackageManager.PERMISSION_GRANTED) {
  9. Snackbar.make(mContainer, “某个权限没有授权”, Snackbar.LENGTH_SHORT).show();
  10. return;
  11. }
  12. }
  13. doSomething();
  14. }else {
  15. }
  16. default:
  17. break;
  18. }
  19. }

为什么读写外部存储属于危险权限

在Android6.0以前,读写外部存储只需要在AndroidManifest.xml中声明即可,但是由于应用的强制行为,导致用户的外部存储中文件杂乱,权限被滥用。所以将其制定为危险权限。

在外部存储目录Android/data/packgae_name属于应用私有的目录,不需要运行时申请读写外部存储危险权限,甚至不需要在AndroidManifest.xml中声明就可以读写。应用可以随意支配自身的文件存储(cache目录经常会被清理软件清理,主要文件放入files目录下)。

这样既保证了外部存储目录的整洁,又不会给应用开发者带来不必要的麻烦。具体操作可以看文章Android数据存储之File总结。而且应用卸载后,相应包名的目录也会删除。

封装

由于运行时权限申请的繁琐,所以封装饰必须的。但是申请权限的操作必须建立在Activity之上。只有在Activity中才可以弹出申请权限对话框。而且申请回调函数属于Activity的方法,所以申请权限的操作和Activity藕合度非常高。可以通过以下办法:

  1. 自定义一个PermissionActivity,专门用于处理申请运行时权限操作。该Activity背景透明,用户无法察觉。执行完后finish掉。
  2. 参照RxPermissions第三方库的实现。
  3. 创建一个BaseActivity去实现运行时权限申请方法,然后所有Activity继承BaseActivity,需要时调用方法即可。BaseActivity对于一个项目可以提高Activity类的扩展性,在里面实现自己的方法供子类使用。

BaseActivity

  1. public class BaseActivity extends AppCompatActivity{
  2. private static final int REQUEST_CODE = 1;
  3. private PermissionListener mListener;
  4. public void requestRuntimePermissions(String[] permissions, PermissionListener listener) {
  5. mListener = listener;
  6. List<String> permissionList = new ArrayList<>();
  7. for(String permission : permissions) {
  8. if(ContextCompat.checkSelfPermission(this, permission)
  9. != PackageManager.PERMISSION_GRANTED) {
  10. permissionList.add(permission);
  11. }
  12. }
  13. if(!permissionList.isEmpty()) {
  14. ActivityCompat.requestPermissions(
  15. this,
  16. permissionList.toArray(new String[permissionList.size()]),
  17. REQUEST_CODE);
  18. }else {
  19. mListener.onGranted();
  20. }
  21. }
  22. @Override
  23. public void onRequestPermissionsResult(
  24. int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
  25. super.onRequestPermissionsResult(requestCode, permissions, grantResults);
  26. switch (requestCode) {
  27. case REQUEST_CODE:
  28. if(grantResults.length > 0) {
  29. List<String> deniedPermission = new ArrayList<>();
  30. for(int i = 0; i < grantResults.length; i++) {
  31. int grantResult = grantResults[i];
  32. if(grantResult == PackageManager.PERMISSION_DENIED) {
  33. deniedPermission.add(permissions[i]);
  34. }
  35. }
  36. if(deniedPermission.isEmpty()) {
  37. mListener.onGranted();
  38. }else {
  39. mListener.onDenied(deniedPermission);
  40. }
  41. }
  42. break;
  43. default:
  44. break;
  45. }
  46. }
  47. }

PermissionListener用于将申请结果返回给调用的Activity。让Activity去实现权限申请结果相应的操作。

  1. public interface PermissionListener {
  2. void onGranted();
  3. void onDenied(List<String> deniedPermissions);
  4. }

*后在Activity中使用:

  1. public class SecondActivity extends BaseActivity{
  2. private LinearLayout mContainer;
  3. @Override
  4. protected void onCreate(@Nullable Bundle savedInstanceState) {
  5. super.onCreate(savedInstanceState);
  6. setContentView(R.layout.activity_second);
  7. initView();
  8. }
  9. private void initView() {
  10. mContainer = (LinearLayout) findViewById(R.id.id_second_container);
  11. }
  12. public void btnRuntimePermission(View view) {
  13. requestRuntimePermissions(new String[]{
  14. Manifest.permission.CALL_PHONE,
  15. Manifest.permission.ACCESS_FINE_LOCATION,
  16. Manifest.permission.CAMERA,
  17. Manifest.permission.WRITE_EXTERNAL_STORAGE}, new PermissionListener() {
  18. @Override
  19. public void onGranted() {
  20. Snackbar.make(mContainer, “All Permissions Granted!”, Snackbar.LENGTH_SHORT).show();
  21. }
  22. @Override
  23. public void onDenied(List<String> deniedPermissions) {
  24. StringBuilder builder = new StringBuilder(32);
  25. int deniedCount = deniedPermissions.size();
  26. for(int i = 0; i < deniedCount; i++) {
  27. String[] strArray = deniedPermissions.get(i).split(“\\.”);
  28. builder.append(strArray[strArray.length – 1]);
  29. if(i == (deniedCount – 1)) {
  30. builder.append(“.”);
  31. }else {
  32. builder.append(“,”);
  33. }
  34. }
  35. Snackbar.make(
  36. mContainer,
  37. “Denied Permissions:” + builder.toString(),
  38. Snackbar.LENGTH_SHORT
  39. ).show();
  40. }
  41. });
  42. }
  43. }

iOS Universal Links 配置 – 收集了配置无效的问题

前面我们写了一篇iOS分发管理( iOS App托管和分发搭建)的文章,这一篇写一下分发后,如果用户已经安装了app需要调起app的需求

快门:地址初步检验、一篇很不错的文章

注意事项:

•    Your domain is valid (valid DNS)
•    Your file must be served over HTTPS
•    Your server shouldn’t return an error status code (>= 400)
•    Your file cannot be behind redirects
•    Your file must be served with content type “application/pkcs7-mime”
•    Your file should validate and return its contents with `openssl smime -verify -inform DER -noverify`
•    Your file should contain valid JSON (using simple JSON.parse). This can be tripped by things like having an extraneous NULL at the end of your string.
教程很多,我们先看注意事项,上面的重定向、页面报错我都有尝试,确实导致link不生效,把apple-app-site-association文件上传到某个域名指向的服务器中,如果我们测试的web页面会重定向、或者是个不存的页面,都无法正常测试你的结果。当然如果有微信登录的服务,可以配置微信的Universal links,然后调起微信试试就能验证结果。

 

下面简述一下Universal links的配置过程
截图和参考环境:Xcode Version 11.4 (11E146)

大致步骤:创建【apple-app-site-association】文件 >> 上传文件到自己的服务器 >> app内配置 >> 打包安装到手机(真机) >> 使用Safari验证结果 >> 内部逻辑

详细步骤:

1、填写apple-app-site-association文件,可以找个json文件,填写,写完后把.json后缀删除。

{
“applinks”: {
“apps”: [],
“details”: [
{
“appID”: “teamId.bundleid”,
“paths”: [ “*” ]
},
{
“appID”: “H7JOD8J4A1.bundleid”,
“paths”: [ “*” ]
}
]
}
}
我们只要配置上面的appID 和 paths 就可以了;
appID是由teamID和bundleID通过”.”链接在一起组成的,其中teamID在developer.apple.com的Membership选项下可查。但是!!,但是如果是子账号打包调试,teamID可能有变,在Signing & Capabilities中可以看到,具体见下图。我曾花费1天的时间,苦苦思考,*终发现问题是这个teamID不正确
paths的配置,建议前期简单配置,测试通过再去琢磨是否有必要丰富一下path
注意格式不要出错,一定是严格的json格式,建议在json.cn中编辑,然后贴过来。

%title插图%num
2、在服务器根目录新建.well-known目录,然后上传刚刚写好的文件,这样就可以通过https://yourHost/.well-known/apple-app-site-association来访问到刚才的文件,如果不能访问,则上传的有问题。

3、app内配置Associated Domains,见上图

前面要追加applinks: ​
不用写协议部分https://,直接写host部分就行了,
举例,假设我们现在配置的是得道app,注意格式即可,参数值只是示例:

apple-app-site-association文件中的teamID为 JDIU78976J.com.dedao.app,paths不变
上传到得到移动端的文件地址:https://m.dedao.com/.well-known/apple-app-site-association,并且可在浏览器访问到内容
在Xcode配置是:applinks:m.dedao.com
把3中配置后的app运行到真机里,或者打包安装也可以。
在4完成后的手机中,找到Safari,浏览器输入:https://m.dedao.com?id=8301280,这里假设
4、把app安装到app中,然后在Safari中打开一个部署在刚才的服务上的h5页面,加载完成后,微微下拉,即可看到打开app的提示。如果有提示,即代表配置成功。

5、如果需要内部跳转逻辑,可以实现以下代理方法,抓取到URL即可自行处理

swift:

func application(_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([Any]?) -> Void) -> Bool
OC:

– (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable restorableObjects))restorationHandler;
取userActivity.webpageURL即可。如果微信分享使用了 universal Links的方式通信,需要单独做个判断,具体如何判断,抓一个微信回调的URL即可发现规律。

iOS图片的加载优化 initWithContentsOfFile

有的小伙伴可能没那么在意图片的加载方式,习惯了imageNamed,看到initWithContentsOfFile时也没有仔细看他的作用。

1、首先我做了一个实验,比较了两种方式对内存的影响。
测试方法,对于app的引导页(5张图)分别使用imageName和initWithContentsOfFile去初始化图片

使用imageName加载图片
[UIImage imageNamed:@”launch_iphonex”];
启动之后,进入引导页,打开Xcode的内存监测,看到每滑动一次,内存增加一次,5张图下来,累计增加了20M+ 的内存。引导页过后,内存依然没被释放(imageFileNamed的特点)

大致计算一下增加的内存:比如iPhoneX的引导图1125 * 2436,加载出来大约是3M(这里我不会很会算,可参考)

内存变化如下

使用initWithContentsOfFile去初始化图片,内存依旧会增加,但是引导页过后,内存直线下降。
内存变化如下

这里看到效果很明显,对于我们不常用/只使用一次的图片资源,使用initWithContentsOfFile去加载图片是非常不错的选择,对于高要求的的开发者几乎是一定要优化的点。

2、下面简单介绍下二者的区别:
1)imageNamed

用这个方法加载的时候,它会在系统缓存中查找并返回一个对象,如果缓存中没有找到对应的对象,就根据文件名找到,然后创建image对象,再返回。在APP的整个生命周期内,加载过的图是一直存在内存中的,内存就会持续累加。但是对于一些文件很小,我们又经常用到的小图,很实用。比如tableView中的一个箭头,在多行都会展示,这时候使用imageNamed效率是很高的。

2)initWithContentsOfFile,加载图片时,image对象的生命周期与imageView绑定,会随着imageView释放而释放掉。对于一些不常用的且大的图片,是很好的选择。有些同学习惯用asset去存图片,自动分配2倍3倍图,如果使用这个方法就不好用了,建议直接搞一张3倍图拖入某个特定的文件夹。还要注意的是,这时候自动布局的话,记得写全布局属性,他这会儿已经不会自动调整了。引导图、某一页面特殊的背景图等等都可以使用这种方式。

 

initWithContentsOfFile编码(这里只兼容了png,可以开放更多参数)

+ (UIImage *)imageFileNamed:(NSString *)imageName {

NSString *filePath = [NSString stringWithFormat:@”%@/%@.png”,[[NSBundle mainBundle] resourcePath],imageName];
UIImage *image = [[UIImage alloc] initWithContentsOfFile:filePath];

return image;
}
OK提高我们的标准,让你的APP运行的更流畅。

Android-代码中获取Logcat打印日志并存放于文件中

1.Logcat命令的使用

  1. logcat -c 清除屏幕显示
  2. logcat -d 显示
  3. logcat 显示日志
  4. log logcat -f filename将日志输出到一个文件中
  5. logcat -v time 显示时间
  6. logcat -v time -s tag:priority 这里会输出与priority相等或者优先级比priority优先级高的

priority有5种:

  1. V — Verbose (优先级*低)
  2. D — Debug
  3. I — Info
  4. W — Warning
  5. E — Error
  6. F — Fatal
  7. S — Silent

比如我们在串口输入logcat -v time -s ScreenSaver:D
会打印出

01-02 10:30:05.690 I/ScreenSaver(  957): screen now time = 8934wait time :480

Screensaver是tag,因为I的优先级比D高,因此也会输出I的(Log.i());

2.如何在代码中运行命令行

这需要用到

    Runtime.getRunntime.exec();

exec的参数一般为String [];
比如我们要抓取日志带时间,我们的代码可以这样写

Runtime.getRuntime.exec(new String[]{"logcat","-v","time","-s","ScreenSaver:D"})

通过exec直接运行”logcat -v time -s ScreenSaver:D”,是不行的。
上面Runntime.getRunntime.exec会返回Process对象,Process有6种方法可以调用:

1.destroy():杀掉子进程
2.exitValue():返回子进程的出口值,值 0 表示正常终止
3.getErrorStream():获取子进程的错误流
4.getInputStream():获取子进程的输入流
5.getOutputStream():获取子进程的输出流
6.waitFor():导致当前线程等待,如有必要,一直要等到由该 Process对象表示的进程已经终止。如果已终止该子进程,此方法立即返回。如果没有终止该子进程,调用的线程将被阻塞,直到退出子进程,根据惯例,0表示正常终止

这里我们主要使用getInputStream()来获取打印日志的输入流

3.具体代码参考

这里我只截取一部分代码作为参考

  1. private FileOutputStream fos;
  2. /*核心代码*/
  3. while(captureLogThreadOpen){
  4. /*
  5. try {
  6. Thread.sleep(100);
  7. } catch (InterruptedException e) {
  8. // TODO Auto-generated catch block
  9. e.printStackTrace();
  10. }
  11. */
  12. try{
  13. /*命令的准备*/
  14. ArrayList<String> getLog = new ArrayList<String>();
  15. getLog.add(“logcat”);
  16. getLog.add(“-d”);
  17. getLog.add(“-v”);
  18. getLog.add(“time”);
  19. ArrayList<String> clearLog = new ArrayList<String>();
  20. clearLog.add(“logcat”);
  21. clearLog.add(“-c”);
  22. Process process = Runtime.getRuntime().exec(getLog.toArray(new String[getLog.size()]));//抓取当前的缓存日志
  23. BufferedReader buffRead = new BufferedReader(new InputStreamReader(process.getInputStream()));//获取输入流
  24. Runtime.getRuntime().exec(clearLog.toArray(new String[clearLog.size()]));//清除是为了下次抓取不会从头抓取
  25. String str = null;
  26. logFile = new File(logPath+“log.txt”);//打开文件
  27. fos = new FileOutputStream(logFile,true);//true表示在写的时候在文件末尾追加
  28. String newline = System.getProperty(“line.separator”);//换行的字符串
  29. //Date date = new Date(System.currentTimeMillis());
  30. //String time = format.format(date);
  31. //Log.i(TAG, “thread”);
  32. while((str=buffRead.readLine())!=null){//循环读取每一行
  33. //Runtime.getRuntime().exec(clearLog.toArray(new String[clearLog.size()]));
  34. //Log.i(TAG, str);
  35. SimpleDateFormat format = new SimpleDateFormat(“yyyy-“);
  36. Date date = new Date(System.currentTimeMillis());
  37. String time = format.format(date);
  38. fos.write((time+str).getBytes());//加上年
  39. fos.write(newline.getBytes());//换行
  40. logCount++;
  41. if(logCount>10000){//大于10000行就退出
  42. captureLogThreadOpen = false;
  43. captureLogThread = null;
  44. fos.close();
  45. break;
  46. }
  47. }
  48. fos.close();
  49. fos = null;
  50. Runtime.getRuntime().exec(clearLog.toArray(new String[clearLog.size()]));
  51. }catch(Exception e){
  52. }
  53. }

 

我们通过str=buffRead.readLine()来读取日志的每一行数据,再通过fos.write((time+str).getBytes());写入到文件中。

4.注意事项

目前android中想要抓取日志,只允许获取系统及以上权限的apk才能进行这个操作,否则只能获得本app的logcat,因此,在抓取日志之前,*好使用签名工具对app进行签名获取系统权限

iOS App托管和分发搭建

我们基本都用过蒲公英或者fir.im做应用分发,但是可有想过他们是怎么实现的吗
如果想自己包里内测包,内部分发,或者企业包的分发,可以参考

自建和三方的优劣对比
产品

优点

缺点

1 自建 稳定
UI和功能可自定义
没有使用限制
方便内测运营 需要开发和维护,投入较大
2 三方
方便快捷,无需开发
功能完备(

版本管理,应用管理
应用合并
开放API
成员管理、统计等
)

有下载次数限制,一般为100次/天,且多款app共享该下载次数
可能需要安装密码,一些场景中不方便使用密码
可能不稳定,偶尔会挂掉
未加壳、加固的内测包上传到第三方平台,被反编译的风险增加
不方便运营,如app调起
用过的三方托管平台:

蒲公英(https://www.pgyer.com)
fir.im(https://www.betaqr.com/apps)
iOS包管理方案
概述
流程步骤:

企业包:【打包】-【配置manifest】-【上传】-【开发前端下载页面】-【下载】
内测包:【添加测试机】-【打包】-【配置manifest】-【上传】-【开发前端下载页面】-【下载】
背景原理:

Apple为了保护ipa包的安全性,不开放直接安装ipa的入口,由Safari的来触发系统命令,增加了直接拿到ipa包的难度(反变异ipa包后,开发信息泄漏的风险就很大)
开发者把ipa的下载地址写入manifest文件,然后把manifest上传到云,这样就可以通过云地址来获取app的基本信息:包地址、icon大、icon小
由Safari的命令(itms-services://?action=download-manifest&url=manifest的地址)来触发内部动作,拉取manifest,读取下载地址,然后下载ipa包,并安装到手机内
用户通过前端页面,如点击按钮,由程序执行上面操作,那么这个安装的过程就被封装起来了,用户没有看到ipa包,只要点击一安装并确定就可以了。
当然了,如果通过手段,分析前端请求和代码,抓取到manifest的云地址,就可以看到ipa的下载地址,这样就拿到了ipa包,只不过是这个过程就比直接提供ipa要复杂很多,破解成本提高了很多。
具体做法
1、准备云存储服务(七牛、阿里),打开上传文件功能,有自己的api上传也可以。

2、打包勾选Additional Options,并填写下载地址、图片地址。

一般来说,先上传图到云,拿到图片地址,然后修改文件名部分就行了,下次上传还使用同样的文件名就 OK 了

配图:

%title插图%num

%title插图%num

 

3、上传ipa到配置好的地址,上传由manifest.plist文件

这里有个tips,第二步骤就是的配置就是为了打包后多生成一个manifest.plist文件,如果你很熟练,可以跳过配置,直接打包,然后自己去修改manifest文件,再上传到云,效果是一样的。具体manifest是什么样的,自己打包生成一个看下。特别是自己开发包管理的后台管理功能时,应该熟练使用manifest文件的编辑。

4、在Safari中安装app

设manifest地址为url,则可在Safari中输入itms-services://?action=download-manifest&url=url,然后会提示是否下载,确定即可下载。这里需要主要的是,如果url中有特殊字符,需要编码。在Safari中输入的链接仅仅是测试你的配置做法是否正确,真正用的时候需要前端做个叶页面,来执行我们输入的地址命令,这样会更加友好

5、企业包和内侧包

如果打企业包,在开通企业账号的前提下,打包时选择Enterprise的方式即可

如果打内测包,需要提前把测试机的UDID倒入公司的测试名单内,倒入方法这里就不啰嗦了,打包时选择Ad Hoc即可。

 

根据企业的需要,如果大范围内测,或者安全性要求比较高,或者对下载页面要求比较高,且有一定的人力资源,那么可以自己开发;如果只是内部使用,可以做的简单一点;如果人力紧张,且只是内部测试时使用,而且安全要求不是很高(代码中没什么可泄漏的)就别瞎折腾了,还是集中精力做业务吧。

iOS Universal Links 配置 – 收集了配置无效的问题

前面我们写了一篇iOS分发管理( iOS App托管和分发搭建)的文章,这一篇写一下分发后,如果用户已经安装了app需要调起app的需求

快门:地址初步检验、一篇很不错的文章

注意事项:

•    Your domain is valid (valid DNS)
•    Your file must be served over HTTPS
•    Your server shouldn’t return an error status code (>= 400)
•    Your file cannot be behind redirects
•    Your file must be served with content type “application/pkcs7-mime”
•    Your file should validate and return its contents with `openssl smime -verify -inform DER -noverify`
•    Your file should contain valid JSON (using simple JSON.parse). This can be tripped by things like having an extraneous NULL at the end of your string.
教程很多,我们先看注意事项,上面的重定向、页面报错我都有尝试,确实导致link不生效,把apple-app-site-association文件上传到某个域名指向的服务器中,如果我们测试的web页面会重定向、或者是个不存的页面,都无法正常测试你的结果。当然如果有微信登录的服务,可以配置微信的Universal links,然后调起微信试试就能验证结果。

 

下面简述一下Universal links的配置过程
截图和参考环境:Xcode Version 11.4 (11E146)

大致步骤:创建【apple-app-site-association】文件 >> 上传文件到自己的服务器 >> app内配置 >> 打包安装到手机(真机) >> 使用Safari验证结果 >> 内部逻辑

详细步骤:

1、填写apple-app-site-association文件,可以找个json文件,填写,写完后把.json后缀删除。

{
“applinks”: {
“apps”: [],
“details”: [
{
“appID”: “teamId.bundleid”,
“paths”: [ “*” ]
},
{
“appID”: “H7JOD8J4A1.bundleid”,
“paths”: [ “*” ]
}
]
}
}
我们只要配置上面的appID 和 paths 就可以了;
appID是由teamID和bundleID通过”.”链接在一起组成的,其中teamID在developer.apple.com的Membership选项下可查。但是!!,但是如果是子账号打包调试,teamID可能有变,在Signing & Capabilities中可以看到,具体见下图。我曾花费1天的时间,苦苦思考,*终发现问题是这个teamID不正确
paths的配置,建议前期简单配置,测试通过再去琢磨是否有必要丰富一下path
注意格式不要出错,一定是严格的json格式,建议在json.cn中编辑,然后贴过来。
2、在服务器根目录新建.well-known目录,然后上传刚刚写好的文件,这样就可以通过https://yourHost/.well-known/apple-app-site-association来访问到刚才的文件,如果不能访问,则上传的有问题。

3、app内配置Associated Domains,见上图

前面要追加applinks: ​
不用写协议部分https://,直接写host部分就行了,
举例,假设我们现在配置的是得道app,注意格式即可,参数值只是示例:

apple-app-site-association文件中的teamID为 JDIU78976J.com.dedao.app,paths不变
上传到得到移动端的文件地址:https://m.dedao.com/.well-known/apple-app-site-association,并且可在浏览器访问到内容
在Xcode配置是:applinks:m.dedao.com
把3中配置后的app运行到真机里,或者打包安装也可以。
在4完成后的手机中,找到Safari,浏览器输入:https://m.dedao.com?id=8301280,这里假设
4、把app安装到app中,然后在Safari中打开一个部署在刚才的服务上的h5页面,加载完成后,微微下拉,即可看到打开app的提示。如果有提示,即代表配置成功。

5、如果需要内部跳转逻辑,可以实现以下代理方法,抓取到URL即可自行处理

swift:

func application(_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([Any]?) -> Void) -> Bool
OC:

– (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable restorableObjects))restorationHandler;
取userActivity.webpageURL即可。如果微信分享使用了 universal Links的方式通信,需要单独做个判断,具体如何判断,抓一个微信回调的URL即可发现规律。

iOS刘海屏适配,iPhoneX、iPhone12系列导航栏高度,刘海平适配

NAVIHEIGHT:(iPhoneX ? 88 : 64)、 TABBARHEIGHT: (iPhoneX ? 83 : 49)

新的iPhone、iPhone12发布后,我*件想做的事就是如何适配新iPhone,新的iPhone都是刘海屏幕

先看下各个尺寸

%title插图%num
各版本iPhone的尺寸及分辨率

%title插图%num

于是我就做了下面的宏定义(ScreenHeight就省略了):

#define iPhoneX :(ScreenHeight == 812.0f || ScreenHeight == 896.0f || ScreenHeight == 844.0f || ScreenHeight == 926.0f)

#define AdaptNaviHeight      (iPhoneX ? 24 : 0) //状态栏高度

#define AdaptTabHeight       (iPhoneX ? 34 : 0) //Tab bar 圆角部分高度

#define NAVIHEIGHT           (iPhoneX ? 88 : 64) //导航

#define TABBARHEIGHT         (iPhoneX ? 83 : 49) // 分栏

除了上面的方法外,官方的iPhone型号对照已更新,我们可以根据对照表,找到具体的手机型号。

具体方法及枚举如下

+ (NSString *)getDeviceSystemName {
static dispatch_once_t one;
static NSString *name;
dispatch_once(&one, ^{
NSString *model = [[UIDevice currentDevice] machineModel];
if (!model) return;
NSDictionary *dic = @{
@”iPhone7,2″ : @”iPhone 6″,
@”iPhone7,1″ : @”iPhone 6 Plus”,
@”iPhone8,1″ : @”iPhone 6s”,
@”iPhone8,2″ : @”iPhone 6s Plus”,
@”iPhone8,4″ : @”iPhone SE”,
@”iPhone9,1″ : @”iPhone 7″,
@”iPhone9,2″ : @”iPhone 7 Plus”,
@”iPhone9,3″ : @”iPhone 7″,
@”iPhone9,4″ : @”iPhone 7 Plus”,
@”iPhone10,1″ : @”iPhone 8″,
@”iPhone10,4″ : @”iPhone 8″,
@”iPhone10,2″ : @”iPhone 8 Plus”,
@”iPhone10,5″ : @”iPhone 8 Plus”,
@”iPhone10,3″ : @”iPhone X”,
@”iPhone10,6″ : @”iPhone X”,
@”iPhone11,2″ : @”iPhone XS”,
@”iPhone11,4″ : @”iPhone XS Max”,
@”iPhone11,6″ : @”iPhone XS Max”,
@”iPhone11,8″ : @”iPhone XR”,
};
name = dic[model];
});
return name;

}
tips:

1、关于每个控制器的适配,我采用了继承的方式,并且自定义了导航栏,所以导航栏的高度我在基控制器就做了控制,子控制器直接取自定义的导航,就可以做页面适配了。

2、关于自定义导航上的控件,每个按钮和标题都以导航的底部为参考,这样导航高度变了,也不会导致导航上控件的位置错误。

iOS – iPhone手机刘海屏判断

前言
*近写毕业设计的时候,发现 iPhoneX 之后的刘海屏手机顶部状态栏高度和底部TabBar高度和原来不一样了,这就需要我们对刘海屏手机做单独的 UI 布局适配了。

刘海屏判断
适配的核心是要对刘海屏进行判断,以下针对刘海屏手机的特征,提供了两种判断方法。

1. 安全区底部边距判断法
在 iOS 11 之后,多了安全区(下图蓝色区域)的概念。刘海屏手机因为多了下方的小黑条,底部安全区存在距离屏幕底部的边距,而且这是非刘海屏所不具有的。

%title插图%num

因此,我们可以将其作为判断刘海屏的依据。如果系统大于 iOS 11 且安全区底部到屏幕底部存在间距,就将其判断为刘海屏。代码如下:

// 刘海屏判断
#define iPhoneX ({ \
BOOL iPhoneX = NO; \
if (@available(iOS 11.0, *)) { \
if ([UIApplication sharedApplication].windows[0].safeAreaInsets.bottom > 0) { \
iPhoneX = YES; \
} \
} \
iPhoneX; \
})

2. 屏幕宽高比判断法
考虑到我不太喜欢写这种多行宏定义,据说会降低编译速度。我又继续寻找其他刘海屏的特征,终于在下面这一张图中找到了灵感。

%title插图%num

由上图所示,但凡是刘海屏手机,屏幕的纵横比都是 19 : 9。

设手机屏幕宽度为W i d t h WidthWidth,高度为 H e i g h t HeightHeight。根据屏幕纵横比规律,它只要满足以下两个条件其中一个,我们就能判它是刘海屏。
∣ H e i g h t W i d t h − 812 375 ∣ < 0.01 |\frac{Height}{Width} – \frac{812}{375}| < 0.01

Width
Height


375
812

∣<0.01

∣ H e i g h t W i d t h − 896 414 ∣ < 0.01 |\frac{Height}{Width} – \frac{896}{414}| < 0.01

Width
Height


414
896

∣<0.01

代码如下:

// 刘海屏判断
#define iPhoneX (ABS(MAX(CGRectGetWidth([UIScreen mainScreen].bounds), CGRectGetHeight([UIScreen mainScreen].bounds)) / MIN(CGRectGetWidth([UIScreen mainScreen].bounds), CGRectGetHeight([UIScreen mainScreen].bounds)) – 896 / 414.0) < 0.01 || ABS(MAX(CGRectGetWidth([UIScreen mainScreen].bounds), CGRectGetHeight([UIScreen mainScreen].bounds)) / MIN(CGRectGetWidth([UIScreen mainScreen].bounds), CGRectGetHeight([UIScreen mainScreen].bounds)) – 812 / 375.0) < 0.01)
1
2
PS:用小于 0.01 是为了避免精度问题,以及 iPhone 12 系列和前面刘海屏手机的纵横比存在一定误差。

相关常用宏
既然有了刘海屏的判断, 后面的工作就变得简单起来了。下面是我在编写相关代码的时候所写的宏,有需要的同学可以参考一下。

// 屏幕宽度
#define ScreenWidth [UIScreen mainScreen].bounds.size.width

// 屏幕高度
#define ScreenHeight [UIScreen mainScreen].bounds.size.height

// 状态栏高度
#define StatusBar_Height (iPhoneX ? 44.0f : 20.0f)

// 导航栏高度
#define NaviBar_Height (44.0f)

// 状态栏+导航栏高度
#define StatusBar_NaviBar_Height (StatusBar_Height + NaviBar_Height)

// TabBar高度
#define TabBar_Height (iPhoneX ? 49.0f + 34.0f : 49.0f)

Gradle工具的学习

Gradle 是一个工具,同时它也是一个编程框架。前面也提到过,使用这个工具可以完成app的编译打包等工作。当然你也可以用它干其他的事情。

相关 Gradle的官网:http://gradle.org/

https://docs.gradle.org/current/dsl/

https://docs.gradle.org/current/javadoc/org/gradle/api/Project.html

  • settings.gradle settings.gradle除了可以include外,还可以设置一些函数。这些函数会在gradle构建整个工程任务的时候执行,所以,可以在settings做一些初始化的工作。比如:我的settings.gradle的内容:

    <pre class=”hljs groovy” data-original-code=”” 定义一个名为initminshenggradleenvironment的函数。该函数内部完成一些初始化操作=”” 比如创建特定的目录,设置特定的参数等”=”” data-snippet-id=”ext.da31982bafbbbb24ed75235f36fda35b” data-snippet-saved=”false” data-codota-status=”done”>

    1. //定义一个名为initMinshengGradleEnvironment的函数。该函数内部完成一些初始化操作//比如创建特定的目录,设置特定的参数等
    2. def initMinshengGradleEnvironment(){
    3. println“initialize Minsheng Gradle Environment …..”
    4. ……//干一些special的私活….
    5. println“initialize Minsheng Gradle Environment completes…”
    6. }
    7. //settings.gradle加载的时候,会执行initMinshengGradleEnvironment
    8. initMinshengGradleEnvironment()
    9. //include也是一个函数:
    10. include ‘CPosSystemSdk’ , ‘CPosDeviceSdk’ , ‘CPosSdkDemo’,‘CPosDeviceServerApk’,‘CPosSystemSdkWizarPosImpl’

gradle命令介绍

  1. gradle projects查看工程信息
  2. gradle tasks查看任务信息
  3. gradle task-name执行任务
    列出了好多任务,这时候就可以通过 gradle 任务名来执行某个任务。这和make xxx很像。比如:
    gradle clean是执行清理任务,和make clean类似。
    gradle properites用来查看所有属性信息。
    gradle tasks会列出每个任务的描述,通过描述,我们大概能知道这些任务是干什么的…..。然后gradle task-name执行它就好。
    这里要强调一点:Task和Task之间往往是有关系的,这就是所谓的依赖关系。比如,assemble task就依赖其他task先执行,assemble才能完成*终的输出

Gradle基本类型

gradle基于groovy, gradle提供的基本类型也都实现了script.并且提供了大量的方法和属性

Build script -> Project (每个project的build.gradle也就是一个project)
Init script -> Gradle
Settings script -> Settings (对应setting.gradle)

  • Project 每个project的build.gradle都会转换成一个project
  • Gradle
  • Settings 每一个settings.gradle都会转换成一个Settings对象

Gradle 生命周期

1.初始化创建Settings实例
2.解析settings.gradle 构造各个Project实例
3.解析每个Project对应的build.gradle,配置相应Project

即 setting.gradle -> 各个build.gradle

%title插图%num

Paste_Image.png

%title插图%num

Paste_Image.png

上面*张图是settings.gradle的配置
第二张是执行assemble 打印出来的执行顺序,也就是说settings.gradle先执行
然后是root.gradle (也就是我们根目录下的gradle) 接下来就是按照settings.gradle配置顺序进行执行~

每个对象的方法就不再赘述,api中都有。

实战

root gradle

  1. // Top-level build file where you can add configuration options common to all sub-projects/modules.
  2. buildscript {
  3. repositories {
  4. mavenCentral()
  5. }
  6. dependencies {
  7. classpath ‘com.android.tools.build:gradle:1.2.3’
  8. }
  9. }
  10. allprojects {//对所有projects都起作用
  11. repositories {
  12. mavenCentral()
  13. }
  14. }
  15. ext {//定义所有project公用参数 使用rootProject.ext.XX就能拿到相应对象
  16. compileSdk = 23
  17. minSdk = 11
  18. targetSdk = 23
  19. support = “23.1.1”
  20. buildTools = “23.0.2”
  21. glide = “3.6.0”
  22. okio = “1.4.0”
  23. okhttp = “2.4.0”
  24. fabric = “2.4.0”
  25. leakcanary = “1.3.1”
  26. logansquare = “1.1.0”
  27. dagger = “2.0.1”
  28. packageName = “com.evilsoulm.keep_nice”
  29. butterknife = “7.0.1”
  30. retrofit = “2.0.0-beta2”
  31. greendao = “2.0.0”
  32. }
  33. task clean(type: Delete) {
  34. delete rootProject.buildDir
  35. }

commonandroid.gradle android项目共用gradle 相当于基类 可以通过apply from 引入

  1. android {
  2. compileSdkVersion rootProject.ext.compileSdk
  3. buildToolsVersion rootProject.ext.buildTools
  4. defaultConfig {
  5. minSdkVersion rootProject.ext.minSdk
  6. targetSdkVersion rootProject.ext.targetSdk
  7. }
  8. compileOptions {
  9. sourceCompatibility JavaVersion.VERSION_1_7
  10. targetCompatibility JavaVersion.VERSION_1_7
  11. }
  12. dexOptions {
  13. javaMaxHeapSize “2048m”
  14. }
  15. dexOptions {
  16. preDexLibraries project.hasProperty(‘debug’)
  17. incremental project.hasProperty(‘debug’)
  18. }
  19. packagingOptions {
  20. exclude ‘.readme’
  21. exclude ‘LICENSE.txt’
  22. exclude ‘META-INF/DEPENDENCIES’
  23. exclude ‘META-INF/dependencies’
  24. exclude ‘META-INF/DEPENDENCIES.txt’
  25. exclude ‘META-INF/dependencies.txt’
  26. }
  27. }

commonproject.gradle project项目共用gradle

  1. repositories {
  2. mavenCentral()
  3. }
  4. apply plugin: ‘findbugs’
  5. apply plugin: ‘checkstyle’
  1. apply plugin: ‘com.android.application’
  2. apply plugin: ‘com.neenbedankt.android-apt’
  3. apply from: rootProject.file(‘basegradle/commonProject.gradle’)//引入commonProject
  4. apply from: rootProject.file(‘basegradle/commonAndroid.gradle’)//引入commonAndroid
  5. buildscript {
  6. repositories {
  7. mavenCentral()
  8. }
  9. dependencies {
  10. classpath ‘com.neenbedankt.gradle.plugins:android-apt:1.8’
  11. }
  12. }
  13. android {
  14. defaultConfig {
  15. applicationId rootProject.ext.packageName
  16. versionCode 1
  17. versionName “1.0”
  18. }
  19. buildTypes {
  20. debug {
  21. minifyEnabled false
  22. debuggable true
  23. jniDebuggable true
  24. }
  25. release {
  26. minifyEnabled true
  27. shrinkResources false
  28. debuggable false
  29. jniDebuggable false
  30. zipAlignEnabled true
  31. proguardFiles getDefaultProguardFile(‘proguard-android.txt’), ‘proguard-rules.pro’
  32. }
  33. }
  34. configurations {
  35. all*.exclude group: ‘com.google.android’, module: ‘android’
  36. all*.exclude group: ‘com.google.android’, module: ‘support-v4’
  37. all*.exclude group: ‘asm’, module: ‘asm’
  38. all*.exclude group: ‘com.google.code.gson’
  39. }
  40. productFlavors {//很棒的一个参数可以在底下更改包名,资源等。。
  41. normal {
  42. }
  43. ddebug {
  44. }
  45. }
  46. }
  47. dependencies {
  48. compile project(‘:common’)
  49. compile project(‘:model’)
  50. compile fileTree(dir: ‘libs’, include: [‘*.jar’])
  51. compile ‘com.android.support:appcompat-v7:’ + rootProject.ext.support
  52. compile ‘com.android.support:design:’ + rootProject.ext.support
  53. compile ‘com.squareup.okio:okio:’ + rootProject.ext.okio
  54. compile(‘com.squareup.okhttp:okhttp:’ + rootProject.ext.okhttp) {
  55. exclude group: ‘com.squareup.okio’, module: ‘okio’
  56. }
  57. //图片加载
  58. compile ‘com.github.bumptech.glide:glide:’ + rootProject.ext.glide
  59. //leakcanary
  60. debugCompile ‘com.squareup.leakcanary:leakcanary-android:’ + rootProject.ext.leakcanary
  61. releaseCompile ‘com.squareup.leakcanary:leakcanary-android-no-op:’ + rootProject.ext.leakcanary
  62. //logansquare
  63. apt ‘com.bluelinelabs:logansquare-compiler:’ + rootProject.ext.logansquare
  64. compile ‘com.bluelinelabs:logansquare:’ + rootProject.ext.logansquare
  65. //dagger2 注入
  66. apt ‘com.google.dagger:dagger-compiler:’ + rootProject.ext.dagger
  67. compile ‘com.google.dagger:dagger:’ + rootProject.ext.dagger
  68. //UI注入
  69. compile ‘com.jakewharton:butterknife:’ + rootProject.ext.butterknife
  70. //rxjava
  71. compile ‘io.reactivex:rxjava:1.0.10’
  72. compile ‘io.reactivex:rxandroid:0.24.0’
  73. //网络框架
  74. compile ‘com.squareup.retrofit:retrofit:’ + rootProject.ext.retrofit
  75. compile ‘com.squareup.retrofit:converter-gson:’ + rootProject.ext.retrofit
  76. compile ‘com.squareup.retrofit:adapter-rxjava:’ + rootProject.ext.retrofit
  77. }

写法2

另一种写法就是只有一个root.build 通过configure进行每个module的配置 ,每个module下的build.gradle就可以删除了~统一在root.build下进行管理
eg.只写了一个module

  1. buildscript {
  2. repositories {
  3. mavenCentral()
  4. }
  5. dependencies {
  6. classpath ‘com.android.tools.build:gradle:1.2.3’
  7. }
  8. }
  9. allprojects {
  10. repositories {
  11. mavenCentral()
  12. }
  13. }
  14. ext {
  15. compileSdk = 23
  16. minSdk = 11
  17. targetSdk = 23
  18. support = “23.1.1”
  19. buildTools = “23.0.2”
  20. glide = “3.6.0”
  21. okio = “1.4.0”
  22. okhttp = “2.4.0”
  23. fabric = “2.4.0”
  24. leakcanary = “1.3.1”
  25. logansquare = “1.1.0”
  26. dagger = “2.0.1”
  27. packageName = “com.evilsoulm.keep_nice”
  28. butterknife = “7.0.1”
  29. retrofit = “2.0.0-beta2”
  30. greendao = “2.0.0”
  31. }
  32. configure(project(‘:app’).subprojects) {//相当于放在app下的build.gradle
  33. apply plugin: ‘com.android.application’
  34. apply plugin: ‘com.neenbedankt.android-apt’
  35. apply from: rootProject.file(‘basegradle/commonProject.gradle’)
  36. apply from: rootProject.file(‘basegradle/commonAndroid.gradle’)
  37. buildscript {
  38. repositories {
  39. mavenCentral()
  40. }
  41. dependencies {
  42. classpath ‘com.neenbedankt.gradle.plugins:android-apt:1.8’
  43. }
  44. }
  45. android {
  46. defaultConfig {
  47. applicationId rootProject.ext.packageName
  48. versionCode 1
  49. versionName “1.0”
  50. }
  51. buildTypes {
  52. debug {
  53. minifyEnabled false
  54. debuggable true
  55. jniDebuggable true
  56. }
  57. release {
  58. minifyEnabled true
  59. shrinkResources false
  60. debuggable false
  61. jniDebuggable false
  62. zipAlignEnabled true
  63. proguardFiles getDefaultProguardFile(‘proguard-android.txt’), ‘proguard-rules.pro’
  64. }
  65. }
  66. configurations {
  67. all*.exclude group: ‘com.google.android’, module: ‘android’
  68. all*.exclude group: ‘com.google.android’, module: ‘support-v4’
  69. all*.exclude group: ‘asm’, module: ‘asm’
  70. all*.exclude group: ‘com.google.code.gson’
  71. }
  72. productFlavors {
  73. normal {
  74. }
  75. ddebug {
  76. }
  77. }
  78. }
  79. dependencies {
  80. compile project(‘:common’)
  81. compile project(‘:model’)
  82. compile fileTree(dir: ‘libs’, include: [‘*.jar’])
  83. compile ‘com.android.support:appcompat-v7:’ + rootProject.ext.support
  84. compile ‘com.android.support:design:’ + rootProject.ext.support
  85. compile ‘com.squareup.okio:okio:’ + rootProject.ext.okio
  86. compile(‘com.squareup.okhttp:okhttp:’ + rootProject.ext.okhttp) {
  87. exclude group: ‘com.squareup.okio’, module: ‘okio’
  88. }
  89. //图片加载
  90. compile ‘com.github.bumptech.glide:glide:’ + rootProject.ext.glide
  91. //leakcanary
  92. debugCompile ‘com.squareup.leakcanary:leakcanary-android:’ + rootProject.ext.leakcanary
  93. releaseCompile ‘com.squareup.leakcanary:leakcanary-android-no-op:’ + rootProject.ext.leakcanary
  94. //logansquare
  95. apt ‘com.bluelinelabs:logansquare-compiler:’ + rootProject.ext.logansquare
  96. compile ‘com.bluelinelabs:logansquare:’ + rootProject.ext.logansquare
  97. //dagger2 注入
  98. apt ‘com.google.dagger:dagger-compiler:’ + rootProject.ext.dagger
  99. compile ‘com.google.dagger:dagger:’ + rootProject.ext.dagger
  100. //UI注入
  101. compile ‘com.jakewharton:butterknife:’ + rootProject.ext.butterknife
  102. //rxjava
  103. compile ‘io.reactivex:rxjava:1.0.10’
  104. compile ‘io.reactivex:rxandroid:0.24.0’
  105. //网络框架
  106. compile ‘com.squareup.retrofit:retrofit:’ + rootProject.ext.retrofit
  107. compile ‘com.squareup.retrofit:converter-gson:’ + rootProject.ext.retrofit
  108. compile ‘com.squareup.retrofit:adapter-rxjava:’ + rootProject.ext.retrofit
  109. }
  110. }

属性

如果是单个脚本,则不需要考虑属性的跨脚本传播,但是Gradle往往包含不止一个build.gradle文件,比如我设置的utils.gradle,settings.gradle。如何在多个脚本中设置属性呢?
Gradle提供了一种名为extra property的方法。extra property是额外属性的意思,在*次定义该属性的时候需要通过ext前缀来标示它是一个额外的属性。定义好之后,后面的存取就不需要ext前缀了。ext属性支持Project和Gradle对象。即Project和Gradle对象都可以设置ext属性

在上面的例子中其实我们已经用到了ext来定义一些常亮,eg:packageName,compileversion等
现在结合groovy相关知识,再重构下上面的gradle配置.

定义一个const.gradle用来管理我们的常亮和各个库所依赖的库.

  1. ext {
  2. android_const = [
  3. compileSdk : 23,
  4. minSdk : 11,
  5. targetSdk : 23,
  6. support : “23.1.1”,
  7. buildTools : “23.0.2”,
  8. packageName: “com.evilsoulm.keep_nice”
  9. ]
  10. dep_const = [
  11. glide : “3.6.0”,
  12. okio : “1.4.0”,
  13. okhttp : “2.4.0”,
  14. fabric : “2.4.0”,
  15. leakcanary : “1.3.1”,
  16. logansquare: “1.1.0”,
  17. dagger : “2.0.1”,
  18. butterknife: “7.0.1”,
  19. retrofit : “2.0.0-beta2”,
  20. greendao : “2.0.0”
  21. ]
  22. app_dep = [
  23. “appcompat-v7” : ‘com.android.support:appcompat-v7:’ + android_const.support,
  24. “design” : ‘com.android.support:design:’ + android_const.support,
  25. “okio” : ‘com.squareup.okio:okio:’ + dep_const.okio,
  26. “okhttp” : ‘com.squareup.okhttp:okhttp:’ + dep_const.okhttp,
  27. “glide” : ‘com.github.bumptech.glide:glide:’ + dep_const.glide,
  28. “debug_leakcanary” : ‘com.squareup.leakcanary:leakcanary-android:’ + dep_const.leakcanary,
  29. “release_leakcanary”: ‘com.squareup.leakcanary:leakcanary-android-no-op:’ + dep_const.leakcanary,
  30. “glide” : ‘com.github.bumptech.glide:glide:’ + dep_const.glide,
  31. “apt_logansquare” : ‘com.bluelinelabs:logansquare-compiler:’ + dep_const.logansquare,
  32. “logansquare” : ‘com.bluelinelabs:logansquare:’ + dep_const.logansquare,
  33. “apt_dagger” : ‘com.google.dagger:dagger-compiler:’ + dep_const.dagger,
  34. “dagger” : ‘com.google.dagger:dagger:’ + dep_const.dagger,
  35. “butterknife” : ‘com.jakewharton:butterknife:’ + dep_const.butterknife,
  36. “rxjava” : ‘io.reactivex:rxjava:1.0.10’,
  37. “rxandroid” : ‘io.reactivex:rxandroid:0.24.0’,
  38. “retrofit” : ‘com.squareup.retrofit:retrofit:’ + dep_const.retrofit,
  39. “converter-gson” : ‘com.squareup.retrofit:converter-gson:’ + dep_const.retrofit,
  40. “adapter-rxjava” : ‘com.squareup.retrofit:adapter-rxjava:’ + dep_const.retrofit
  41. ]
  42. model_generator_dep = [
  43. “greendao-generator”: ‘de.greenrobot:greendao-generator:’ + dep_const.greendao
  44. ]
  45. model_dep = [
  46. “greendao”: ‘de.greenrobot:greendao:’ + dep_const.greendao,
  47. “android” : ‘com.google.android:android:4.1.1.4’
  48. ]
  49. }

根目录的build.gradle

  1. // Top-level build file where you can add configuration options common to all sub-projects/modules.
  2. apply from: “basegradle/const.gradle” //引入const.gradle 有点继承的感觉 也就是现在rootProject也就拥有const.gradle里的方法和变量
  3. buildscript {
  4. repositories {
  5. mavenCentral()
  6. }
  7. dependencies {
  8. classpath ‘com.android.tools.build:gradle:1.2.3’
  9. }
  10. }
  11. allprojects {
  12. repositories {
  13. mavenCentral()
  14. }
  15. }

使用
//老的版本

  1. apply plugin: ‘java’
  2. dependencies {
  3. compile ‘de.greenrobot:greendao:’ + rootProject.ext.greendao
  4. compile(group: ‘com.google.android’, name: ‘android’, version: ‘4.1.1.4’)
  5. }

改造后 简单清楚

  1. apply plugin: ‘java’
  2. dependencies {
  3. compile rootProject.ext.model_dep.greendao
  4. compile rootProject.ext.model_dep.android
  5. }

查看依赖树

gradle dependencies
  1. task helloShortCut << {
  2. println ‘short cut’
  3. }
  4. //打印0道9
  5. task count << {
  6. 10.times { print(“$it “) }
  7. }
  8. task intro(dependsOn: helloShortCut) << {
  9. print “i am gradle”
  10. }
  11. //动态task
  12. 10.times {
  13. time ->
  14. task “task$time” << {
  15. println ” i am task number $time”
  16. }
  17. }
  18. task8.dependsOn task1, task2, task3, task4
  19. task hello << {
  20. println ‘Hello Earth’
  21. }
  22. hello.doFirst {
  23. println ‘Hello Venus’
  24. }
  25. hello.doLast {
  26. println ‘Hello Mars’
  27. }
  28. hello << {
  29. println ‘Hello Jupiter’
  30. }
  31. task loadFile << {
  32. def files = file(‘../antLoadfileResources’).listFiles().sort();
  33. files.each {
  34. File file ->
  35. if (file.isFile()) {
  36. }
  37. }
  38. }
  39. gradle.taskGraph.whenReady {taskGraph ->
  40. if (taskGraph.hasTask(“assembleSpeedCompilation”)) {
  41. println ‘_____________________________assembleSpeedCompilationDebug’
  42. } else {
  43. println ‘_____________________________–不包含’
  44. }
  45. }
  1. //显示依赖包的存储路径task
  2. showRemoteDependencies << {
  3. configurations.compile.each { println it
  4. }}

demo->github

android studio 导入外部库文件,以及将项目中module变成library引用依赖

android studio 导入外部库文件,以及将项目中module变成library引用依赖

一:导入如百度地图等的外部类。

步骤:1.首先 将androidstudio项目显示切换到 project 状态显示项目

2.然后添加.jar文件,将所有的.jar文件放入libs文件夹内(libs文件夹就在项目文件夹下),然后在引入的.jar文件上右键然后点击 Add As Library… OK jar文件引入。

3.添加.so文件,在项目下的src目录下的main目录下新建jniLibs文件夹,然后将so文件连带着他外面的文件夹整个复制到jniLibs文件夹下(注意:so文件不能直接存在于jniLibs文件夹下,需要存在于如armeabi等文件中放入jniLibs文件夹下),倒入文件后在该文件的build.gradle中添加。(为了保证不出错,可以将.jar文件放入JinLibs将so文件放入libs文件中,使得libs跟jniLibs文件夹下都存在so跟jar。)

sourceSets{
    main(){
        jniLibs.srcDirs = ['libs']
    }
}
代码。具体放入位置如下:

apply plugin: 'com.android.library'
android {
    compileSdkVersion 23
    buildToolsVersion "23.0.3"

    defaultConfig {
        minSdkVersion 11
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    sourceSets{
        main(){
            jniLibs.srcDirs = ['libs']
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.4.0'
    compile files('libs/BaiduLBS_Android.jar')
}
现在就可以使用外部类的方法了。

二:将同项目的module作为依赖包引用
1.选择你想作为library的module。选择他的build.gradle文件将*上方的代码apply plugin: 'com.android.application'改为apply plugin: 'com.android.library'。然后将下面的代码删去位置为:android下的defaultConfig下的applicationId "frame.myc.com.mycframe"。删除后代码为
android {
    compileSdkVersion 23
    buildToolsVersion "23.0.3"

    defaultConfig {
        minSdkVersion 11
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    sourceSets{
        main(){
            jniLibs.srcDirs = ['libs']
        }
    }
}
2.为主文件添加依赖 mac下使用以下操作:点击file->project structure左边的module下选择你的主工程,然后右边点击dependencies,点击下方或者右方的+点开后在三个选项中选择module dependency,在弹出界面选择你刚刚修改作为library的midule文件 ok了。
其实*简单的方法就是刚开始建立module的时候就作为library来新建。仔细去新建一个module来体会一下,在选择模式的时候选择library就可以啦


出现错误:当你的程序需要引用两个及以上的module library或者其他的jar包是出现错误类型如下

Error:Execution failed for task ‘:app:transformResourcesWithMergeJavaResForDebug’.
> com.android.build.api.transform.TransformException: com.android.builder.packaging.DuplicateFileException: Duplicate files copied in APK org/apache/log4j/xml/log4j.dtd
File1: /Users/minyuchun/androidwork/projectstudio/StarFaceFrame/app/build/intermediates/exploded-aar/StarFaceFrame/facelibrary/unspecified/jars/classes.jar
File2: /Users/minyuchun/androidwork/projectstudio/StarFaceFrame/app/build/intermediates/exploded-aar/StarFaceFrame/rylibrary/unspecified/jars/classes.jar

出现上述错误的原因是因为 你在引用的labrary中多个存在相同的包导致在打包是冲突 解决方式如下,在android下的 写
packagingOptions{
  exclude 'org/apache/log4j/xml/log4j.dtd'
}
''单引号中的内容为上述错误中APK后面的内容,按照上述的样式填写在 主的app.gradle 中,重新编译后运行,运行后还可能出现相同的错误 这时候请注意错误后面APK中的内容 此时应该与前一次出现的内容不相同。如果是这样的话继续按照上述的方式增加,循环*后就没有这个错误了。

%title插图%num