Python实现用手机监控远程控制电脑

Python实现用手机监控远程控制电脑

 

C语言数据结构与算法
数据结构是一门研究数据之间关系的一门课程,是编程境界提升的一大跳板,学好数据结构对提升编程能力有很大的帮助。 本次专栏全部使用C语言作为实践语言,带大家全面的了解数据结构和算法。以*直观的例子,让大家认识到各个数据结构,一级一些我们经常使用的算法和一些非常有趣的算法。希望本专栏可以给大家带来帮助。
ZackSock
¥19.90
订阅专栏
一、前言
很多时候,我们都有远程控制电脑的需求。比如正在下载某样东西,需要让电脑在下载完后关机。或者你需要监控一个程序的运行状况等。

今天我们就来用Python实现一个远程监控并控制电脑的小程序。

二、实现原理
听起来远程控制电脑好像很高级的样子,但是实现起来其实非常简单。实现原理如下:

运行程序,让程序不停地读取邮件
用手机给电脑发送邮件
判断是否读取到指定主题的邮件,如果有,则获取邮件内容
根据邮件内容,执行预设的函数
与其说是学习如何远程控制电脑,还不如说是学习如何读取邮件。当然,上面的的流程只实现了远程控制电脑,而没实现对电脑的监控。而监控的操作可以以截图的方式来进行。

我们可以预设一个指令,当读取到邮件内容为grab时,我们就发送电脑截图。如何将电脑截图发送给手机邮箱,这样就达到了监控的效果。

关于如何发送邮件可以参考博客:如何用Python发送邮件?。这里就不再详细说了。下面我们看看如何读取邮件。

三、读取邮件
读取邮件需要使用到imbox模块,安装语句如下:

pip install imbox
1
读取邮件的代码如下:

from imbox import Imbox

def read_mail(username, password):
with Imbox(‘imap.163.com’, username, password, ssl=True) as box:
all_msg = box.messages(unread=True)
for uid, message in all_msg:
# 如果是手机端发来的远程控制邮件
if message.subject == ‘Remote Control’:
# 标记为已读
box.mark_seen(uid)
return message.body[‘plain’][0]

首先我们用with语句,打开邮箱。然后通过下面语句获取所有的未读邮件:

all_msg = box.messages(unread=True)
1
获取未读邮件后,对邮件进行遍历。将主题为“Reomte Control”的邮件标记为已读,并返回文本内容。

这里需要注意,因为我们筛选出了主题为“Remote Control”的邮件,因此我们在用手机发邮件的时候需要将主题设置为“Remote Control”,这样可以避免其它邮件的干扰。

四、截图
截图需要使用到PIL模块,安装如下:

pip install pillow
1
截图的代码很简单:

from PIL import ImageGrab

def grab(sender, to):
# 截取电脑全屏
surface = ImageGrab.grab()
# 将截屏保存为surface.jpg
surface.save(‘surface.jpg’)
# 将截屏发送给手机
send_mail(sender, to, [‘surface.jpg’])

其中send_mail的代码如下:

import yagmail

def send_mail(sender, to, contents):
smtp = yagmail.SMTP(user=sender, host=’smtp.163.com’)
smtp.send(to, subject=’Remote Control’, contents=contents)

关于发送邮件的介绍可以参考上面提到的博客。

五、关机
关机的操作非常简单,我们可以用python来执行命令行语句即可。代码如下:

import os

def shutdown():
# 关机
os.system(‘shutdown -s -t 0’)

除了关机,我们还可以执行很多操作。对于一些复杂的操作,我们可以预编写一些bat文件,这里就不演示了。

六、完整代码
上面我们编写了各个部分的代码,然后再来看看主体部分的代码:

def main():
# 电脑用来发送邮件已经电脑读取的邮箱
username = ‘sockwz@163.com’
password = ‘********’

# 手机端的邮箱
receiver = ‘2930777518@qq.com’

# 读取邮件的时间间隔
time_space = 5

# 注册账户
yagmail.register(username, password)

# 循环读取
while True:
# 读取未读邮件
msg = read_mail(username, password)
if msg:
# 根据不同的内容执行不同操作
if msg == ‘shutdown’:
shutdown()
elif msg == ‘grab’:
grab(username, receiver)
time.sleep(time_space)

其中:

yagmail.register(username, password)
1
会使用到keyring模块,安装如下:

pip install keyring
1
后面我们可以根据自己的需求编写一些其它功能。下面是完整的代码:

import os
import time
import yagmail
from imbox import Imbox
from PIL import ImageGrab

def send_mail(sender, to, contents):
smtp = yagmail.SMTP(user=sender, host=’smtp.163.com’)
smtp.send(to, subject=’Remote Control’, contents=contents)

def read_mail(username, password):
with Imbox(‘imap.163.com’, username, password, ssl=True) as box:
all_msg = box.messages(unread=True)
for uid, message in all_msg:
# 如果是手机端发来的远程控制邮件
if message.subject == ‘Remote Control’:
# 标记为已读
box.mark_seen(uid)
return message.body[‘plain’][0]

def shutdown():
os.system(‘shutdown -s -t 0’)

def grab(sender, to):
surface = ImageGrab.grab()
surface.save(‘surface.jpg’)
send_mail(sender, to, [‘surface.jpg’])

def main():
username = ‘sockwz@163.com’
password = ‘你的授权码’
receiver = ‘2930777518@qq.com’
time_space = 5
yagmail.register(username, password)
while True:
# 读取未读邮件
msg = read_mail(username, password)
if msg:
if msg == ‘shutdown’:
shutdown()
elif msg == ‘grab’:
grab(username, receiver)
time.sleep(time_space)

if __name__ == ‘__main__’:
main()

 

云计算要具备什么技能 如何学好云计算架构

云计算要具备什么技能?如何学好云计算架构?随着互联网时代的发展以及市场需求推动,云计算应用继云物联、云安全、云存储、云游戏之后得到进一步扩张。云手机、云电视已成为人们生活不可或缺的一部分,各大企业对于云计算人才的需求也进一步加大。很多人都想加入这个行列,那么该如何学习云计算技术呢?下面就给大家分享一下云计算架构的相关知识。
%title插图%num

随着企业的不断壮大以及数据的增多,云计算已无法支持如此复杂的企业环境,而云计算架构正是基于此而诞生。云计算架构主要可分为四层:显示层、中间层、基础设施层和管理层。

显示层:以友好的方式展现用户所需的内容和服务体验,并会利用到下面中间件层提供的多种服务。主要有五种技术:HTML、JavaScript、CSS、Flash和Silverlight。

中间层:承上启下的作用,在下面的基础设施层所提供资源的基础上提供了多种服务,比如缓存服务和REST服务等,而且这些服务即可用于支撑显示层,也可以直接让用户调用。主要有五种技术:REST、多租户、并行处理、应用服务器、分布式缓存。

基础设施层:给上面的中间件层或者用户准备其所需的计算和存储等资源。主要有四种技术:虚拟化、分布式存储、关系型数据库、NoSQL。

管理层:是为横向的三层服务的,并给这三层提供多种管理和维护等方面的技术。

通过云计算架构各层中所包含的应用技术,我们可以了解到当前企业对于云计算架构师的技术要求。而这些只是你从事云计算开发所应具备的基本技能,如果你想成为更高端、更高薪的云计算开发人才,你可以选择专业的学习。

python面向对象基础之魔术方法

python面向对象基础之魔术方法

 

一. 类和对象
通俗理解:类就是模板,对象就是通过模板创造出来的物体

类(Class)由3个部分构成:

类的名称: 类名

类的属性: 一组数据

类的方法: 允许对进行操作的方法 (行为)

二. 魔法方法
在python中,有一些内置好的特定的方法,方法名是“__xxx__”,在进行特定的操作时会自动被调用,这些方法称之为魔法方法。下面介绍几种常见的魔法方法。

__init__方法 :初始化一个 类 ,在创建实例对象为其 赋值 时使用。
__str__方法:在将对象转换成字符串 str(对象) 测试的时候,打印对象的信息。
__new__方法:创建并返回一个实例对象,调用了一次,就会得到一个对象。
__class__方法:获得已知对象的类 ( 对象.class)。
__del__方法:对象在程序运行结束后进行对象销毁的时候调用这个方法,来释放资源。
三. 理解self
self和对象指向同一个内存地址,可以认为self就是对象的引用。

# 创建一个类
class Car(object):
# 创建一个方法打印 self 的id
def getself(self):
print(‘self=%s’%(id(self)))

bmw = Car()
print(id(bmw))
bmw.getself()
”’输出
140033867265696
140033867265696
”’

所谓的self,可以理解为对象自己,某个对象调用其方法时,python解释器会把这个对象作为*个参数传递给self,所以开发者只需要传递后面的参数即可。

# 创建一个类
class Car(object):
def __init__(self,name,colour):
self.name = name
self.colour = colour
# 创建一个方法打印 self 的id
def getself(self):
print(‘self=%s’%(id(self)))

bmw = Car(‘宝马’,’黑色’)
# 实例化对象时,self不需要开发者传参,python自动将对象传递给self
print(id(bmw))
bmw.getself()

三. 练习对战
做两个人物对战

import random
import time
#定义类
class hero(object):
# 定义属性
def __init__(self,name,blood,dblood,ablood):
self.name=name#名字
self.blood=blood#血量
self.dblood=dblood#这是减少的血量
self.ablood=ablood
#定义方法
# 互捅
def tong(self,enemy):
enemy.blood-=self.dblood
print(‘%s砍掉了%s%d的血量’%(self.name,enemy.name,self.dblood))

def addblood(self):
self.blood+=self.ablood
print(‘%s吃了一颗补血药,加了%d血量’%(self.name,self.ablood))

def __str__(self):
return ‘%s 还剩下 %s 血’ % (self.name, self.blood)

xm = hero(‘西门吹雪’,100,random.randint(10,20),random.randint(10,20))
ygc = hero(‘叶孤城’,100,random.randint(10,20),random.randint(10,20))

x=[1,2]

while xm.blood>=0 or ygc.blood>=0:
if xm.blood<=0:
print(‘%s获胜’%ygc.name)
break
pass
elif ygc.blood<=0:
print(‘%s获胜’%xm.name)
break
if 10<=xm.blood <=20:
xm.addblood()
pass
elif 10 <= ygc.blood <= 20:
ygc.addblood()
pass
if random.choice(x)%2==0:
xm.tong(ygc)
print(ygc)
print(xm)
else:
ygc.tong(xm)
print(ygc)
print(xm)
print(‘***’*10)
time.sleep(1)
pass

云计算运用了哪些技术 如何掌握企业所需技能

云计算中运用了哪些技术?如何掌握企业所需技能?近年来,云计算市场前景火爆,越来越多的企业将业务迁移到云上,云计算人才也成为企业高薪招聘的对象。有人看到这些之后,就想转行加入云计算行业,下面一同来看看吧。

%title插图%num

云计算是一种按使用付费模式,可为可配置的计算资源池(包括网络、服务器、存储、应用程序、服务等资源)提供可用、方便、按需的网络访问。资源可以快速交付,只需*少的管理工作或与服务提供商的交互很少。

在云计算系统中运用了许多技术,其中以编程模型、数据管理技术、数据存储技术、虚拟化技术、云计算平台管理技术更为关键。掌握这些技术点,你可以胜任Linux运维工程师、数据库管理员、Linux高级运维工程师、Linux集群/网站架构师、Python运维开发师、云计算运维工程师、云计算架构师、云安全工程师、开发运维工程师、高级系统工程师、信息技术架构师、企业架构师等职位。

专业调查数据分析,未来五年内云计算相关的工作需求每年将会以26%的速度增长。以北京为例,云计算工程师平均薪资在1W+每月,全网每天发布招聘职位都在3000条以上。

不过云计算就业工资多少还是要取决自己的能力有多强,对于转行或者刚刚开始学习云计算的新手而言,选择专业学习无疑是非常便捷走进云计算的方法。

如果你想快速掌握企业所需的云计算技术、积累更多云计算实战项目经验,可以选择专业的学习,胜任运维工程师、云计算工程师以及Web渗透测试工程师等岗位,让自己轻松实现高薪梦!

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即可发现规律。