Android五种数据存储方式

android的五种数据存储方式

  • 文件存储
  • SharedPreferences
  • SQLite数据库存储
  • ContentProvider
  • 网络存储
一、文件存储

默认存储路径:/data/data/<PackageName>/files
文件操作模式:MODE_PRIVATE(默认):覆盖、MODE_APPEND:追加

  • 写入文件
public void save(){
      String data = "save something here";
      FileOutputStream out = null;
      ButteredWriter writer = null;
      try{
            out = openFileOutput("data",Context.MODE_PRIVATE);
            writer = new ButteredWriter(new OutputSreamWriter(out));
            writer.write(data);
      }catch(IOException e){
            e.printStackTrace();
      }finally{
            try{
                  if(writer!=null){
                        writer.close();
                  }
            }catch(IOException e){
                  e.printStackTrace();
       }
}
  • 读取数据
public String load(){
      FileInputStream in = null;
      ButteredReader reader = null;
      StringBuilder builder = new StringBuilder();
      try{
            in = openFileInput("data");
            reader = new ButteredReader(new InputStreamReader(in));
            String line= "";
            while((line = reader.readline()) != null){
                   builder.append();
            }
      }catch(IOException e){
            e.printStackTrace();
      }finally{
            if(reader != null){
                    try{
                          reader.close();
                    }catch(IOException e){
                          e.printStackTrace();
                    }
             }
      }
}
二、SharedPreferences

默认存储路径:/data/data/<PackageName>/shared_prefs
操作模式:MODE_PRIVATE(默认):只有当前的应用程序才能对文件进行读写、MODE_MULTI_PROCESS:用于多个进程对同一个SharedPreferences进行读写。
存储数据格式:键值对

获取SharedPreferences对象的方法
  • Context的getSharedPreferences()方法,参数一是文件名,参数二是操作模式
  • Activity的getPreferences()方法,参数为操作模式,使用当前应用程序包名为文件名
  • PreferenceManager的getDefaultSharedPreferences()静态方法,接收Context参数,使用当前应用程序包名为文件名
存储数据
  • 调用SharedPreferences对象的edit()方法获取一个SharedPreferences.Editor对象
  • 向Editor对象中添加数据putBoolean、putString等
  • 调用commit()方法提交数据
SharedPreferences.Editor editor = getSharedPreferences("data",MODE_PRIVATE).edit();
editor.putString("name","ZhangSan");
editor.putInt("age",12);
editor.putBoolean("isMarried",false);
editor.commit();
从SharedPreferences文件中读取数据
SharedPreferences pref = getSharedPreferences("data",MODE_PRIVATE);
String name = pref.getString("name");
int age = pref.getInt("age");
boolean isMarried = pref.getBoolean("isMarried");
三、SQLite数据库存储

默认存储路径:/data/data/<PackageName>/databases
数据类型

  • integer 整型
  • real 浮点型
  • text 文本类型
  • blob 二进制类型
public class MyDatabaseHelper extends SQLiteOpenHelper{  
    public static final String CREATE_BOOK = "create table book ( "
               + " id integer primary key autoincrement,"
               + " author text,"
               + "price real,"
               + "pages integer,"
               + "name text)"; 
    private Context context;
    public MyDatabaseHelper (Context context, String name, CursorFactory factory, int version) {  
        super(context, name, factory, version);  
        this.context = context;
    }  

    @Override  
    public void onCreate(SQLiteDatabase db) {  
        db.execSQL(CREATE_BOOK);          
    }  
      
    //当打开数据库时传入的版本号与当前的版本号不同时会调用该方法  
    @Override  
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {    
    
    }    
}  

在MainActivity中

MyDatabaseHelper helper = new MyDatabaseHelper(this,"BookStore.db",null,1);
 //检测到没有BookStore这个数据库,会创建该数据库并调用MyDatabaseHelper中的onCreated方法。
helper.getWritableDatabase(); 
升级数据库
public class MyDatabaseHelper extends SQLiteOpenHelper{  
    ......
    //当打开数据库时传入的版本号与当前的版本号不同时会调用该方法  
    @Override  
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {   
          db.execSQL("drop table if exists Book");
          onCreate(db):
    }    
} 

在MainActivity中只需将version改为大于原来版本号即可。

MyDatabaseHelper helper = new MyDatabaseHelper(this,"BookStore.db",null,2);
helper.getWritableDatabase(); 
向数据库添加数据

insert()方法,参数一表名,参数二是在未指定添加数据的情况下给某些可为空的列自动赋值为NULL,设置为null即可,参数三是ContentValues对象。
MainActivity

SQLiteDatabase db = helper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put("name","The Book Name");
values.put("author","chen");
values.put("pages",100);
values.put("price",200);
db.insert("Book",null,values);
更新数据库中的数据

update()方法,参数一是表名,参数二是ContentValues对象,参数三、四是去约束更新某一行或某几行的数据,不指定默认更新所有。
MainActivity

SQLiteDatabase db = helper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put("price",120);
db.update("Book",values,"name= ?",new String[]{"The Book Name"});
从数据库中删除数据

delete()方法,参数一是表名,参数二、三是去约束删除某一行或某几行的数据,不指定默认删除所有。
MainActivity

SQLiteDatabase db = helper.getWritableDatabase();
db.delete("Book","pages> ?",new String[]{"100"});
查询数据库中的数据

query()方法,参数一是表名,参数二是指定查询哪几列,默认全部,参数三、四是去约束查询某一行或某几行的数据,不指定默认查询所有,参数五是用于指定需要去group by的列,参数六是对group by的数据进一步的过滤,参数七是查询结果的排序方式
MainActivity

SQLiteDatabase db = helper.getWritableDatabase();
Cursor cursor = db.query("Book",null,null,null,null,null,null);
if(cursor.moveToFirst()){
      do{
            String name = cursor.getString(cursor.getColumnIndex("name");
            String author = cursor.getString(cursor.getColumnIndex("author");
            int pages = cursor.getString(cursor.getColumnIndex("pages");
            double price = cursor.getString(cursor.getColumnIndex("price");
       }while(cursor.moveToNext());
}
cursor.close():
使用SQL语句操作数据库
//添加数据
db.execSQL("insert into Book(name,author,pages,price) values(?,?,?,?) "
            ,new String[]{"The Book Name","chen",100,20});
//更新数据
db.execSQL("update Book set price = ? where name = ?",new String[]
            {"10","The Book Name"});
//删除数据
db.execSQL("delete from Book where pages > ?",new String[]{"100"});
//查询数据
db.execSQL("select * from Book",null);
使用事务操作
SQLiteDatabase db = helper.getWritableDatabase();
db.beginTransaction();  //开启事务
try{
      ......
      db.insert("Book",null,values);
      db.setTransactionSuccessful();  //事务成功执行
}catch(SQLException e){
      e.printStackTrace();
}finally{
      db.endTransaction();  //结束事务
}
四、ContentProvider

ContentProvider主要用于不同的程序之间实现数据共享的功能。

  • 访问其他应用程序中的数据

工具类ContentResolver,提供了一系列方法对数据进行CRUD操作。

ContentResolver的使用方法

1、内容URI
内容URI是由权限和路径组成的,权限是用于区分不同的应用程序,一般是以包名来命名。路径是用于区分同一个应用程序的不同表。

//包名为com.example.app的表table1访问路径
Uri uri  = Uri.parse("content://com.example.app.provider/table1");

2、使用Uri对象进行数据操作

  • 查询
Cursor cursor = getContentResolver().query(uri,null,null,null,null);
if(cursor != null){
      while(cursor.moveToNext()){
            String column1 = cursor.getString(cursor.getColumnIndex("column1"));
            String column2 = cursor.getString(cursor.getColumnIndex("column2"));
      }
      cursor.close();
}
  • 插入
ContentValues values = new ContentValues();
values.put("column1","text");
values.put("column2",1);
getContentResolver().insert(uri,values);
五、网络存储

作者:cy_why
链接:https://www.jianshu.com/p/536ca489a7f4
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

下载百度文库的文档老要下载劵!用Python“破解”这个限制!

前言

大家都应该有过从百度文库下载东西的经历,对于下载需要下载券的文章,我们可以办理文库VIP(土豪的选择):

有的人也会在某宝购买一定的下载券,然后进行下载。而另一些勤勤恳恳的人,则会选择上传文章,慢慢攒下载券。任劳任怨的人,则会自己一点一点的复制粘贴,复制到word里文字太大,那就复制到txt文件里。而既不想花钱又不想攒下载券,也不想一点一点复制粘贴的人,会选择“冰点文库”这样的下载软件,不过貌似现在“冰点文库”已经不能使用了。当然,还有一些其他破解方法,比如放到手机的百度文库APP里,另存为文章,不需要下载券就可以下载文章。诸如此类的方法,可谓五花八门。而对于学习爬虫的人来说,面对怎样免费下载一个付费的word文章的问题,第一个想到的应该就是:自己写个程序搞下来。

2

问题分析

我们以如何下载下面这篇文章为例,分析问题:

URL : https://wenku.baidu.com/view/aa31a84bcf84b9d528ea7a2c.html

我想,看到这样的一个文章,如果爬取当前页面的内容还是很好爬的吧。感觉so easy!至少我当时是这么想的,但是当把文章翻到最下方的时候,我看到了如下内容:

呃….需要点击“继续阅读”才能显示后续的内容,我单爬这一页内容,是爬不到后续的内容的。第一个想到的方法是,抓包分析下,然后我又一次蒙逼了:

Request URL这么长!!最后的expire时间信息好解决,其他的信息呢?不想做无谓的挣扎,因此,我果断地放弃这个方法。

问题:获取当前页的内容好办,怎么获取接下来页面的内容?

带着这个思考,Selenium神器走入了我的视线。

3

预备知识

3.1 Selenium

3.1.1 简介

Selenium 是什么?一句话,自动化测试工具。它支持各种浏览器,包括 Chrome,Safari,Firefox 等主流界面式浏览器,如果你在这些浏览器里面安装一个 Selenium 的插件,那么便可以方便地实现Web界面的测试。换句话说叫 Selenium 支持这些浏览器驱动。Selenium支持多种语言开发,比如 Java,C,Ruby等等,而对于Python,当然也是支持的!

3.1.2 安装

3.1.3 基础知识

3.1.3.1 小试牛刀

我们先来一个小例子感受一下 Selenium,这里我们用 Chrome 浏览器来测试。

运行这段代码,会自动打开浏览器,然后访问百度。

如果程序执行错误,浏览器没有打开,那么应该是没有装 Chrome 浏览器或者 Chrome 驱动没有配置在环境变量里。下载驱动,然后将驱动文件路径配置在环境变量即可。

驱动下载地址:https://sites.google.com/a/chromium.org/chromedriver/downloads

windows下设置环境变量的方法:,输入,点击确定,出现如下对话框:

选择高级->环境变量。在系统变量的Path变量中,添加驱动文件路径即可(注意:分号)。

Linux的环境变量也好设置,在文件中export即可,记得。

当然,你不设置环境变量也是可以的,程序可以这样写:

上面的 是你的chrome驱动文件位置,可以使用绝对路径。我们通过驱动的位置传递参数,也可以调用驱动,结果如下图所示:

3.1.3.2 模拟提交

下面的代码实现了模拟提交提交搜索的功能,首先等页面加载完成,然后输入到搜索框文本,点击提交,然后使用page_source打印提交后的页面的信息。

全自动的哦,程序操控!是不是很酷炫?

其中 driver.get 方法会打开请求的URL,WebDriver 会等待页面完全加载完成之后才会返回,即程序会等待页面的所有内容加载完成,JS渲染完毕之后才继续往下执行。注意:如果这里用到了特别多的 Ajax 的话,程序可能不知道是否已经完全加载完毕。

WebDriver 提供了许多寻找网页元素的方法,譬如 的方法。例如一个输入框可以通过 方法寻找 name 属性来确定。

然后我们输入来文本然后模拟点击了回车,就像我们敲击键盘一样。我们可以利用 Keys 这个类来模拟键盘输入。

最后最重要的一点是可以获取网页渲染后的源代码。通过,输出 属性即可。这样,我们就可以做到网页的动态爬取了。

3.1.3.3 元素选取

关于元素的选取,有如下API:

单个元素选取:

多个元素选取:

另外还可以利用 By 类来确定哪种选择方式:

By类的一些属性如下:

这些方法跟JavaScript的一些方法有相似之处,,就是根据标签的id属性查找元素,,就是根据标签的name属性查找元素。举个简单的例子,比如我想找到下面这个元素:

我们可以这样获取它:

前三个都很好理解,最后一个xpath什么意思?这个无需着急,xpath是非常强大的元素查找方式,使用这种方法几乎可以定位到页面上的任意元素,在后面我会进行单独讲解。

3.1.3.4 界面交互

通过元素选取,我们能够找到元素的位置,我们可以根据这个元素的位置进行相应的事件操作,例如输入文本框内容、鼠标单击、填充表单、元素拖拽等等。由于篇幅原因,我就不一一讲解了,主要讲解本次实战用到的鼠标单击,更详细的内容,可以查看官方文档。

比如上面这句话,我使用找到元素位置,暂且不用理会这句话什么意思,暂且理解为找到了一个按键的位置。然后我们使用click()方法,就可以触发鼠标左键单击事件。是不是很简单?但是有一点需要注意,就是在点击的时候,元素不能有遮挡。什么意思?就是说我在点击这个按键之前,窗口最好移动到那里,因为如果这个按键被其他元素遮挡,click()就触发异常。因此稳妥起见,在触发鼠标左键单击事件之前,滑动窗口,移动到按键上方的一个元素位置:

上面的代码,就是将窗口滑动到page这个位置,在这个位置,我们能够看到我们需要点击的按键。

3.1.3.5 添加User-Agent

使用webdriver,是可以更改User-Agent的,代码如下:

使用Android的User-Agent打开浏览器,画风是这样的(第二条新闻的图片略劲爆):

Selenium就先介绍这么多,对于本次实战内容,已经足够。那么接下来,让我们聊聊xpath。

3.2 Xpath

这个方法是非常强大的元素查找方式,使用这种方法几乎可以定位到页面上的任意元素。在正式开始使用XPath进行定位前,我们先了解下什么是XPath。XPath是XML Path的简称,由于HTML文档本身就是一个标准的XML页面,所以我们可以使用XPath的语法来定位页面元素。

假设我们现在以图所示HTML代码为例,要引用对应的对象,XPath语法如下:

绝对路径写法(只有一种),写法如下:

引用页面上的form元素(即源码中的第3行):

注意:

元素的xpath绝对路径可通过firebug直接查询。

一般不推荐使用绝对路径的写法,因为一旦页面结构发生变化,该路径也随之失效,必须重新写。

绝对路径以单/号表示,而下面要讲的相对路径则以表示,这个区别非常重要。另外需要多说一句的是,当xpath的路径以开头时,表示让Xpath解析引擎从文档的根节点开始解析。当xpath路径以开头时,则表示让xpath引擎从文档的任意符合的元素节点开始进行解析。而当出现在xpath路径中时,则表示寻找父节点的直接子节点,当出现在xpath路径中时,表示寻找父节点下任意符合条件的子节点,不管嵌套了多少层级(这些下面都有例子,大家可以参照来试验)。弄清这个原则,就可以理解其实xpath的路径可以绝对路径和相对路径混合在一起来进行表示,想怎么玩就怎么玩。

下面是相对路径的引用写法:

查找页面根元素:

查找页面上所有的input元素:

查找页面上第一个form元素内的直接子input元素(即只包括form元素的下一级input元素,使用绝对路径表示,单/号):

查找页面上第一个form元素内的所有子input元素(只要在form元素内的input都算,不管还嵌套了多少个其他标签,使用相对路径表示,双//号):

查找页面上第一个form元素:

查找页面上id为loginForm的form元素:

查找页面上具有name属性为username的input元素:

查找页面上id为loginForm的form元素下的第一个input元素:

查找页面具有name属性为contiune并且type属性为button的input元素:

查找页面上id为loginForm的form元素下第4个input元素:

Xpath功能很强大,所以也可以写得更加复杂一些,如下面图所示的HTML源码。

如果我们现在要引用id为“J_password”的input元素,该怎么写呢?我们可以像下面这样写:

也可以写成:

这里解释一下,其中//*[@id=’ J_login_form’]这一段是指在根元素下查找任意id为J_login_form的元素,此时相当于引用到了form元素。后面的路径必须按照源码的层级依次往下写。按照图(3)所示代码中,我们要找的input元素包含在一个dt标签内,而dt又包含在dl标签内,所以中间必须写上dl和dt两层,才到input这层。当然我们也可以用*号省略具体的标签名称,但元素的层级关系必须体现出来,比如我们不能写成//[@id=’J_login_form’]/input[@id=’J_password’],这样肯定会报错的。

前面讲的都是xpath中基于准确元素属性的定位,其实xpath作为定位神器也可以用于模糊匹配。本次实战,可以进行准确元素定位,因此就不讲模糊匹配了。如果有兴趣,可以自行了解。

4

动手实战

以上面提到的文章为例,进行爬取讲解。URL :https://wenku.baidu.com/view/aa31a84bcf84b9d528ea7a2c.html

4.1 页面切换

由于网页的百度文库页面复杂,可能抓取内容不全,因此使用User-Agent,模拟手机登录,然后打印文章标题,文章页数,并进行翻页。先看下这个网站。

我们需要找到两个元素的位置,一个是页码元素的位置,我们根据这个元素的位置,将浏览器的滑动窗口移动到这个位置,这样就可以避免click()下一页元素的时候,有元素遮挡。然后找到下一页元素的位置,然后根据下一页元素的位置,触发鼠标左键单击事件。

我们审查元素看一下,这两个元素:

我们根据这两个元素,就可以通过xpath查找元素位置,代码分别如下:

由于page元素有很多,所以我们使用find_elements_by_xpath()方法查找,然后使用page[-1],也就是链表中的最后一个元素的信息进行浏览器窗口滑动,代码如下:

运行效果,自动翻页了有木有!

4.2 内容爬取

爬取内容这里,使用之前重点讲过的BeautifulSoup就可以。这里不再细奖,审查元素,自己分析下就有了。代码如下:

爬取结果如下:

爬取的内容还是蛮规整的,对吧?

4.3 整体代码

我们能够翻页,也能够爬取当前页面内容,代码稍作整合,就可以爬取所有页面的内容了!找下网页的规律就会发现,5页文章放在一个网页里。思路:爬取正文内容,再根据爬取到的文章页数,计算页数/5.0,得到一个分数,如果这个分数大于1,则翻页继续爬,如果小于或等于1,代表到最后一页了。停止翻页。有一点注意一下,翻页之后,等待延时一下,等待页面加载之后在爬取内容,这里,我们使用最简单的办法,用sleep()进行延时。因此总体代码如下:

瞧,最后一页的内容也爬取下来了,接下来的工作就简单了,把这个结果写到txt文件中,我这里就不再进行讲解了。

至此,整篇的内容,我们都爬取下来了。是不是很酷?那就开始动手实践吧!

5

总结

这样爬取是可以爬取到内容,但是缺点也很明显:

没有处理图片内容,可以后续完善;

代码通用性不强,有的文章结构不是这样,需要对代码进行略微修改,才能爬取到内容;

对于上百页的内容爬取有些问题,翻页方式变了,需要换种方法处理,有兴趣的可以自己看下;

等待页面切换方法太out,可以使用显示等待的方式,等待页面加载;

selenium虽好,但是有些耗时,可以使用PhantomJS对这部分代码进行替换;

最后,我感觉我的方法可能有些low,如果有更好的方法,欢迎交流。

使用VB.NET开发多线程

摘要:.NET 框架提供了新的类,可以方便地创建多线程应用程序。本文介绍如何使用 Visual Basic® .NET 的多线程编程技术来开发效率更高、响应速度更快的应用程序。

目录

  • 简介
  • 多线程处理的优点
  • 创建新线程
  • 同步线程
  • 线程计时器
  • 取消任务
  • 总结

简介

过去,Visual Basic 开发人员创建的应用程序都是程序任务依次执行的同步应用程序。虽然多线程应用程序因多个任务几乎同时运行而具有更高的效率,但使用早期版本的 Visual Basic 来创建这样的应用程序却很困难。

一项称为多任务处理的操作系统功能使多线程程序成为可能,它能模拟同时运行多个应用程序的功能。虽然多数个人计算机都只安装了一个处理器,但现代操作系统通过将处理器时间分配给多段可执行代码(称为线程),提供了多任务处理功能。线程可以代表整个应用程序,但通常只代表应用程序中可单独运行的一部分。操作系统根据线程的优先级、上次运行线程后经过的时间等因素为每个线程分配处理时间。在执行耗时的任务(如文件输入和输出)时,多线程能够显著提高性能。

但要注意一个问题。虽然多线程可以提高性能,但每个线程都需要额外的内存来创建线程,还需要处理器时间来运行线程。如果创建的线程过多,反而会降低应用程序的性能。在设计多线程应用程序时,应在添加更多线程所获得的好处及其成本之间进行权衡。

多任务处理成为操作系统的一部分已经很长时间了。但直到最近,Visual Basic 程序员也只能通过非正式发布的功能,来执行多线程任务,或者通过使用 COM 组件或操作系统的异步组件,来间接实现此功能。而 .NET 框架在 System.Threading 命名空间中为开发多线程应用程序提供了全面的支持。

本文讨论多线程的一些优点以及如何使用 Visual Basic .NET 来开发多线程应用程序。虽然 Visual Basic .NET 和 .NET 框架使多线程应用程序的开发变得很简单,但本文主要面向中高级开发人员,以及正在从 Visual Basic 的早期版本过渡到 Visual Basic .NET 的开发人员。对于 Visual Basic .NET 的初学者,请首先阅读 Visual Basic Language Tour(英文)中的相应主题。

本文并非是对多线程编程的全面讨论。要获得更多的信息,请参阅本文最后列出的其他资源。

多线程处理的优点

同步应用程序的开发比较容易,但由于需要在上一个任务完成后才能开始新的任务,所以其效率通常比多线程应用程序低。如果完成同步任务所用的时间比预计时间长,应用程序可能会不响应。多线程处理可以同时运行多个过程。例如,文字处理器应用程序在您处理文档的同时,可以检查拼写(作为单独的任务)。由于多线程应用程序将程序划分成独立的任务,因此可以在以下方面显著提高性能:

  • 多线程技术使程序的响应速度更快,因为用户界面可以在进行其他工作的同时一直处于活动状态。
  • 当前没有进行处理的任务可以将处理器时间让给其他任务。
  • 占用大量处理时间的任务可以定期将处理器时间让给其他任务。
  • 可以随时停止任务。
  • 可以分别设置各个任务的优先级以优化性能。

是否需要创建多线程应用程序取决于多个因素。在以下情况下,最适合采用多线程处理:

  • 耗时或大量占用处理器的任务阻塞用户界面操作。
  • 各个任务必须等待外部资源(如远程文件或 Internet 连接)。

例如,用于跟踪 Web 页上的链接并下载满足特定条件的文件的 Internet 应用程序“robot”。这种应用程序可以依次同步下载各个文件,也可以使用多线程同时下载多个文件。多线程方法比同步方法的效率高很多,因为即使在某些线程中远程 Web 服务器的响应非常慢,也可以下载文件。

创建新线程

创建线程最直接的方法是创建新的线程类实例,并使用 AddressOf 语句为要运行的过程传递委托。例如,以下代码将名为 SomeTask 的子过程作为单独的线程运行。

Dim Thread1 As New System.Threading.Thread(AddressOf SomeTask)
Thread1.Start
' 此处的代码立即运行。

以上所述就是创建和启动线程的方法。在线程 Start 方法调用之后的任何代码将立即运行,而无需等待前一个线程运行结束。

下表列出了用于控制各个线程的一些方法。

方法 操作
Start 使线程开始运行。
Sleep 使线程暂停一段指定的时间。
Suspend 使线程在到达安全点后暂停。
Abort 使线程在到达安全点后停止。
Resume 重新启动挂起的线程。
Join 使当前线程等待其他线程运行结束。如果使用超时值,且线程在分配的时间内结束,此方法将返回 True

多数方法都无需再加以说明,但“安全点”可能是个新的概念。安全点是指代码中的某些位置,在这些位置公共语言运行时可以安全地执行自动垃圾回收,即释放未使用的变量并回收内存。调用线程的 Abort 或 Suspend 方法时,公共语言运行时将分析代码并确定线程停止运行的适当位置。

线程还包含许多有用的属性,如下表所示:

属性
IsAlive 如果线程处于活动状态,则包含值 True
IsBackground 获取或设置布尔值,指示线程是否是后台线程或是否应该是后台线程。后台线程与前台线程类似,但后台线程并不阻止进程的终止。当进程的所有前台线程都终止后,公共语言运行时将对仍处于活动状态的后台线程调用 Abort 方法,以结束该进程。
Name 获取或设置线程的名称。常用于在调试时查找各个线程。
Priority 获取或设置操作系统用来确定线程优先级安排的值。
ApartmentState 获取或设置用于特定线程的线程模型。当线程调用非托管的代码时,线程模型将非常重要。
ThreadState 包含说明线程状态的值。

线程属性和方法对创建和管理线程非常有用。本文的线程同步部分将介绍如何使用这些属性和方法控制和协调线程。

线程参数和返回值

前面示例中的方法调用不能包含任何参数或返回值。这一限制是使用此方法创建和运行线程的主要缺点之一。然而,可以通过将在单独的线程中运行的过程包装到类或结构中,为它们提供参数,并使之能返回参数。

Class TasksClass
   Friend StrArg As String
   Friend RetVal As Boolean
   Sub SomeTask()
      ' 将 StrArg 字段用作参数。
      MsgBox("StrArg 包含字符串" & StrArg)
      RetVal = True ' 设置返回参数的返回值。
   End Sub
End Class
' 要使用类,请设置存储参数的属性或字段,
' 然后,根据需要异步调用方法。
Sub DoWork()
   Dim Tasks As New TasksClass()
   Dim Thread1 As New System.Threading.Thread( _
       AddressOf Tasks.SomeTask)
   Tasks.StrArg = "某个参数" ' 设置用作参数的字段。
   Thread1.Start() ' 启动新线程。
   Thread1.Join() ' 等待线程 1 运行结束。
   ' 显示返回值。
   MsgBox("线程 1 返回值" & Tasks.RetVal)
End Sub

手动创建和管理线程最适合需要控制细节(例如线程优先级和线程模型)的应用程序。可以想象,使用这种方法管理大量线程将是非常困难的。如果需要很多线程,可以考虑使用线程池以降低复杂程度。

线程池

线程池是多线程的一种形式。在线程池中,当创建线程时任务被添加到队列并自动启动。使用线程池,可以使用要运行的过程的委托来调用 Threadpool.QueueUserWorkItem 方法,Visual Basic .NET 将创建线程并运行该过程。以下示例说明了如何使用线程池启动多个任务。

Sub DoWork()
   Dim TPool As System.Threading.ThreadPool
   ' 将一个任务排队
   TPool.QueueUserWorkItem(New System.Threading.WaitCallback _ (AddressOf SomeLongTask)) ' 将另一个任务排队 TPool.QueueUserWorkItem(New System.Threading.WaitCallback _ (AddressOf AnotherLongTask)) End Sub

如果要启动很多单独的任务,但并不需要单独设置每个线程的属性,则线程池将非常有用。每个线程都以默认的堆栈大小和优先级启动。默认情况下,每个系统处理器上最多可以运行 25 个线程池线程。超过该限制的其他线程会被排队,直至其他线程运行结束后它们才能开始运行。

线程池的一个优点是可以将状态对象中的参数传递到任务过程。如果正在调用的过程需要多个参数,则可以将类的结构或实例强制转换为 Object 数据类型。

参数和返回值

从线程池线程返回值有点复杂。不允许使用从函数调用返回值的标准方法,因为只有 Sub 过程可以排队进入线程池。提供参数和返回值的一种方法是将参数、返回值和方法包装到包装类中,如线程参数和返回值中所述。一种更简单的提供参数和返回值的方法,是使用 QueueUserWorkItem 方法的 ByVal 状态对象变量(可选)。如果使用此变量将引用传递给类的实例,则该实例的成员便可以由线程池线程修改并用作返回值。您可以修改由变量(通过值传递)引用的对象,这在开始可能并非显而易见,但的确是可能的,因为只有对象引用是通过值传递的。对由对象引用所引用的对象成员进行更改之后,这些更改将应用于实际的类实例。

不能使用结构返回状态对象中的值。因为结构是值类型,异步进程所作的更改并不更改原始结构的成员。如果不需要返回值,则可以使用结构提供参数。

Friend Class StateObj
   Friend StrArg As String
   Friend IntArg As Integer
   Friend RetVal As String
End Class

Sub ThreadPoolTest()
   Dim TPool As System.Threading.ThreadPool
   Dim StObj1 As New StateObj()
   Dim StObj2 As New StateObj()
   ' 设置一些字段,用作状态对象中的参数。
   StObj1.IntArg = 10
   StObj1.StrArg = "某个字符串"
   StObj2.IntArg = 100
   StObj2.StrArg = "另一个字符串"
   ' 将一个任务排队
   TPool.QueueUserWorkItem(New System.Threading.WaitCallback _
                          (AddressOf SomeOtherTask), StObj1)
   ' 将另一个任务排队
   TPool.QueueUserWorkItem(New System.Threading.WaitCallback _
                          (AddressOf AnotherTask), StObj2)
End Sub

Sub SomeOtherTask(ByVal StateObj As Object)
   ' 将状态对象字段用作参数。
   Dim StObj As StateObj
   StObj = CType(StateObj, StateObj)   ' 强制转换为正确的类型。
   MsgBox("StrArg 包含字符串" & StObj.StrArg)
   MsgBox("IntArg 包含数字" & CStr(StObj.IntArg))
   ' 将字段用作返回值。
   StObj.RetVal = "SomeOtherTask 的返回值"
End Sub

Sub AnotherTask(ByVal StateObj As Object)
   ' 将状态对象字段用作参数。
   ' 状态对象作为 Object 进行传递。
   ' 将其强制转换为特定的类型以使其更易于使用。
   Dim StObj As StateObj
   StObj = CType(StateObj, StateObj)
   MsgBox("StrArg 包含字符串 " & StObj.StrArg)
   MsgBox("IntArg 包含数字" & CStr(StObj.IntArg))
   ' 将字段用作返回值。
   StObj.RetVal = "AnotherTask 的返回值"
End Sub

公共语言运行时自动为排队的线程池任务创建线程,然后,当任务完成后释放这些资源。将任务排队后,很难再将其取消。ThreadPool 线程始终使用多线程单元 (MTA) 线程模型来运行。如果需要使用单线程单元 (STA) 模型的线程,则应手动创建线程。

同步线程

同步在多线程编程的非结构化性质与同步处理的结构化次序之间提供了一个折衷的办法。

使用同步技术,可以完成以下操作:

  • 在必须以特定顺序执行任务时,显式控制代码运行的次序。- 或者 –
  • 当两个线程同时共享相同的资源时,避免可能出现的问题。

例如,可以使用同步使显示过程处于等待状态,直至在另一线程中运行的数据检索过程结束。

同步的方法有两种:轮询和使用同步对象。轮询反复从循环中检查异步调用的状态。使用轮询管理线程的效率最低,因为反复检查各种线程属性的状态会浪费大量资源。

例如,如果轮询要查看线程是否已结束,可以使用 IsAlive 属性。使用此属性时要很小心,因为活动的线程不一定正在运行。可以使用线程的 ThreadState 属性来获得有关线程状态的详细信息。由于在任意给定时间,线程都可能处于多种状态,因此 ThreadState 中存储的值可以是 System.Threading.Threadstate 枚举中的值的组合。因此,在轮询时应当仔细检查所有相关的线程状态。例如,如果线程的状态表明它没有运行,则该线程可能已经完成。另一方面,它也可能被挂起或处于休眠状态。

可以想象,轮询为控制运行线程的次序,牺牲了多线程的部分优点。为此,可以使用效率较高的 Join 方法来控制线程。Join 使调用过程处于等待状态,直至线程完成或调用超时(如果指定了超时)。“Join”这个名称来自这一想法,即创建的新线程是执行路径的一个分支。使用 Join 可以再次将单独的执行路径合并成一个线程。

图 1:线程

有一点需要清楚:Join 是同步调用或阻塞调用。调用 Join 或等待句柄的等待方法后,调用过程将停止并等待线程发出信号通知它已经完成。

Sub JoinThreads()
   Dim Thread1 As New System.Threading.Thread(AddressOf SomeTask)
   Thread1.Start()
   Thread1.Join()      ' 等待线程运行结束。
   MsgBox("线程运行结束")
End Sub

这些控制线程的简单方法在管理少量线程时非常有用,但不适合大型项目。下一节将讨论可用于同步线程的一些高级技术。

高级同步技术

多线程应用程序通常使用等待句柄和监视器对象来同步多个线程。下表介绍了可用于同步线程的部分 .NET 框架类。

用途
AutoResetEvent 等待句柄,用于通知一个或多个等待线程发生了一个事件。AutoResetEvent 在等待线程被释放后自动将状态更改为已发出信号。
Interlocked 为多个线程共享的变量提供原子操作。
ManualResetEvent 等待句柄,用于通知一个或多个等待线程发生了一个事件。手动重置事件的状态将保持为已发出信号,直至 Reset 方法将其设置为未发出信号状态。同样,该状态将保持为未发出信号,直至 Set 方法将其设置为已发出信号状态。当对象的状态为已发出信号时,任意数量的等待线程(即通过调用一个等待函数开始对指定事件对象执行等待操作的线程)都可以被释放。
Monitor 提供同步访问对象的机制。Visual Basic .NET 应用程序调用 SyncLock 以使用监视器对象。
Mutex 等待句柄,可用于进程间同步。
ReaderWriterLock 定义用于实现单个写入者和多个读取者的锁定。
Timer 提供按指定间隔运行任务的机制。
WaitHandle 封装操作系统特有的、等待对共享资源进行独占访问的对象。

等待句柄

等待句柄是将一个线程的状态通知另一个线程的对象。线程可以使用等待句柄,通知其他线程它们需要对资源进行独占访问。然后,其他线程必须等到没有线程在使用等待句柄时才能使用此资源。等待句柄有两种状态:已发出信号和未发出信号。不属于任何线程的等待句柄处于已发出信号状态。属于某线程的等待句柄处于未发出信号状态。

线程通过调用一种等待方法(例如 WaitOneWaitAny 或 WaitAll)来请求等待句柄的所有权。等待方法是与单独线程的 Join 方法相类似的阻塞调用。

  • 如果没有其他线程拥有该等待句柄,则调用将立即返回 True,等待句柄的状态将更改为未发出信号,而拥有等待句柄的线程将继续运行。
  • 如果线程调用了等待句柄的一种等待方法,但该等待句柄归另一线程所有,则调用线程将等待指定的时间(如果指定了超时),或者无限期地等待(未指定超时),直至其他线程释放等待句柄。如果指定了超时,并且在超时到期前释放等待句柄,则调用返回 True。否则,调用返回 False,并且进行调用的线程将继续运行。

拥有等待句柄的线程在运行结束后,或不再需要等待句柄时将调用 Set 方法。其他线程通过调用 Reset 方法,或者调用 WaitOneWaitAll 或 WaitAny 以及成功地等待某一线程调用 Set 方法之后,可以将等待句柄的状态重置为未发出信号。在单个等待线程被释放后,系统将 AutoResetEvent 句柄自动重置为未发出信号。如果没有线程处于等待状态,则事件对象的状态将保持为已发出信号。

方法 用途
WaitOne 接受一个等待句柄作为参数,并使调用线程处于等待状态,直至另一个进程调用 Set 将当前的等待句柄设置为已发出信号。
WaitAny 接受一个等待句柄数组作为参数,并使调用线程处于等待状态,直至任一指定的等待句柄已通过调用 Set 设置为已发出信号。
WaitAll 接受一个等待句柄数组作为参数,并使调用线程处于等待状态,直至所有指定的等待句柄已通过调用 Set 设置为已发出信号。
Set 将指定的等待句柄的状态设置为已发出信号,并使任何等待线程继续运行。
Reset 将指定事件的状态设置为未发出信号。

Visual Basic .NET 常用的等待句柄有三种:互斥对象、ManualResetEvent 和 AutoResetEvent。后两种通常称为同步事件。

互斥对象

互斥对象是一次只能由一个线程拥有的同步对象。实际上,“互斥”这个名称来自互斥对象的所有权相互排斥这一事实。如果线程要对资源进行独占访问,则需要请求互斥对象的所有权。由于在任何时刻,只能有一个线程拥有互斥对象,因此其他线程必须等待,直至获得互斥对象的所有权后才能使用资源。

WaitOne 方法使调用线程等待获得互斥对象的所有权。如果拥有互斥对象的线程正常终止,则互斥对象的状态将设置为已发出信号,下一个等待线程将获得所有权。

同步事件

同步事件用于通知其他线程某件事情已发生或某个资源已可用。不要被这些使用“事件”一词的项误导。同步事件与其他 Visual Basic 事件不同,它们实际上是等待句柄。与其他等待句柄类似,同步事件也有两种状态:已发出信号和未发出信号。调用同步事件的一种等待方法的线程必须等待,直至另一个线程通过调用 Set 方法向事件发出通知。有两种同步事件类。线程使用 Set 方法将 ManualResetEvent 实例的状态设置为已发出信号。线程使用 Reset 方法,或者在控制返回到一个等待 WaitOne 的调用时,将 ManualResetEvent 实例的状态设置为未发出信号。还可以使用 Set 将 AutoResetEvent 类的实例设置为已发出信号,但是只要等待线程被通知事件已发出信号,这些实例就自动返回到未发出信号状态。

以下示例使用 AutoResetEvent 类来同步线程池任务。

Sub StartTest()
   Dim AT As New AsyncTest()
   AT.StartTask()
End Sub

Class AsyncTest
   Private Shared AsyncOpDone As New _
      System.Threading.AutoResetEvent(False)

   Sub StartTask()
      Dim Tpool As System.Threading.ThreadPool
      Dim arg As String = "SomeArg"
      Tpool.QueueUserWorkItem(New System.Threading.WaitCallback( _
         AddressOf Task), arg)  ' 将一个任务排队。
      AsyncOpDone.WaitOne() ' 等待线程调用 Set。
      MsgBox("线程运行结束。")
   End Sub

   Sub Task(ByVal Arg As Object)
      MsgBox("线程正在启动。")
      System.Threading.Thread.Sleep(4000) ' 等待 4 秒钟。
      MsgBox("状态对象包含字符串 " & CStr(Arg))
      AsyncOpDone.Set()   ' 通知线程运行结束。
   End Sub
End Class

监视器对象和 SyncLock

监视器对象用于确保代码块在运行时不会被其他线程运行的代码中断。换句话说,直到同步代码块中的代码运行结束后,其他线程中的代码才能运行。在 Visual Basic .NET 中,SyncLock 关键字用于简化对监视器对象的访问。在 Visual C#® .NET 中则使用 Lock 关键字。

例如,假设有一个反复异步读取数据并显示结果的程序。如果操作系统使用抢占式多任务处理技术,则可以中断正在运行的线程而将时间用于运行其他某个线程。如果不进行同步,则如果在显示数据时,代表数据的对象被其他线程修改,则可能会看到被部分更新的数据。SyncLock 语句可以保证代码段在运行时不会被中断。以下示例说明了如何使用 SyncLock 为显示过程提供数据对象的独占访问权限。

Class DataObject
   Public ObjText As String
   Public ObjTimeStamp As Date
End Class

Sub RunTasks()
   Dim MyDataObject As New DataObject()
   ReadDataAsync(MyDataObject)
   SyncLock MyDataObject
      DisplayResults(MyDataObject)
   End SyncLock
End Sub

Sub ReadDataAsync(ByRef MyDataObject As DataObject)
   ' 添加代码以异步读取和处理数据。
End Sub

Sub DisplayResults(ByVal MyDataObject As DataObject)
   ' 添加代码以显示结果。
End Sub

如果需要确保代码段不会被在其它线程中运行的代码中断,请使用 SyncLock

Interlocked 类

为避免在多个线程尝试同时更新或比较相同的值时可能出现的问题,可以使用 Interlocked 类的方法。此类的方法使您能够安全地递增、递减、交换和比较任何线程中的值。以下示例说明了如何使用 Increment方法来递增由在其它线程中运行的过程所共享的变量。

Sub ThreadA(ByRef IntA As Integer)
   System.Threading.Interlocked.Increment(IntA)
End Sub

Sub ThreadB(ByRef IntA As Integer)
   System.Threading.Interlocked.Increment(IntA)
End Sub

ReaderWriter 锁定

在某些情况下,可能希望只在写入数据时锁定资源,而在不更新数据时则允许多个客户端同时读取数据。ReaderWriterLock 类在线程修改资源时强制独占访问资源,但在读取资源时允许进行非独占访问。ReaderWriter 锁定是独占锁定的一个很有用的替代选择,因为独占锁定使其他线程一直处于等待状态,即使那些线程并不需要更新数据。以下示例说明了如何使用 ReaderWriter 来协调多个线程的读写操作。

Class ReadWrite
' 可以从多个线程中安全地调用
' ReadData 和 WriteData 方法。
   Public ReadWriteLock As New System.Threading.ReaderWriterLock()
   Sub ReadData()
      ' 此过程从某个来源读取信息。
      ' 读取锁定禁止在线程完成读取之前写入数据, 
      ' 同时允许其他线程调用 ReadData。
      ReadWriteLock.AcquireReaderLock(System.Threading.Timeout.Infinite)
      Try
         ' 此处执行读取操作。
      Finally
         ReadWriteLock.ReleaseReaderLock() ' 释放读取锁定。
      End Try
   End Sub

   Sub WriteData()
      ' 此过程将信息写入某个来源。
      ' 写入锁定禁止在线程完成写入操作前
      ' 读取或写入数据。
      ReadWriteLock.AcquireWriterLock(System.Threading.Timeout.Infinite)
      Try
         ' 此处执行写入操作。
      Finally
         ReadWriteLock.ReleaseWriterLock() ' 释放写入锁定。
      End Try
   End Sub
End Class

死锁

线程同步在多线程应用程序中十分重要,但在多个线程相互等待时总是存在死锁的危险。就象四个方向上都停有汽车的情况,每个人都在等待另一个人走,死锁使一切操作终止。显然,避免死锁非常重要。有许多情况会导致死锁,同样,避免死锁的方法也很多。虽然本文没有足够篇幅来讨论与死锁相关的所有问题,但有一点很重要,即认真规划是避免死锁的关键。在开始编码之前,通过图解多线程应用程序,通常可以预测死锁。

线程计时器

Threading.Timer 类对在单独线程中定期运行任务十分有用。例如,可以使用线程计时器检查数据库的状态和完整性,或者备份重要文件。以下示例每两秒钟启动一个任务,并使用标志来启动使计时器停止的 Dispose 方法。本例将状态发送到输出窗口,因此在测试代码之前,应按 CONTROL+ALT+O 键以使此窗口可见。

Class StateObjClass
' 用于保留调用 TimerTask 所需的参数
      Public SomeValue As Integer
      Public TimerReference As System.Threading.Timer
      Public TimerCanceled As Boolean
End Class

Sub RunTimer()
   Dim StateObj As New StateObjClass()
   StateObj.TimerCanceled = False
   StateObj.SomeValue = 1
   Dim TimerDelegate As New Threading.TimerCallback(AddressOf TimerTask)
   ' 创建每隔 2 秒钟调用过程的计时器。
   ' 注意:这里没有 Start 方法;创建实例之后, 
   ' 计时器就开始运行。
   Dim TimerItem As New System.Threading.Timer(TimerDelegate, StateObj, _
                                               2000, 2000)
   StateObj.TimerReference = TimerItem  ' 为 Dispose 保存一个引用。

   While StateObj.SomeValue < 10 ' 运行 10 个循环。
      System.Threading.Thread.Sleep(1000)  ' 等待 1 秒钟。
   End While

   StateObj.TimerCanceled = True  ' 请求计时器对象的 Dispose。
End Sub

Sub TimerTask(ByVal StateObj As Object)
   Dim State As StateObjClass = CType(StateObj, StateObjClass)
   Dim x As Integer
   ' 使用 Interlocked 类递增计数器变量。
   System.Threading.Interlocked.Increment(State.SomeValue)
   Debug.WriteLine("已启动了新线程 " & Now)
   If State.TimerCanceled Then    ' 已请求 Dispose。
      State.TimerReference.Dispose()
      Debug.WriteLine("完成时间 " & Now)
   End If
End Sub

当 System.Windows.Forms.Timer 类不可用时(例如在开发控制台应用程序时),线程计时器特别有用。

取消任务

多线程的一个优点是,应用程序的用户界面部分始终可以作出响应,即使其他线程正在执行任务。同步事件和作为标志的字段通常用于通知其他线程停止。以下示例使用同步事件来取消任务。要使用本示例,请在项目中添加以下模块。要启动线程,请调用 StartCancel.StartTask() 方法。要取消一个或多个正在运行的线程,请调用 StartCancel.CancelTask() 方法。

Module StartCancel
   Public CancelThread As New System.Threading.ManualResetEvent(False)
   Public ThreadisCanceled As New System.Threading.ManualResetEvent(False)
   Private Sub SomeLongTask()
      Dim LoopCount As Integer
      Dim Loops As Integer = 10
      ' 在 While 循环中运行 10 秒钟代码,或者
      ' 直至设置了 CancelThread。
      While Not CancelThread.WaitOne(0, False) And LoopCount < Loops
         ' 此处执行某种类型的任务。
         System.Threading.Thread.Sleep(1000) ' 休眠 1 秒钟。
         LoopCount += 1
      End While
      If CancelThread.WaitOne(0, False) Then
         ' 确认设置了 ManualResetEvent CancelThread。
         ThreadisCanceled.Set()
         MsgBox("取消线程")
      Else
         MsgBox("线程运行结束")
      End If
   End Sub

   Public Sub StartTask()
      ' 启动新线程。
      Dim th As New System.Threading.Thread(AddressOf SomeLongTask)
      CancelThread.Reset()
      ThreadisCanceled.Reset()
      th.Start()
      MsgBox("线程已启动")
   End Sub

   Public Sub CancelTask()
      ' 停止任何由 StartTask 过程启动的线程。
      ' 注意,此线程同时接收和发送 
      ' 同步事件以协调线程操作。 
      CancelThread.Set()  ' 设置 CancelThread 以通知线程停止。
      If ThreadisCanceled.WaitOne(4000, False) Then
         ' 最多等待 4 秒钟,以便线程 
         ' 确认它已经停止。
         MsgBox("线程已停止。")
      Else
         MsgBox("线程无法停止。")
      End If
   End Sub
End Module

总结

多线程处理是开发具有快速响应且可扩展的应用程序的关键。Visual Basic .NET 支持强大的多线程开发模型,使用此模型,开发人员可以快速利用多线程应用程序的强大功能。

  • Visual Basic .NET 使用新的 .NET 框架类,可以方便地创建多线程应用程序。
  • 请记住,虽然多线程可以提高性能,但每个线程都需要额外的内存来创建线程,还需要处理器时间来运行线程。
  • 线程的属性和方法控制线程之间的交互,并确定正在运行的线程何时可以使用资源。
  • 尽管多线程可能会造成混乱,但可以使用同步技术来控制运行的线程。
  • 多线程通过高效地分配可用资源,提高了应用程序的可扩展性,但也增加了应用程序的复杂性。

使用本文介绍的技术可以开发出非常专业的应用程序,从而处理即使是最耗用处理器的任务。

Win10一个命令让你掌握笔记本/平板电池健康状况

使用笔记本以及平板设备的朋友应该对设备电池健康状况比较关心,Win10系统中就提供了可查看设备电池详细使用报告的方法,感兴趣的朋友可以尝试一下。其实这项功能在Win7中就已存在,在Win8中进一步完善,报告也更加详细。

• 以管理员身份运行CMD,输入下面的命令回车,系统就会在你指定的路径生成设备电池使用报告。

powercfg /batteryreport /output “C:\battery_report.html”

• 双击C:\battery_report.html会用你默认的浏览器打开该报告,一起来简单了解一下:

▲系统以及电池基本信息,包括电池设计容量、完全充电容量、充电周期

▲设备电池完全充电容量变化

▲最近三日电池使用情况记录,包括何时处于活动状态以及何时进入待机状态

▲最近三日电池使用变化曲线

▲电池续航预估值变化,这里的数据比你在任务栏电池图标上看到的要准确

这份报告会详细记录自系统安装后电池的使用详情,根据这些数据可掌握电池续航变化情况。

JS判断是否在微信浏览器打开的简单实例(推荐)

这里有新鲜出炉的 Javascript 教程,程序狗速度看过来!

JavaScript 客户端脚本语言

Javascript 是一种由 Netscape 的 LiveScript 发展而来的原型化继承的基于对象的动态类型的区分大小写的客户端脚本语言,主要目的是为了解决服务器端语言,比如 Perl,遗留的速度问题,为客户提供更流畅的浏览效果。


下面小编就为大家带来一篇 JS 判断是否在微信浏览器打开的简单实例 (推荐)。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧

最近做很多 HTML5 的项目,很多页面会通过微信微博等 SNS 分享出去。在分享页面上提供公司 APP 的下载。但是在很多应用的浏览器中,点击下载链接无法下载应用。那么针对这些浏览器我们需要给用户提示从 safari 或者系统自带的浏览器打开分享页面。通过 js 就可以判断当前页面是在什么浏览器打开的。

以下是一段示例代码,注释中表明了通过 JS 如何判断是否在微信浏览器打开,是否在 QQ 空间浏览器,是否在新浪微博打开。当然可以做得更完善一点,再加上判断是在移动设备打开还是在 PC 端浏览器打开的,更加细分一点,可以判断是在安卓系统的浏览器打开的还是 IOS 系统浏览器打开的。

  1.  
  2. if (browser.versions.mobile) {//判断是否是移动设备打开。browser代码在下面
  3. var ua = navigator.userAgent.toLowerCase();//获取判断用的对象
  4. if (ua.match(/MicroMessenger/i) == “micromessenger”) {
  5. //在微信中打开
  6. }
  7. if (ua.match(/WeiBo/i) == “weibo”) {
  8. //在新浪微博客户端打开
  9. }
  10. if (ua.match(/QQ/i) == “qq”) {
  11. //在QQ空间打开
  12. }
  13. if (browser.versions.ios) {
  14. //是否在IOS浏览器打开
  15. }
  16. if(browser.versions.android){
  17. //是否在安卓浏览器打开
  18. }
  19. } else {
  20. //否则就是PC浏览器打开
  21. }

再附上 browser 的代码,通过以下方法可以判断很多浏览器。包括判断 IE 浏览器,Opera 浏览器,苹果浏览器,谷歌浏览器,火狐浏览器等。

  1. var browser = {
  2. versions: function() {
  3. var u = navigator.userAgent,
  4. app = navigator.appVersion;
  5. return { //移动终端浏览器版本信息
  6. trident: u.indexOf(‘Trident’) > 1,
  7. //IE内核
  8. presto: u.indexOf(‘Presto’) > 1,
  9. //opera内核
  10. webKit: u.indexOf(‘AppleWebKit’) > 1,
  11. //苹果、谷歌内核
  12. gecko: u.indexOf(‘Gecko’) > 1 && u.indexOf(‘KHTML’) == 1,
  13. //火狐内核
  14. mobile: !!u.match(/AppleWebKit.*Mobile.*/),
  15. //是否为移动终端
  16. ios: !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/),
  17. //ios终端
  18. android: u.indexOf(‘Android’) > 1 || u.indexOf(‘Linux’) > 1,
  19. //android终端或uc浏览器
  20. iPhone: u.indexOf(‘iPhone’) > 1,
  21. //是否为iPhone或者QQHD浏览器
  22. iPad: u.indexOf(‘iPad’) > 1,
  23. //是否iPad
  24. webApp: u.indexOf(‘Safari’) == 1 //是否web应该程序,没有头部与底部
  25. };
  26. } (),
  27. language: (navigator.browserLanguage || navigator.language).toLowerCase()
  28. }

以上这篇 JS 判断是否在微信浏览器打开的简单实例 (推荐) 就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持 phperz。

基于JS判断iframe是否加载成功的方法(多种浏览器)

这篇文章主要介绍了基于 JS 判断 iframe 是否加载成功的方法【多种浏览器】的相关资料, 需要的朋友可以参考下

Javascript 是一种由 Netscape 的 LiveScript 发展而来的原型化继承的基于对象的动态类型的区分大小写的客户端脚本语言,主要目的是为了解决服务器端语言,比如 Perl,遗留的速度问题,为客户提供更流畅的浏览效果。

推荐阅读:

在项目中经常要动态添加 iframe,然后再对添加的 iframe 进行相关操作,而往往 iframe 还没添加完呢,后边的代码就已经执行完了,所以有些你写的东西根本没有显示出来。这时,我们就要考虑是否可以等 iframe 加载完后再执行后边的操作,当然,各种浏览器早就为我们考虑到啦,看下面:

ie 浏览器

IE 的每个 elem 节点都会拥有一个 onreadystatechange 事件,这个事件每次在 elem 内容发送变化的时候触发,比如内容正在载入 loading 会触发,内容载入完毕 loaded 会触发,内容载入成功 complete 会触发,这个函数还需要配合 readyState,这是 ie 上每个 elem 都拥有的属性,用来查看每次触发时候的状态。

  1. //先为iframe 添加一个 onreadystatechange
  2. iframe.attachEvent(“onreadystatechange”,
  3. function() {
  4. //此事件在内容没有被载入时候也会被触发,所以我们要判断状态
  5. //有时候会比较怪异 readyState状态会跳过 complete 所以我们loaded状态也要判断
  6. if (iframe.readyState === “complete” || iframe.readyState == “loaded”) {
  7. //代码能执行到这里说明已经载入成功完毕了
  8. //要清除掉事件
  9. iframe.detachEvent(“onreadystatechange”, arguments.callee);
  10. //这里是回调函数
  11. }
  12. });

其他浏览器(Firefox,Opera,chrome 等 )

在其他非 IE 的浏览器上 Firefox,Opera,chrome 等 iframe 都会拥有一个 onload 事件,此事件只要触发就说名内容已经加载完毕。

  1. iframe.addEventListener(“load”,
  2. function() {
  3. //代码能执行到这里说明已经载入成功完毕了
  4. this.removeEventListener(“load”, arguments.call, false);
  5. //这里是回调函数
  6. },
  7. false);

综合一下

  1. if (iframe.attachEvent) {
  2. iframe.attachEvent(“onreadystatechange”,
  3. function() {
  4. //此事件在内容没有被载入时候也会被触发,所以我们要判断状态
  5. //有时候会比较怪异 readyState状态会跳过 complete 所以我们loaded状态也要判断
  6. if (iframe.readyState === “complete” || iframe.readyState == “loaded”) {
  7. //代码能执行到这里说明已经载入成功完毕了
  8. //要清除掉事件
  9. iframe.detachEvent(“onreadystatechange”, arguments.callee);
  10. //这里是回调函数
  11. }
  12. });
  13. } else {
  14. iframe.addEventListener(“load”,
  15. function() {
  16. //代码能执行到这里说明已经载入成功完毕了
  17. this.removeEventListener(“load”, arguments.call, false);
  18. //这里是回调函数
  19. },
  20. false);
  21. }

注意:上面的函数必须放在 iframe 被 appendChild 到 body 后,否则不会被触发

以上内容是小编给大家介绍的 JS 判断 iframe 是否加载成功的方法,希望对大家有所帮助!

AndroidKiller:解决高版本APK编译错误

1、反编译卡死

卡死在这里,解决方法参考:
https://www.52pojie.cn/thread-658341-1-1.html

2、反编译成功,无法回编
>W: E:\移动安全\静态分析反编译\反编译工具\Androidkiller\projects\xiongdi\Project\AndroidManifest.xml:3: error: No resource identifier found for attribute ’roundIcon’ in package ‘android’
>W:
>brut.androlib.AndrolibException: brut.common.BrutException: could not exec (exit code = 1): [

android 7.1api level 25)有一个新特性,就是圆形桌面Icon,对应的是在AndroidManifest.xmlapplication节点配置:android:roundIcon=”@mipmap/ic_launcher_round” 。
反编译后,在AndroidManifest.xml文件中将此属性删除再次回编译即可。

3、apktool版本太旧
Exception in thread mainbrut.androlib.AndrolibException: Could not decode 这个问题,就是apktool.jar比较老旧的问题。
apktools.jar下载官网:https://ibotpeaches.github.io/Apktool/install/  对应平台下载,更新到apktool管理器,图片下方选择好默认的apktool版本。

逆向某直播app全程

  • 抓包

逆向原因不表。第一步当然是从抓包开始,电脑打开fiddler,设置手机wifi代理为电脑局域网ip,端口8888。打开app,抓到几个请求,其中获取直播列表请求如下:

  1. POST http://xxx.xxxx.cn/mapi/index.php HTTP/1.1
  2. Host: xxx.xxxx.cn
  3. Connection: keep-alive
  4. Accept-Encoding: gzip, deflate
  5. Accept: */*
  6. User-Agent: Dalvik/2.1.0 (Linux; U; Android 7.1.1; E6683 Build/32.4.A.1.54)
  7. X-JSL-API-AUTH: sha1|1515724202|pRQCJVbTdAI6|e91cd7a62631b4426175b2ca4f77948f0f350fd9
  8. Cookie: client_ip=143.97.168.92; user_id=464890; __jsluid=c13ebd1234964c11ds9802a5b24sd6afc; nick_name=%e7%bb%ad%e5%91%bd; $Path=/; user_pwd=0091ea2a881c56fae1sece697510ec80; PHPSESSID2=9eirdal3p5q05ppdkj7uj767h5; PHPSESSID=5rdo1e11mk3bjctoaaeksmun63; $Domain=69.9vtao.cn
  9. Content-Length: 342
  10. Content-Type: application/x-www-form-urlencoded
  11. itype=&i_type=1&sdk_version_name=2.5.12&requestData=apwlYyV6t85HEc7wCgrb41qkcD0E%2F%2BMKEQcyaZzX0Xsob1xZKtUfh5P6PIGRHOPzocvGjqyDRcsW11TC7ZCJwRgLMmmew5%2FKTmYY9M3Pl8u2ohlBQRCkuR4YmL9%2FzTIQt2ieybEsLPh3Fa2ZC2EBmJwNqq0NtO3L5%2Fq2nxi%2FzX4L2xzFsL5dnJHMLD4jQxwFwWJs98OWS91ImakLf7MXqIhOoZEhslBzqHkVtFRq4TMHMhdgmQPyGaoRTXEXd%2BRR&act=index&ctl=index

出于隐私考虑,Host及Cookie值并非实际值,有修改。

响应(output字段太长,已省略。):

  1. HTTP/1.1 200 OK
  2. Date: Fri, 12 Jan 2018 02:25:12 GMT
  3. Content-Type: text/html; charset=utf-8
  4. Connection: keep-alive
  5. Vary: Accept-Encoding
  6. Vary: Accept-Encoding
  7. Cache-Control: no-cache, no-store, max-age=0, must-revalidate
  8. X-Cache: bypass
  9. Content-Length: 128941
  10. {"output":"5x2AZX2LDCzk2qFiaD1E4UmfLy1LzPnHKMuEHGkthendVDWGGuJZP16YLxvyhhIsKu7T\/4IRa0S1cc3pJRf0UZsiBO1aNHjw4P3eCYxH2AX0GQRfW39pmQhCdOj2klek..."}

很显然,请求和响应加密了,只能反编译app寻找加密方法。

  • 反编译

使用apktool解包apk,得到资源文件和classes.dex文件,使用dex2jar工具反编译得到jar包,使用jd-gui工具查看并导出jar包源码,使用编辑器或ide浏览源码。apk没有加固,顺利拿到源码。

jd-gui需要java8环境,java9会出错,真替java捉急。

尝试全局搜索请求参数关键字"requestData"(不加引号结果太多,加引号就只搜索到出现该字符串的代码了),找到AppHttpUtil.java关键部分代码如下,参数跟抓到的相符,说明发起请求的逻辑正是此处。

  1. for (localObject1 = localObject2;; localObject1 = AESUtil.encrypt(SDJsonUtil.object2Json(localObject1), ApkConstant.getAeskey()))
  2. {
  3. if (i != 0)
  4. {
  5. localRequestParams.addBodyParameter("requestData", (String)localObject1);
  6. localRequestParams.addBodyParameter("i_type", String.valueOf(i));
  7. localRequestParams.addBodyParameter("ctl", (String)localObject3);
  8. localRequestParams.addBodyParameter("act", str);
  9. localRequestParams.addBodyParameter("itype", AppRequestParams.getItypeParams());
  10. localRequestParams.addBodyParameter("sdk_version_name", SDPackageUtil.getVersionName());
  11. }
  12. // 省略
  13. }

AESUtil.encrypt显示这是一个AES加密,找到AESUtil.encrypt方法的代码:

查看AppHttpUtil.java文件顶部的import xxx.AESUtil行,按照import路径即可找到AESUtil.java文件。下面跳转到代码都用这个方法,如果没有import 这行,说明这个类跟当前类在同一个目录下。

  1. public static String encrypt(String paramString1, String paramString2)
  2. {
  3. Object localObject2 = null;
  4. Object localObject1 = null;
  5. try
  6. {
  7. paramString1 = paramString1.getBytes("UTF-8");
  8. paramString2 = new SecretKeySpec(paramString2.getBytes("UTF-8"), "AES");
  9. Cipher localCipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
  10. localCipher.init(1, paramString2);
  11. paramString1 = localCipher.doFinal(paramString1);
  12. paramString2 = (String)localObject1;
  13. if (paramString1 != null) {
  14. paramString2 = Base64.encodeToString(paramString1, 0);
  15. }
  16. return paramString2;
  17. }
  18. // 省略
  19. }

AES加密方式为AES/ECB/PKCS5Padding,再看上面的调用方式,加密key为ApkConstant.getAeskey(),找到该类:

  1. public static final String AES_KEY = SDResourcesUtil.getString(2131165252) + "000000";
  2. private static String AES_KEY_DYNAMIC = "";
  3. public static String getAeskey()
  4. {
  5. if (!TextUtils.isEmpty(AES_KEY_DYNAMIC)) {
  6. return AES_KEY_DYNAMIC;
  7. }
  8. return AES_KEY;
  9. }

这里使用了AES_KEY_DYNAMIC字段,如果没有则使用默认的AES_KEY字段,说明服务端可能会动态更新AES_KEY_DYNAMIC。我们先用默认的AES_KEY试试可不可行。

AES_KEY获取方式为SDResourcesUtil.getString(2131165252),根据名字猜测是获取资源文件值,2131165252是资源id,全局搜索一下这个id,在R.java中找到下面一行定义:

由于编译优化的原因,常量名会直接替换为本来的数值。方法的局部变量命名很奇怪也是如此,编译后不存在局部变量的名字,名字都是反编译工具生成的。

  1. public static final int app_id_tencent_live = 2131165252;

百度搜索一下R.java,出来的是安卓资源文件相关的结果,说明这个值在资源文件中定义。打开解包apk后的res文件夹,全局搜索app_id_tencent_live,在strings.xml文件中找到了该值:

  1. <string name="app_id_tencent_live">1400057185</string>

那么AES_KEY="1400057185" + "000000",也即AES加密KEY为1400057185000000

  • AES加密

找一个在线AES加密网站验证一下结果是否正确,设置参数如下:

  1. 加密模式:ECB
  2. 填充:pkcs5padding
  3. 密码:1400057185000000
  4. 字符集: utf8

粘贴上面抓包请求中的requestData字段值,注意上面是urlencode后的,可以直接从fiddler的WebFroms视图里复制原始值。点击解密,得到结果如下:

  1. {"screen_width": 1080, "sdk_type": "android", "sdk_version_name": "2.5.12", "sex": 0, "p": 1, "sdk_version": 2017122401, "cate_id": 0, "act": "index", "ctl": "index", "screen_height": 1776}

说明我们的猜测是对的,通信协议加密搞定。粘贴响应结果的output字段,点击解密,同样得到了原文(结果这里就不贴了,是一个json结构,返回了直播列表,包含房间id等信息)。

观察该请求的参数,并没有什么变量(screen_width跟设备相关,sdk_version跟app版本相关,app升级后可能会变),直接照抄参数即可模拟该请求。

python可使用pycrypto库实现AES加/解密,由于该库安装需要编译,而且不自动处理padding,比较麻烦,这里直接使用上面AES网站的API来做AES加/解密操作。fiddler抓包如下:

  1. Method:
  2. POST http://tool.chacuo.net/cryptaes
  3. WebForms:
  4. type: aes
  5. data: test
  6. arg: m=ecb_pad=pkcs5_block=128_p=1400057185000000_o=0_s=utf-8_t=0
  7. Returns:
  8. {"status":1,"info":"ok","data":["3KLa9xdRIRq4WVy7Sr3\/Ew=="]}

解密协议相同,只是arg参数变为以t=1结尾。python调用代码如下:

  1. def aes_encrypt(text):
  2. params = {
  3. 'data': text,
  4. 'type': 'aes',
  5. 'arg': 'm=ecb_pad=pkcs5_block=128_p=%s_i=_o=0_s=utf-8_t=0' % conf['aeskey']
  6. }
  7. r = requests.post('http://tool.chacuo.net/cryptaes', data=params)
  8. return r.json()['data'][0]
  9. def aes_decrypt(text):
  10. params = {
  11. 'data': text,
  12. 'type': 'aes',
  13. 'arg': 'm=ecb_pad=pkcs5_block=128_p=%s_i=_o=0_s=utf-8_t=1' % conf['aeskey']
  14. }
  15. r = requests.post('http://tool.chacuo.net/cryptaes', data=params)
  16. return r.json()['data'][0]
  • sign签名

接下来寻找获取房间信息的请求,先在手机上请求,fiddler抓包,报文跟直播列表请求结构相似,解密requestData得到原文:

  1. {"screen_width": 1080, "sdk_type": "android", "sdk_version_name": "2.5.12", "sign": "65c65aefb02040eebca7127576c47a2c", "is_vod": 0, "sdk_version": 2017122401, "room_id": 172084, "act": "get_video2", "ctl": "video", "screen_height": 1776}

解密响应包,内容是房间的具体信息,包含了一个flv链接的直播流,使用支持流媒体的播放器打开这个链接可直接观看。

注意多了一个sign字段,有签名验证,没有想象中顺利。尝试搜索请求参数get_video2(其他重复率可能比较低的参数也行,多尝试),找到CommonInterface.java代码如下:

  1. public static SDRequestHandler requestRoomInfo(int paramInt1, int paramInt2, String paramString, AppRequestCallback<App_get_videoActModel> paramAppRequestCallback)
  2. {
  3. AppRequestParams localAppRequestParams = new AppRequestParams();
  4. localAppRequestParams.putCtl("video");
  5. localAppRequestParams.putAct("get_video2");
  6. localAppRequestParams.put("room_id", Integer.valueOf(paramInt1));
  7. localAppRequestParams.put("is_vod", Integer.valueOf(paramInt2));
  8. localAppRequestParams.put("private_key", paramString);
  9. paramString = AppRuntimeWorker.getSdkappid();
  10. String str = AppRuntimeWorker.getLoginUserID();
  11. localAppRequestParams.put("sign", MD5Util.MD5(paramString + str + paramInt1));
  12. return AppHttpUtil.getInstance().post(localAppRequestParams, paramAppRequestCallback);
  13. }

可以看到sign参数为MD5Util.MD5(paramString + str + paramInt1)
strAppRuntimeWorker.getLoginUserID(),根据名字知道这是用户id,用户id当然在cookie中,查看最开始抓到的请求中,cookie头有user_id字段,就是该值。
结合localAppRequestParams.put("room_id", Integer.valueOf(paramInt1))这行,知道paramInt1为room_id,即房间id,在直播列表请求返回的结果中,有room_id字段。
paramStringAppRuntimeWorker.getSdkappid()

转到AppRuntimeWorker.getSdkappid方法代码:

  1. public static String getSdkappid()
  2. {
  3. InitActModel localInitActModel = InitActModelDao.query();
  4. if (localInitActModel == null) {
  5. return null;
  6. }
  7. return localInitActModel.getSdkappid();
  8. }

sdkappid是从InitActModel中读取的,找到InitActModel类,发现是一个纯数据类,sdkappid是外部设置的。尝试全局搜索setSdkappid方法,找到设置该值的地方,结果略多,没有找到有用的代码。

变换思路,查看InitActModelDao代码:

  1. public class InitActModelDao
  2. {
  3. public static void delete()
  4. {
  5. JsonDbModelDao.getInstance().delete(InitActModel.class);
  6. }
  7. public static boolean insertOrUpdate(InitActModel paramInitActModel)
  8. {
  9. boolean bool = JsonDbModelDao.getInstance().insertOrUpdate(paramInitActModel);
  10. HostManager.getInstance().saveActHost();
  11. return bool;
  12. }
  13. public static InitActModel query()
  14. {
  15. return (InitActModel)JsonDbModelDao.getInstance().query(InitActModel.class);
  16. }
  17. }

看上去是orm代码,继续查看JsonDbModelDao代码,从代码风格可以确定是orm了。代码不多,看到该行:

  1. DbManagerX.getDb().selector(JsonDbModel.class)

说明数据库初始化部分在DbManagerX类里,查看DbManagerX代码,发现下面这行:

  1. private static final DbManager.DaoConfig configDefault = new DbManager.DaoConfig().setDbName("fanwe.db")

数据库文件为fanwe.db,应该是一个sqlite数据库。可能是内置于apk里的,去apk解包后的文件夹里查找该文件,并没有找到。那就说明数据库文件是apk运行后创建的,那么sdkappid也可能是动态设置的,由服务端返回。

回到fiddler,查看之前抓到的请求,对每个请求结果都解密,查找sdkappid关键字。在一个包含ctl=app_init参数的请求中找到了该值"sdkappid": "1400057185",结果跟上面的aes_key是同一个值,what the fuck…

这里也可以直接去手机里找fanwe.db文件, 但是我用的手机并未root,无法查看app私有数据目录。实在找不到的情况下,可以用安卓虚拟机去运行app再找到数据库文件。

现在用于签名的三个参数都已经找到,我们使用上面的room_id 172084,结合userid和sdkappid,找个在线的md5网站,计算md5值,得到结果65c65aefb02040eebca7127576c47a2c,跟sign字段一致,签名验证搞定。本来还以为它的MD5Util.md5方法还要在加个什么key之类的…

现在房间信息请求也搞定了,然而我看到了请求头有一个X-JSL-API-AUTH的特殊的头,就知道事情并没有那么简单。

  • HTTP头

抓包请求中有如下一个http头,使用fiddler的Reissue and Edit功能重发请求,并编辑,去掉该头,发现返回了一个html页面,提示请求被拦截,说明该头是用来验证请求的有效性。

  1. X-JSL-API-AUTH: sha1|1515583942|EgloVUHapki7|e4abc467fdfb98d72c0c881329b1450c301b2d26

发现每次请求这个头的值都不一样,并且第二个参数是一串数字,看起来是时间戳,猜测这个头每次请求会生成,并且具有有效期。特意等待几分钟后,使用fiddler重新发起该请求,返回结果提示拦截,果然如此。

在源码中搜索X-JSL-API-AUTH关键字,找到关键代码:

  1. Object localObject1 = new StringBuilder(paramSDRequestParams.getUrl());
  2. localRequestParams.addHeader("X-JSL-API-AUTH", getToken(((StringBuilder)localObject1).toString()));
  3. //省略
  4. public static String getToken(String paramString)
  5. {
  6. String str = getStringRandom();
  7. long l = getSysTime();
  8. paramString = getStrToSHA("sha1|09969cd379a84835b6fece3c8d327bf1|" + l + "|" + str + "|" + paramString);
  9. return "sha1|" + l + "|" + str + "|" + paramString;
  10. }

实现逻辑比较简单,拼接字符串sha1|09969cd379a84835b6fece3c8d327bf1|、时间戳、一个随机字符串、要请求的url得到加密用原文,求sha1值,作为签名,然后拼接sha1、时间戳、随机字符串、以及签名即可得到该头部值。

python代码实现如下:

  1. def token():
  2. l = str(int(time.time()) + 300)
  3. string = ''.join(random.sample('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', 12))
  4. sign = sha1("sha1|09969cd379a84835b6fece3c8d327bf1|" + l + "|" + string + "|" + conf['url']).hexdigest()
  5. return "sha1|" + l + "|" + string + "|" + sign

事后发现,这里有个坑,脚本在我电脑上运行正常,在服务器上运行一直返回请求被拦截,开始以为是ip或cookie的问题,调试了好久发现,上面签名用的时间戳不是当前时间,而是失效时间。我自己电脑的时间比较快,所以坑了…解决方法也很简单,取当前时间加上5分钟就好。

猜测很可能会验证User-Agent头,试了下果然如此, 请求需要伪造User-Agent头。使用fiddler抓到的手机发出的请求的User-Agent即可。

  1. User-Agent: Dalvik/2.1.0 (Linux; U; Android 7.1.1; E6683 Build/32.4.A.1.54)
  • 模拟

至此,请求已分析完毕,可以编码模拟了。待填坑。。

Android逆向之旅—分析某直播App的协议加密原理以及调用加密方法进行协议参数构造

一、前言随着直播技术火爆之后,各家都出了直播app,早期直播app的各种请求协议的参数信息都没有做任何加密措施,但是慢慢的有人开始利用这个后门开始弄刷粉关注工具,可以让一个新生的小花旦分分钟变成网红。所以介于这个问题,直播App开始对网络请求参数做了加密措施。所以就是本文分析的重点。逆向领域不仅只有脱壳操作,一些加密解密操作也是很有研究的目的。
二、抓包查看加密协议本文就看一款直播app的协议加密原理,以及如何获取加密之后的信息,我们如何通过探针技术,去调用他的加密函数功能。首先这里找突破点,毋庸置疑,直接抓包即可,我们进入主播页面,点击关注之后,看Fiddler中的抓包数据:

我们会发现,请求参数中,有些重要信息,比如用户的id,设备的imei值等。不过最重要也是我们关心的就是s_sg字段了,因为这个字段就是请求参数信息的一个签名信息。也是服务端需要进行比对的信息。如果发现不对或者没这个字段,那么就认为这次请求操作是非法的。所以我们只要得到这个字段的正确值,才能模拟访问此次操作的网络请求。
三、分析加密流程找到突破口了,就是这个字段s_sg,用Jadx打开app的dex文件,打开dex文件之后,全局搜索s_sg字符串:

这里看到只有一处出现了这个字段值,我们点进去查看:

看到这个方法,很兴奋,感觉就是加密协议的功能,而且字段都能对上,我们把这个加密方法直接拷贝出来,写一个demo之后模拟加密之后的信息,悲伤的是发现数据是对不上的,而且看看这个方法所在的类:

尽然是一个和网页交互的类,说明应该不是这个地方进行请求加密了。这个突破口就断了。注意:这里说的是dex文件,不是apk文件,因为Jadx打开apk文件会解析资源文件,如果一个app有很多资源文件,那么Jadx打开就会卡死,所以很多同学问我为什么Jadx打开apk文件就出现卡死状态,主要是因为解析资源文件导致的。所以为了防止卡死,直接解压出dex文件,然后打开就不会卡死了。

我们继续上面的抓包信息,就是请求的url地址,再去Jadx全局搜索:

找到了,点进去查看:

然后全局搜索这个”USER_RELATION_FOLLOW”字符串:

搜到结果,点进去进行查看:

这里看到了,用了注解方式来构造请求信息,而这里的核心类就是InkeDefaultBuilder,全局搜索这个类:

可惜没搜到,因为这个app进行拆包操作,有多个dex文件:

所以我们需要用Jadx继续打开他的第二个dex文件进行搜索:

果然,在第二个dex中找到了这个类。注意:这里需要注意,对于Jadx打开dex或者apk文件之后,跟踪发现找不到一些搜索内容的时候,需要有如下猜想:第一、是否包含多个dex文件,可以利用Jadx去打开其他dex文件进行搜索。第二、是否存在动态加载插件功能,全局搜索DexClassLoader找到插件加载位置,获取插件功能包,在用Jadx打开插件包进行搜索。第三、是否存在so文件中,可以利用IDA打开so文件,Shift+F12展示so中所有的字符串信息视图,然后进行搜索。第四、是否信息来源于网络请求返回,比如一些字符串信息展示,有可能是服务器返回的信息。
继续分析,点进去查看这个类的定义:

查看他的父类信息:

看到有一个url加密的方法,比较好奇。我们查看这个方法:

继续查看这个方法:


这里发现有一个网络请求,会发现一些信息,然后设置到一个地方。我们继续看方法:

看到d变量的定义类型,一般我们看到不可点击的可能这个类不在这个dex文件中了,所以我们需要去另外一个dex文件进行查找,而本文案例就是来回这么折腾查找信息操作的,去另外一个dex文件中进行搜索:

查找到了,点进去查看:

这里又看到是一个a变量,看看他的定义:

看到,这里这个类又不可以点击,说明这个类不在这个dex文件中,去另外一个dex文件中进行搜索:

发现这个类的定义了,点进去进行查看:

原来是一个native的工具类,内部有很多native方法,包括了设置信息的方法,加密解密url方法等。看看他加载的so是什么:

原来是这三个so文件,看到crypto和ssl,弄过加密的知道,这个是openssl加密的库文件,这里猜想他在native层用了这个加密算法了。先不管,我们用IDA打开这个so文件,因为我们知道libcrypto和libssl这两个是库文件,所以直接打开它自己的libsecret文件吧,然后Shift+F12查看他的字符串信息页面,在之前不是想看看那个加密字段,这里搜索看看结果:

的确找到了,那个加密的字段了,我们点击进入查看:

然后点击X键,查看调用地方,不过可惜的是,跳转过去之后发现,那个汇编代码不是一般的多。这里先不去看了,回过头来,看看那个加密url的函数:

点击进去,然后按F5查看对应的C代码:

这时候就要开始怀疑人生了,IDA卡死了。然后简单看一下这个函数的汇编代码,简直蒙圈。太长了。如果靠静态分析,我是没这个耐心和能力了。动态调试?我觉得也够呛,搞不好还有反调试,各种跳转。不知道调试到猴年马月。
四、获取加密内容那么到这里,我大致分析这个直播app的请求协议有一个加密签名的字段s_sg,这个值是在native层中进行加密操作的,采用openssl进行加密,但是加密函数非常长,分析难度加大。但是不能就这样放弃了。我们想要这个加密结果,用于我们自己构造参数之后获取正确的签名信息值。那么就需要转化思维了。我们或许只要结果,加密过程对我们来说并不那么重要。所以这里的一个思路:自己写一个程序调用它的so文件中的这个加密函数。
我们做过NDK开发的都知道,默认情况native方法在so中的JNI_OnLoad函数自动注册,但是native中的函数必须按照这种格式:Java_类名_方法名,类似这样:

那么我们就可以在自己的程序中,把app的那个Secret类拷贝过去,不过一定要注意包名一定不能变:

然后在代码中直接调用native方法:

不过可惜的是,调用的结果,没有加密字段信息。所以这里我们会发现应该还缺少什么设置。我们回过头看看java层的代码:

这里有一个set方法,在之前分析已经发现了,他的调用地方:

这里有一个SecretDataModel类,应该是从网上请求获取到的数据,然后解析构造出这个类,看看这个类定义:

看到这里,发现已经用了第三方的json解析包,注解直接解析字段值。但是我们全局搜索这个类的话,跟踪太麻烦了,这里就采用另外一种方式跟踪代码,那就是利用Xposed拦截这个类的构造方法,然后在内部打印堆栈信息来查看方法的调用路径,这种方法我在之前已经用过了。本文能够更好的体现:

因为应用是多dex文件,所以hook必须先hook他的Application类的attach方法拿到正确的类加载器,正确加载需要hook的类信息,不然就报错了。这个已经讲了很多遍了。然后就是利用自动抛异常来打印方法的调用堆栈信息,安装运行,重启生效看日志:

这里看到了,他用google的gson库解析json数据的,看到了json数据是从下面这两个方法中传递过来的,查看这个类的方法:

点进去进行查看:

为了看到返回的json数据,我们在拦截这个方法,把返回的json数据打印出来看看是啥:

运行模块,重启生效,看日志信息:

看到这段json数据了,这时候,在返回去看看SecretDataModel那个调用set方法:

看到这四个参数值,第一个是Context不解释了,第二个是serverTime字段值,第三个是startCode字段值,第四个是runCode字段值。这三个字段都是可以在上面打印的json中找到的,我们把json格式化看看:

有了这三个值和Context变量,直接调用Secret的native方法set进行设置,看看能否正确获取到加密之后的字段值:

运行demo程序看看日志信息:

看到了,设置成功了,而且获取加密字段也成功了。到这里我们就成功的获取到加密字段s_sg值了。
五、获取加密配置信息不过到这里还没有结束,因为我们发现那三个set值字段从网络获取,我们还需要知道是哪个url获取到的,这里从代码跟踪依然很困难,所以我们还需要利用hook来打印方法的堆栈信息来追踪代码,通过上面打印的获取json数据的堆栈信息:

看到是这个类访问获取json数据的,进去看看:

而传入的参数,在进行查看:

看到有一个getUrl方法,就是获取访问的url值,我们就可以这么来进行hook操作,打印这个url访问地址了:

运行模块,重启生效,看日志信息:

下面打印了那个我们想要的json数据,上面有几个url,我们在去Fiddler观察这几个url返回的数据,最后定位到这个是这个url返回的数据信息:

六、梳理加密流程到这里我们就分析完了直播app的协议加密流程,下面来总结一下:第一步:通过/user/account/token_v2接口获取加密前的配置信息第二步:通过native方法把第一步获取到的信息进行设置(set方法)。第三步:通过native方法将java层拼接参数的url值进行加密处理返回(encryUrl方法)那么我们可以这么做,自己协议demo程序,在Java层拼接好参数,然后调用so的native方法,返回还有加密字段的url,然后可以解析出这个字段就是加密信息了。我们就可以批量处理这种网络请求了。比如刷粉,观看直播,点赞等操作。当然有的同学会认为这次操作其实不是真正意义上的破解加密算法。的确不算,但是我们弄到我们想要的就好,过程其实没那么重要,而在这个操作过程中,我们又学习到了很多逆向技巧。
七、逆向技巧总结第一、在用Jadx打开apk卡死的时候,记得先解压出dex文件,在用Jadx打开dex文件即可。第二、对于多dex的应用,使用Xposed进行hook的时候,需要先拦截Application类的attach方法获取正确的类加载器。不然会报拦截失败。第三、当我们在使用Jadx进行全局搜索内容,发现没有搜索结果的时候,可能需要从以下几个方面考虑:1、是否包含多个dex文件,可以利用Jadx去打开其他dex文件进行搜索。2、是否存在动态加载插件功能,全局搜索DexClassLoader找到插件加载位置,获取插件功能包,在用Jadx打开插件包进行搜索。3、是否存在so文件中,可以利用IDA打开so文件,Shift+F12展示so中所有的字符串信息视图,然后进行搜索。4、是否信息来源于网络请求返回,比如一些字符串信息展示,有可能是服务器返回的信息。

第四、在我们用Jadx进行代码跟踪非常困难的时候,记得还可以利用Xposed拦截指定方法,打印堆栈信息来跟踪代码,也是一种高效方法。第五、当你在使用Jadx右键一个方法看看调用路径,发现没有结果的时候,那么看看这个方法是否是该类实现的一个接口中的方法或者是抽象方法。去父类或者接口中的那个方法在右键看看调用路径。第六、在不关心过程,只关心结果的场景下,可以构造一个app来调用程序的so,获取我们想要的结果,这种方式一定要记住,后面很多场景都会用到。可以用它来嗅探so中一些函数的功能。比如通过调用so中的一个方法,输入规律数据,看输出结果是否符合一定规律,通过规律来破解加密算法。
八、总结本文介绍的内容有点多,感谢该直播app开发者提供这么好的研究样本,当然最后需要说一句就是:安全防护策略不够,我们在本文可以看到我们利用调用他的so来获取加密信息,这个方法是可以用于很多app的,对于那些我们无需关心过程,只关心结果的内容,这种方法屡试不爽。那么作为开发者如何规避这种安全问题呢?很多人第一就想到了:在so的JNI_OnLoad方法中判断签名信息是否正确,不正确就直接退出。这个的确可以防范。但是如果用我之前介绍的kstools工具原理,直接hook系统的PMS服务拦截获取签名信息方法,返回正确的应用签名信息,这种防护策略就失效了。所以说:安全不息,逆向不止。两者都在进步。
严重声明本文介绍的内容只是为了逆向技术探讨,绝对不允许不法分子进行恶意用途。如果带来任何法律问题均与本文作者无关,由操作者本人承担,而涉及到安全隐患,技术交流可以加入编码美丽技术圈