Android开发中使用多线程实现断点下载
1.使用多线程实现文件下载...
多线程下载是加快下载速度的一种方式..通过开启多个线程去执行一个任务..可以使任务的执行速度变快..多线程的任务下载时常都会使用得到..比如说我们手机内部应用宝的下载机制..一定是通过使用了多线程创建的下载器..并且这个下载器可以实现断点下载..在任务被强行终止之后..下次可以通过触发按钮来完成断点下载...那么如何实现断点下载这就是一个问题了..
首先我们需要明确一点就是多线程下载器通过使用多个线程对同一个任务进行下载..但是这个多线程并不是线程的数目越多,下载的速度就越快..当线程增加的很多的时候,单个线程执行效率也会变慢..因此线程的数目需要有一个限度..经过楼主亲自测试..多线程下载同一个任务时,线程的数目5-8个算是比较高效的..当线程的数量超过10个之后,那么多线程的效率反而就变慢了(前提:网速大体相同的时候..)
那么在实现多线程下载同一个任务的时候我们需要明白其中的道理..下面先看一张附加图..
这个图其实就很简单的说明了其中的原理..我们将一个任务分成多个部分..然后开启多个线程去下载对应的部分就可以了..那么首先要解决的问题就是如何使各自的线程去下载各自对应的任务,不能越界..那么我们来看看具体的实现过程..首先我们使用普通Java代码去实现..最后将Java代码移植到Android就可以了..
public class Download { public static int threadCount = 5; //线程开启的数量.. public static void main(String[] args) { // TODO Auto-generated method stub String path = "http://192.168.199.172:8080/jdk.exe"; try { URL url = new URL(path); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setConnectTimeout(5000); int status = conn.getResponseCode(); if(status == 200){ int length = conn.getContentLength(); System.out.println(length); int blocksize = length/threadCount; //将文件长度进行平分.. for(int threadID=1; threadID<=threadCount;threadID++){ int startIndex = (threadID-1)*blocksize; //开始位置的求法.. int endIndex = threadID*blocksize -1; //结束位置的求法.. /** * 如果一个文件的长度无法整除线程数.. * 那么最后一个线程下载的结束位置需要设置文件末尾.. * */ if(threadID == threadCount){ endIndex = length; } System.out.println("线程下载位置:"+startIndex+"---"+endIndex); } } } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
这样只是实现了通过连接服务器来获取文件的长度..然后去设置每一个线程下载的开始位置和结束位置..这里只是完成了这些步骤..有了下载的开始位置和结束位置..我们就需要开启线程来完成下载了...因此我们需要自己定义下载的过程...
首先我们需要明确思路:既然是断点下载..那么如果一旦发生了断点情况..我们在下一次进行下载的时候需要从原来断掉的位置进行下载..已经下载过的位置我们就不需要进行下载了..因此我们需要记载每一次的下载记录..那么有了记录之后..一旦文件下载完成之后..这些记录就需要被清除掉...因此明确了这两个地方的思路就很容易书写了..
//下载线程的定义.. public static class DownLoadThread implements Runnable{ private int threadID; private int startIndex; private int endIndex; private String path; public DownLoadThread(int threadID,int startIndex,int endIndex,String path){ this.threadID = threadID; this.startIndex = startIndex; this.endIndex = endIndex; this.path = path; } @Override public void run() { // TODO Auto-generated method stub try { //判断上一次是否下载完毕..如果没有下载完毕需要继续进行下载..这个文件记录了上一次的下载位置.. File tempfile =new File(threadID+".txt"); if(tempfile.exists() && tempfile.length()>0){ FileInputStream fis = new FileInputStream(tempfile); byte buffer[] = new byte[1024]; int leng = fis.read(buffer); int downlength = Integer.parseInt(new String(buffer,0,leng));//从上次下载后的位置开始下载..重新拟定开始下载的位置.. startIndex = downlength; fis.close(); } URL url = new URL(path); HttpURLConnection conn =(HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setConnectTimeout(5000); conn.setRequestProperty("Range", "bytes="+startIndex+"-"+endIndex); int status = conn.getResponseCode(); //206也表示服务器响应成功.. if(status == 206){ //获取服务器返回的I/O流..然后将数据写入文件当中.. InputStream in = conn.getInputStream(); //文件写入开始..用来保存当前需要下载的文件.. RandomAccessFile raf = new RandomAccessFile("jdk.exe", "rwd"); raf.seek(startIndex); int len = 0; byte buf[] =new byte[1024]; //记录已经下载的长度.. int total = 0; while((len = in.read(buf))!=-1){ //用于记录当前下载的信息.. RandomAccessFile file =new RandomAccessFile(threadID+".txt", "rwd"); total += len; file.write((total+startIndex+"").getBytes()); file.close(); //将数据写入文件当中.. raf.write(buf, 0, len); } in.close(); raf.close(); } } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); }finally{ //如果所有的线程全部下载完毕后..也就是任务完成..清除掉所有原来的记录文件.. runningThread -- ; if(runningThread==0){ for(int i=1;i<threadCount;i++){ File file = new File(i+".txt"); file.delete(); } } } } }
这样就完成了文件的数据信息的下载..经过测试..一个13M的文件在5个线程共同作用下下载的时间差不多是12秒左右(网速稳定在300k的情况下..带宽越宽..速度就会更快)单个线程下载的时间差不多是15秒左右..这里才缩短了两秒钟的时间..但是我们不要忘记..如果文件过大的话呢?因此楼主亲测了一下一个90M的文件在5个线程同时作用下时间差不多1分20秒左右..而使用一个线程进行下载差不多2分钟左右..这里还是缩短了大量的时间..
因此根据对比,还是使用多个线程来进行下载更加的好一些..虽然Android里的一般应用不会超过50M左右..但是游戏的话一般差不多能达到100-200M左右..因此使用多线程还是能够提高下载的进度和效率..同样我们可以通过使用线程池的方式去开启线程..最后这些线程交给线程池去管理就可以了..
在正常的Java项目中我们书写好了下载代码..就可以移植到我们的Android应用程序当中..但是还是有一些地方需要注意..因此在这里去强调一下..
package com.example.mutithread; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.io.RandomAccessFile; import java.net.HttpURLConnection; import java.net.URL; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.app.Activity; import android.text.TextUtils; import android.view.Menu; import android.view.View; import android.widget.EditText; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; public class MainActivity extends Activity { private EditText et; private ProgressBar pb; public static int threadCount = 5; public static int runningThread=5; public int currentProgress=0; //当前进度值.. private TextView tv; private Handler handler = new Handler(){ @Override public void handleMessage(Message msg){ switch (msg.what) { case 1: Toast.makeText(getApplicationContext(), msg.obj.toString(), 0).show(); break; case 2: break; case 3: tv.setText("当前进度:"+(pb.getProgress()*100)/pb.getMax()); default: break; } } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); et = (EditText) findViewById(R.id.et); pb =(ProgressBar) findViewById(R.id.pg); tv= (TextView) findViewById(R.id.tv); } public void downLoad(View v){ final String path = et.getText().toString().trim(); if(TextUtils.isEmpty(path)){ Toast.makeText(this, "下载路径错误", Toast.LENGTH_LONG).show(); return ; } new Thread(){ // String path = "http://192.168.199.172:8080/jdk.exe"; public void run(){ try { URL url = new URL(path); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setConnectTimeout(5000); int status = conn.getResponseCode(); if(status == 200){ int length = conn.getContentLength(); System.out.println("文件总长度"+length); pb.setMax(length); RandomAccessFile raf = new RandomAccessFile("/sdcard/setup.exe","rwd"); raf.setLength(length); raf.close(); //开启5个线程来下载当前资源.. int blockSize = length/threadCount ; for(int threadID=1;threadID<=threadCount;threadID++){ int startIndex = (threadID-1)*blockSize; int endIndex = threadID*blockSize -1; if(threadID == threadCount){ endIndex = length; } System.out.println("线程"+threadID+"下载:---"+startIndex+"--->"+endIndex); new Thread(new DownLoadThread(threadID, startIndex, endIndex, path)).start() ; } } } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } }; }.start(); } /** * 下载线程.. * */ public class DownLoadThread implements Runnable{ private int ThreadID; private int startIndex; private int endIndex; private String path; public DownLoadThread(int ThreadID,int startIndex,int endIndex,String path){ this.ThreadID = ThreadID; this.startIndex = startIndex; this.endIndex = endIndex; this.path = path; } @Override public void run() { // TODO Auto-generated method stub URL url; try { //检查是否存在还未下载完成的文件... File tempfile = new File("/sdcard/"+ThreadID+".txt"); if(tempfile.exists() && tempfile.length()>0){ FileInputStream fis = new FileInputStream(tempfile); byte temp[] =new byte[1024]; int leng = fis.read(temp); int downlength = Integer.parseInt(new String(temp,0,leng)); int alreadydown = downlength -startIndex; currentProgress += alreadydown;//发生断点之后记录下载的文件长度.. startIndex = downlength; fis.close(); } url = new URL(path); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setRequestProperty("Range", "bytes="+startIndex+"-"+endIndex); conn.setConnectTimeout(5000); //获取响应码.. int status =conn.getResponseCode(); if(status == 206){ InputStream in = conn.getInputStream(); RandomAccessFile raf =new RandomAccessFile("/sdcard/jdk.exe", "rwd"); //文件开始写入.. raf.seek(startIndex); int len =0; byte[] buffer =new byte[1024]; //已经下载的数据长度.. int total = 0; while((len = in.read(buffer))!=-1){ //记录当前数据下载的长度... RandomAccessFile file = new RandomAccessFile("/sdcard/"+ThreadID+".txt", "rwd"); raf.write(buffer, 0, len); total += len; System.out.println("线程"+ThreadID+"total:"+total); file.write((total+startIndex+"").getBytes()); file.close(); synchronized (MainActivity.this) { currentProgress += len; //获取当前总进度... //progressBar progressDialog可以直接在子线程内部更新UI..由于源码内部进行了特殊的处理.. pb.setProgress(currentProgress); //更改界面上的进度条进度.. Message msg =Message.obtain(); //复用以前的消息..避免多次new... msg.what = 3; handler.sendMessage(msg); } } raf.close(); in.close(); System.out.println("线程:"+ThreadID+"下载完毕"); }else{ System.out.println("线程:"+ThreadID+"下载失败"); } } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); Message msg = new Message(); msg.what = 1; msg.obj = e; handler.sendMessage(msg); }finally{ synchronized (MainActivity.this) { runningThread--; if(runningThread == 0){ for(int i=1;i<=threadCount;i++){ File file = new File("/sdcard/"+i+".txt"); file.delete(); } Message msg =new Message(); msg.what = 2; msg.obj ="下载完毕"; handler.sendMessage(msg); } } } } } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.main, menu); return true; } }
源代码如上..优化的事情我就不做了..为了方便直接就贴上了..这里定义了一个ProgressBar进度条..一个TextView来同步进度条的下载进度..在Android中我们自然不能够在主线程中去调用耗时间的操作..因此这些耗时的操作我们就通过开启子线程的方式去使用..但是子线程是不能够更新UI界面的..因此我们需要使用到Handler Message机制来完成主界面UI更新的操作.
但是上面的代码当中我们会发现一个问题..在子线程内部居然更新了ProgressBar操作..其实ProgressBar和ProgressDialog是两个特例..我们可以在子线程内部去更新他们的属性..我们来看一下源代码的实现过程..
private synchronized void refreshProgress(int id, int progress, boolean fromUser) { if (mUiThreadId == Thread.currentThread().getId()) { //如果当前运行的线程和主线程相同..那么更新进度条.. doRefreshProgress(id, progress, fromUser, true); } else { //如果不满足上面说的情况.. if (mRefreshProgressRunnable == null) { mRefreshProgressRunnable = new RefreshProgressRunnable();//那么新建立一个线程..然后执行下面的过程.. } final RefreshData rd = RefreshData.obtain(id, progress, fromUser); //获取消息队列中的消息.. mRefreshData.add(rd); if (mAttached && !mRefreshIsPosted) { post(mRefreshProgressRunnable); //主要是这个地方..调用了post方法..将当前运行的线程发送到消息队列当中..那么这个线程就可以在UI中运行了..因此这一步是决定因素.. mRefreshIsPosted = true; } } }
正是由于源码内部调用了post方法..将当前的线程放入到消息队列当中..那么UI中的Looper线程就会对这个线程进行处理..那么就表示这个线程是可以被执行在UI当中的..也正是这个因素导致了我们可以在子线程内部更新ProgressBar..但是我们可以看到如果我们想要去更新TextView的时候..我们就需要调用Handler Message机制来完成UI界面的更新了..因此这一块需要我们去注意..
移植之后代码其实并没有发生太大的变化..这样就可以完成一个在Android中的多线程断点下载器了..
Android开发多线程断点续传下载器
使用多线程断点续传下载器在下载的时候多个线程并发可以占用服务器端更多资源,从而加快下载速度,在下载过程中记录每个线程已拷贝数据的数量,如果下载中断,比如无信号断线、电量不足等情况下,这就需要使用到断点续传功能,下次启动时从记录位置继续下载,可避免重复部分的下载。这里采用数据库来记录下载的进度。
效果图
断点续传
1.断点续传需要在下载过程中记录每条线程的下载进度
2.每次下载开始之前先读取数据库,查询是否有未完成的记录,有就继续下载,没有则创建新记录插入数据库
3.在每次向文件中写入数据之后,在数据库中更新下载进度
4.下载完成之后删除数据库中下载记录
Handler传输数据
这个主要用来记录百分比,每下载一部分数据就通知主线程来记录时间
1.主线程中创建的View只能在主线程中修改,其他线程只能通过和主线程通信,在主线程中改变View数据
2.我们使用Handler可以处理这种需求
主线程中创建Handler,重写handleMessage()方法
新线程中使用Handler发送消息,主线程即可收到消息,并且执行handleMessage()方法
动态生成新View
可实现多任务下载
1.创建XML文件,将要生成的View配置好
2.获取系统服务LayoutInflater,用来生成新的View
LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
3.使用inflate(int resource, ViewGroup root)方法生成新的View
4.调用当前页面中某个容器的addView,将新创建的View添加进来
示例
进度条样式 download.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="wrap_content" > <LinearLayout android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_weight="1" > <!--进度条样式默认为圆形进度条,水平进度条需要配置style属性, ?android:attr/progressBarStyleHorizontal --> <ProgressBar android:layout_width="fill_parent" android:layout_height="20dp" style="?android:attr/progressBarStyleHorizontal" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:text="0%" /> </LinearLayout> <Button android:layout_width="40dp" android:layout_height="40dp" android:onClick="pause" android:text="||" /> </LinearLayout>
顶部样式 main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" android:id="@+id/root" > <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="请输入下载路径" /> <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginBottom="30dp" > <EditText android:id="@+id/path" android:layout_width="fill_parent" android:layout_height="wrap_content" android:singleLine="true" android:layout_weight="1" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="下载" android:onClick="download" /> </LinearLayout> </LinearLayout>
MainActivity.java
public class MainActivity extends Activity { private LayoutInflater inflater; private LinearLayout rootLinearLayout; private EditText pathEditText; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); //动态生成新View,获取系统服务LayoutInflater,用来生成新的View inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE); rootLinearLayout = (LinearLayout) findViewById(R.id.root); pathEditText = (EditText) findViewById(R.id.path); // 窗体创建之后, 查询数据库是否有未完成任务, 如果有, 创建进度条等组件, 继续下载 List<String> list = new InfoDao(this).queryUndone(); for (String path : list) createDownload(path); } /** * 下载按钮 * @param view */ public void download(View view) { String path = "http://192.168.1.199:8080/14_Web/" + pathEditText.getText().toString(); createDownload(path); } /** * 动态生成新View * 初始化表单数据 * @param path */ private void createDownload(String path) { //获取系统服务LayoutInflater,用来生成新的View LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE); LinearLayout linearLayout = (LinearLayout) inflater.inflate(R.layout.download, null); LinearLayout childLinearLayout = (LinearLayout) linearLayout.getChildAt(0); ProgressBar progressBar = (ProgressBar) childLinearLayout.getChildAt(0); TextView textView = (TextView) childLinearLayout.getChildAt(1); Button button = (Button) linearLayout.getChildAt(1); try { button.setOnClickListener(new MyListener(progressBar, textView, path)); //调用当前页面中某个容器的addView,将新创建的View添加进来 rootLinearLayout.addView(linearLayout); } catch (Exception e) { e.printStackTrace(); } } private final class MyListener implements OnClickListener { private ProgressBar progressBar; private TextView textView; private int fileLen; private Downloader downloader; private String name; /** * 执行下载 * @param progressBar //进度条 * @param textView //百分比 * @param path //下载文件路径 */ public MyListener(ProgressBar progressBar, TextView textView, String path) { this.progressBar = progressBar; this.textView = textView; name = path.substring(path.lastIndexOf("/") + 1); downloader = new Downloader(getApplicationContext(), handler); try { downloader.download(path, 3); } catch (Exception e) { e.printStackTrace(); Toast.makeText(getApplicationContext(), "下载过程中出现异常", 0).show(); throw new RuntimeException(e); } } //Handler传输数据 private Handler handler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case 0: //获取文件的大小 fileLen = msg.getData().getInt("fileLen"); //设置进度条最大刻度:setMax() progressBar.setMax(fileLen); break; case 1: //获取当前下载的总量 int done = msg.getData().getInt("done"); //当前进度的百分比 textView.setText(name + "\t" + done * 100 / fileLen + "%"); //进度条设置当前进度:setProgress() progressBar.setProgress(done); if (done == fileLen) { Toast.makeText(getApplicationContext(), name + " 下载完成", 0).show(); //下载完成后退出进度条 rootLinearLayout.removeView((View) progressBar.getParent().getParent()); } break; } } }; /** * 暂停和继续下载 */ public void onClick(View v) { Button pauseButton = (Button) v; if ("||".equals(pauseButton.getText())) { downloader.pause(); pauseButton.setText("▶"); } else { downloader.resume(); pauseButton.setText("||"); } } } }
Downloader.java
public class Downloader { private int done; private InfoDao dao; private int fileLen; private Handler handler; private boolean isPause; public Downloader(Context context, Handler handler) { dao = new InfoDao(context); this.handler = handler; } /** * 多线程下载 * @param path 下载路径 * @param thCount 需要开启多少个线程 * @throws Exception */ public void download(String path, int thCount) throws Exception { URL url = new URL(path); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); //设置超时时间 conn.setConnectTimeout(3000); if (conn.getResponseCode() == 200) { fileLen = conn.getContentLength(); String name = path.substring(path.lastIndexOf("/") + 1); File file = new File(Environment.getExternalStorageDirectory(), name); RandomAccessFile raf = new RandomAccessFile(file, "rws"); raf.setLength(fileLen); raf.close(); //Handler发送消息,主线程接收消息,获取数据的长度 Message msg = new Message(); msg.what = 0; msg.getData().putInt("fileLen", fileLen); handler.sendMessage(msg); //计算每个线程下载的字节数 int partLen = (fileLen + thCount - 1) / thCount; for (int i = 0; i < thCount; i++) new DownloadThread(url, file, partLen, i).start(); } else { throw new IllegalArgumentException("404 path: " + path); } } private final class DownloadThread extends Thread { private URL url; private File file; private int partLen; private int id; public DownloadThread(URL url, File file, int partLen, int id) { this.url = url; this.file = file; this.partLen = partLen; this.id = id; } /** * 写入操作 */ public void run() { // 判断上次是否有未完成任务 Info info = dao.query(url.toString(), id); if (info != null) { // 如果有, 读取当前线程已下载量 done += info.getDone(); } else { // 如果没有, 则创建一个新记录存入 info = new Info(url.toString(), id, 0); dao.insert(info); } int start = id * partLen + info.getDone(); // 开始位置 += 已下载量 int end = (id + 1) * partLen - 1; try { HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setReadTimeout(3000); //获取指定位置的数据,Range范围如果超出服务器上数据范围, 会以服务器数据末尾为准 conn.setRequestProperty("Range", "bytes=" + start + "-" + end); RandomAccessFile raf = new RandomAccessFile(file, "rws"); raf.seek(start); //开始读写数据 InputStream in = conn.getInputStream(); byte[] buf = new byte[1024 * 10]; int len; while ((len = in.read(buf)) != -1) { if (isPause) { //使用线程锁锁定该线程 synchronized (dao) { try { dao.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } raf.write(buf, 0, len); done += len; info.setDone(info.getDone() + len); // 记录每个线程已下载的数据量 dao.update(info); //新线程中用Handler发送消息,主线程接收消息 Message msg = new Message(); msg.what = 1; msg.getData().putInt("done", done); handler.sendMessage(msg); } in.close(); raf.close(); // 删除下载记录 dao.deleteAll(info.getPath(), fileLen); } catch (IOException e) { e.printStackTrace(); } } } //暂停下载 public void pause() { isPause = true; } //继续下载 public void resume() { isPause = false; //恢复所有线程 synchronized (dao) { dao.notifyAll(); } } } Dao: DBOpenHelper: public class DBOpenHelper extends SQLiteOpenHelper { public DBOpenHelper(Context context) { super(context, "download.db", null, 1); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL("CREATE TABLE info(path VARCHAR(1024), thid INTEGER, done INTEGER, PRIMARY KEY(path, thid))"); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { } }
InfoDao:
public class InfoDao { private DBOpenHelper helper; public InfoDao(Context context) { helper = new DBOpenHelper(context); } public void insert(Info info) { SQLiteDatabase db = helper.getWritableDatabase(); db.execSQL("INSERT INTO info(path, thid, done) VALUES(?, ?, ?)", new Object[] { info.getPath(), info.getThid(), info.getDone() }); } public void delete(String path, int thid) { SQLiteDatabase db = helper.getWritableDatabase(); db.execSQL("DELETE FROM info WHERE path=? AND thid=?", new Object[] { path, thid }); } public void update(Info info) { SQLiteDatabase db = helper.getWritableDatabase(); db.execSQL("UPDATE info SET done=? WHERE path=? AND thid=?", new Object[] { info.getDone(), info.getPath(), info.getThid() }); } public Info query(String path, int thid) { SQLiteDatabase db = helper.getWritableDatabase(); Cursor c = db.rawQuery("SELECT path, thid, done FROM info WHERE path=? AND thid=?", new String[] { path, String.valueOf(thid) }); Info info = null; if (c.moveToNext()) info = new Info(c.getString(0), c.getInt(1), c.getInt(2)); c.close(); return info; } public void deleteAll(String path, int len) { SQLiteDatabase db = helper.getWritableDatabase(); Cursor c = db.rawQuery("SELECT SUM(done) FROM info WHERE path=?", new String[] { path }); if (c.moveToNext()) { int result = c.getInt(0); if (result == len) db.execSQL("DELETE FROM info WHERE path=? ", new Object[] { path }); } } public List<String> queryUndone() { SQLiteDatabase db = helper.getWritableDatabase(); Cursor c = db.rawQuery("SELECT DISTINCT path FROM info", null); List<String> pathList = new ArrayList<String>(); while (c.moveToNext()) pathList.add(c.getString(0)); c.close(); return pathList; } }
下面我们来看一下如何对native层进行加密,从而增加破解难度。我们在使用native层的时候,我们都知道一般是和Java层调用native层函数,那么我们就需要对native层函数进行加密,把重要的功能实现存放到native层,加大破解难度,那么我们来看一下如何对so中的函数进行加密?
这里有两种方案:
1、我们知道so文件中有很多section,我们可以将我们的目标函数存到指定的section中,然后对section进行加密即可。
技术原理
加密:在之前的文章中我们介绍了so中的格式,那么对于找到一个section的base和size就可以对这段section进行加密了
解密:因为我们对section进行加密之后,肯定需要解密的,不然的话,运行肯定是报错的,那么这里的重点是什么时候去进行解密,对于一个so文件,我们load进程序之后,在运行程序之前我们可以从哪个时间点来突破?这里就需要一个知识点:
__attribute__((constructor));
关于这个,属性的用法这里就不做介绍了,网上有相关资料,他的作用很简单,就是优先于main方法之前执行,类似于Java中的构造函数,当然其实C++中的构造函数就是基于这个属性实现的,我们在之前介绍elf文件格式的时候,有两个section会引起我们的注意:
对于这两个section,其实就是用这个属性实现的函数存在这里,
在动态链接器构造了进程映像,并执行了重定位以后,每个共享的目标都获得执行 某些初始化代码的机会。这些初始化函数的被调用顺序是不一定的,不过所有共享目标 初始化都会在可执行文件得到控制之前发生。
类似地,共享目标也包含终止函数,这些函数在进程完成终止动作序列时,通过 atexit() 机制执行。动态链接器对终止函数的调用顺序是不确定的。
共享目标通过动态结构中的 DT_INIT 和 DT_FINI 条目指定初始化/终止函数。通常 这些代码放在.init 和.fini 节区中。
这个知识点很重要,我们后面在进行动态调试so的时候,还会用到这个知识点,所以一定要理解。
所以,在这里我们找到了解密的时机,就是自己定义一个解密函数,然后用上面的这个属性声明就可以了。
实现流程
第一、我们编写一个简单的native代码,这里我们需要做两件事:
1、将我们核心的native函数定义在自己的一个section中,这里会用到这个属性:__attribute__((section (".mytext")));
其中.mytext就是我们自己定义的section.
说到这里,还记得我们之前介绍的一篇文章中介绍了,动态的给so添加一个section:
http://www.lai18.com/content/1425305.html
2、需要编写我们的解密函数,用属性: __attribute__((constructor));声明
这样一个native程序就包含这两个重要的函数,使用ndk编译成so文件
第二、编写加密程序,在加密程序中我们需要做的是:
1、通过解析so文件,找到.mytext段的起始地址和大小,这里的思路是:
找到所有的Section,然后获取他的name字段,在结合String Section,遍历找到.mytext字段
2、找到.mytext段之后,然后进行加密,最后在写入到文件中。
技术实现
前面介绍了原理和实现方案,下面就开始coding吧,
第一、我们先来看看native程序
#include <jni.h>
#include <stdio.h>
#include <android/log.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <elf.h>
#include <sys/mman.h>
jstring getString(JNIEnv*) __attribute__((section (".mytext")));
jstring getString(JNIEnv* env){
return (*env)->NewStringUTF(env, "Native method return!");
};
void init_getString() __attribute__((constructor));
unsigned long getLibAddr();
void init_getString(){
char name[15];
unsigned int nblock;
unsigned int nsize;
unsigned long base;
unsigned long text_addr;
unsigned int i;
Elf32_Ehdr *ehdr;
Elf32_Shdr *shdr;
base = getLibAddr();
ehdr = (Elf32_Ehdr *)base;
text_addr = ehdr->e_shoff + base;
nblock = ehdr->e_entry >> 16;
nsize = ehdr->e_entry & 0xffff;
__android_log_print(ANDROID_LOG_INFO, "JNITag", "nblock = 0x%x,nsize:%d", nblock,nsize);
__android_log_print(ANDROID_LOG_INFO, "JNITag", "base = 0x%x", text_addr);
printf("nblock = %d\n", nblock);
if(mprotect((void *) (text_addr / PAGE_SIZE * PAGE_SIZE), 4096 * nsize, PROT_READ | PROT_EXEC | PROT_WRITE) != 0){
puts("mem privilege change failed");
__android_log_print(ANDROID_LOG_INFO, "JNITag", "mem privilege change failed");
}
for(i=0;i< nblock; i++){
char *addr = (char*)(text_addr + i);
*addr = ~(*addr);
}
if(mprotect((void *) (text_addr / PAGE_SIZE * PAGE_SIZE), 4096 * nsize, PROT_READ | PROT_EXEC) != 0){
puts("mem privilege change failed");
}
puts("Decrypt success");
}
unsigned long getLibAddr(){
unsigned long ret = 0;
char name[] = "libdemo.so";
char buf[4096], *temp;
int pid;
FILE *fp;
pid = getpid();
sprintf(buf, "/proc/%d/maps", pid);
fp = fopen(buf, "r");
if(fp == NULL)
{
puts("open failed");
goto _error;
}
while(fgets(buf, sizeof(buf), fp)){
if(strstr(buf, name)){
temp = strtok(buf, "-");
ret = strtoul(temp, NULL, 16);
break;
}
}
_error:
fclose(fp);
return ret;
}
JNIEXPORT jstring JNICALL
Java_com_example_shelldemo_MainActivity_getString( JNIEnv* env,
jobject thiz )
{
#if defined(__arm__)
#if defined(__ARM_ARCH_7A__)
#if defined(__ARM_NEON__)
#define ABI "armeabi-v7a/NEON"
#else
#define ABI "armeabi-v7a"
#endif
#else
#define ABI "armeabi"
#endif
#elif defined(__i386__)
#define ABI "x86"
#elif defined(__mips__)
#define ABI "mips"
#else
#define ABI "unknown"
#endif
return getString(env);
}
下面来分析一下代码:
1、定义自己的段
jstring getString(JNIEnv*) __attribute__((section (".mytext")));
jstring getString(JNIEnv* env){
return (*env)->NewStringUTF(env, "Native method return!");
};
这里的getString返回一个字符串,提供给Android上层,然后将getString定义在.mytext段中。
2、获取so加载到内存中的起始地址
unsigned long getLibAddr(){
unsigned long ret = 0;
char name[] = "libdemo.so";
char buf[4096], *temp;
int pid;
FILE *fp;
pid = getpid();
sprintf(buf, "/proc/%d/maps", pid);
fp = fopen(buf, "r");
if(fp == NULL)
{
puts("open failed");
goto _error;
}
while(fgets(buf, sizeof(buf), fp)){
if(strstr(buf, name)){
temp = strtok(buf, "-");
ret = strtoul(temp, NULL, 16);
break;
}
}
_error:
fclose(fp);
return ret;
}
这里的代码其实就是读取设备的proc/<uid>/maps中的内容,因为这个maps中是程序运行的内存映像:
我们只有获取到so的起始地址,才能找到指定的Section然后进行解密。
3、解密函数
void init_getString(){
char name[15];
unsigned int nblock;
unsigned int nsize;
unsigned long base;
unsigned long text_addr;
unsigned int i;
Elf32_Ehdr *ehdr;
Elf32_Shdr *shdr;
//获取so的起始地址
base = getLibAddr();
//获取指定section的偏移值和size
ehdr = (Elf32_Ehdr *)base;
text_addr = ehdr->e_shoff + base;
nblock = ehdr->e_entry >> 16;
nsize = ehdr->e_entry & 0xffff;
__android_log_print(ANDROID_LOG_INFO, "JNITag", "nblock = 0x%x,nsize:%d", nblock,nsize);
__android_log_print(ANDROID_LOG_INFO, "JNITag", "base = 0x%x", text_addr);
printf("nblock = %d\n", nblock);
//修改内存的操作权限
if(mprotect((void *) (text_addr / PAGE_SIZE * PAGE_SIZE), 4096 * nsize, PROT_READ | PROT_EXEC | PROT_WRITE) != 0){
puts("mem privilege change failed");
__android_log_print(ANDROID_LOG_INFO, "JNITag", "mem privilege change failed");
}
//解密
for(i=0;i< nblock; i++){
char *addr = (char*)(text_addr + i);
*addr = ~(*addr);
}
if(mprotect((void *) (text_addr / PAGE_SIZE * PAGE_SIZE), 4096 * nsize, PROT_READ | PROT_EXEC) != 0){
puts("mem privilege change failed");
}
puts("Decrypt success");
}
这里我们获取到so文件的头部,然后获取指定section的偏移地址和size
//获取so的起始地址
base = getLibAddr();
//获取指定section的偏移值和size
ehdr = (Elf32_Ehdr *)base;
text_addr = ehdr->e_shoff + base;
nblock = ehdr->e_entry >> 16;
nsize = ehdr->e_entry & 0xffff;
这里可能会有困惑?为什么这里是这么获取offset和size的,其实这里我们做了一点工作,就是我们在加密的时候顺便改写了so的头部信息,将offset和size值写到了头部中,这样加大破解难度。后面在说到加密的时候在详解。
text_addr是起始地址+偏移值,就是我们的section在内存中的绝对地址
nsize是我们的section占用的页数
然后修改这个section的内存操作权限
//修改内存的操作权限
if(mprotect((void *) (text_addr / PAGE_SIZE * PAGE_SIZE), 4096 * nsize, PROT_READ | PROT_EXEC | PROT_WRITE) != 0){
puts("mem privilege change failed");
__android_log_print(ANDROID_LOG_INFO, "JNITag", "mem privilege change failed");
}
这里调用了一个系统函数:mprotect
第一个参数:需要修改内存的起始地址
必须需要页面对齐,也就是必须是页面PAGE_SIZE(0x1000=4096)的整数倍
第二个参数:需要修改的大小
占用的页数*PAGE_SIZE
第三个参数:权限值
最后读取内存中的section内容,然后进行解密,在将内存权限修改回去。
然后使用ndk编译成so即可,这里我们用到了系统的打印log信息,所以需要用到共享库,看一下编译脚本Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := demo
LOCAL_SRC_FILES := demo.c
LOCAL_LDLIBS := -llog
include $(BUILD_SHARED_LIBRARY)
第二、加密程序
1、加密程序(Java版)
我们获取到上面的so文件,下面我们就来看看如何进行加密的:
package com.jiangwei.encodesection;
import com.jiangwei.encodesection.ElfType32.Elf32_Sym;
import com.jiangwei.encodesection.ElfType32.elf32_phdr;
import com.jiangwei.encodesection.ElfType32.elf32_shdr;
public class EncodeSection {
public static String encodeSectionName = ".mytext";
public static ElfType32 type_32 = new ElfType32();
public static void main(String[] args){
byte[] fileByteArys = Utils.readFile("so/libdemo.so");
if(fileByteArys == null){
System.out.println("read file byte failed...");
return;
}
/**
* 先解析so文件
* 然后初始化AddSection中的一些信息
* 最后在AddSection
*/
parseSo(fileByteArys);
encodeSection(fileByteArys);
parseSo(fileByteArys);
Utils.saveFile("so/libdemos.so", fileByteArys);
}
private static void encodeSection(byte[] fileByteArys){
//读取String Section段
System.out.println();
int string_section_index = Utils.byte2Short(type_32.hdr.e_shstrndx);
elf32_shdr shdr = type_32.shdrList.get(string_section_index);
int size = Utils.byte2Int(shdr.sh_size);
int offset = Utils.byte2Int(shdr.sh_offset);
int mySectionOffset=0,mySectionSize=0;
for(elf32_shdr temp : type_32.shdrList){
int sectionNameOffset = offset+Utils.byte2Int(temp.sh_name);
if(Utils.isEqualByteAry(fileByteArys, sectionNameOffset, encodeSectionName)){
//这里需要读取section段然后进行数据加密
mySectionOffset = Utils.byte2Int(temp.sh_offset);
mySectionSize = Utils.byte2Int(temp.sh_size);
byte[] sectionAry = Utils.copyBytes(fileByteArys, mySectionOffset, mySectionSize);
for(int i=0;i<sectionAry.length;i++){
sectionAry[i] = (byte)(sectionAry[i] ^ 0xFF);
}
Utils.replaceByteAry(fileByteArys, mySectionOffset, sectionAry);
}
}
//修改Elf Header中的entry和offset值
int nSize = mySectionSize/4096 + (mySectionSize%4096 == 0 ? 0 : 1);
byte[] entry = new byte[4];
entry = Utils.int2Byte((mySectionSize<<16) + nSize);
Utils.replaceByteAry(fileByteArys, 24, entry);
byte[] offsetAry = new byte[4];
offsetAry = Utils.int2Byte(mySectionOffset);
Utils.replaceByteAry(fileByteArys, 32, offsetAry);
}
private static void parseSo(byte[] fileByteArys){
//读取头部内容
System.out.println("+++++++++++++++++++Elf Header+++++++++++++++++");
parseHeader(fileByteArys, 0);
System.out.println("header:\n"+type_32.hdr);
//读取程序头信息
//System.out.println();
//System.out.println("+++++++++++++++++++Program Header+++++++++++++++++");
int p_header_offset = Utils.byte2Int(type_32.hdr.e_phoff);
parseProgramHeaderList(fileByteArys, p_header_offset);
//type_32.printPhdrList();
//读取段头信息
//System.out.println();
//System.out.println("+++++++++++++++++++Section Header++++++++++++++++++");
int s_header_offset = Utils.byte2Int(type_32.hdr.e_shoff);
parseSectionHeaderList(fileByteArys, s_header_offset);
//type_32.printShdrList();
//这种方式获取所有的Section的name
/*byte[] names = Utils.copyBytes(fileByteArys, offset, size);
String str = new String(names);
byte NULL = 0;//字符串的结束符
StringTokenizer st = new StringTokenizer(str, new String(new byte[]{NULL}));
System.out.println( "Token Total: " + st.countTokens() );
while(st.hasMoreElements()){
System.out.println(st.nextToken());
}
System.out.println("");*/
/*//读取符号表信息(Symbol Table)
System.out.println();
System.out.println("+++++++++++++++++++Symbol Table++++++++++++++++++");
//这里需要注意的是:在Elf表中没有找到SymbolTable的数目,但是我们仔细观察Section中的Type=DYNSYM段的信息可以得到,这个段的大小和偏移地址,而SymbolTable的结构大小是固定的16个字节
//那么这里的数目=大小/结构大小
//首先在SectionHeader中查找到dynsym段的信息
int offset_sym = 0;
int total_sym = 0;
for(elf32_shdr shdr : type_32.shdrList){
if(Utils.byte2Int(shdr.sh_type) == ElfType32.SHT_DYNSYM){
total_sym = Utils.byte2Int(shdr.sh_size);
offset_sym = Utils.byte2Int(shdr.sh_offset);
break;
}
}
int num_sym = total_sym / 16;
System.out.println("sym num="+num_sym);
parseSymbolTableList(fileByteArys, num_sym, offset_sym);
type_32.printSymList();
//读取字符串表信息(String Table)
System.out.println();
System.out.println("+++++++++++++++++++Symbol Table++++++++++++++++++");
//这里需要注意的是:在Elf表中没有找到StringTable的数目,但是我们仔细观察Section中的Type=STRTAB段的信息,可以得到,这个段的大小和偏移地址,但是我们这时候我们不知道字符串的大小,所以就获取不到数目了
//这里我们可以查看Section结构中的name字段:表示偏移值,那么我们可以通过这个值来获取字符串的大小
//可以这么理解:当前段的name值 减去 上一段的name的值 = (上一段的name字符串的长度)
//首先获取每个段的name的字符串大小
int prename_len = 0;
int[] lens = new int[type_32.shdrList.size()];
int total = 0;
for(int i=0;i<type_32.shdrList.size();i++){
if(Utils.byte2Int(type_32.shdrList.get(i).sh_type) == ElfType32.SHT_STRTAB){
int curname_offset = Utils.byte2Int(type_32.shdrList.get(i).sh_name);
lens[i] = curname_offset - prename_len - 1;
if(lens[i] < 0){
lens[i] = 0;
}
total += lens[i];
System.out.println("total:"+total);
prename_len = curname_offset;
//这里需要注意的是,最后一个字符串的长度,需要用总长度减去前面的长度总和来获取到
if(i == (lens.length - 1)){
System.out.println("size:"+Utils.byte2Int(type_32.shdrList.get(i).sh_size));
lens[i] = Utils.byte2Int(type_32.shdrList.get(i).sh_size) - total - 1;
}
}
}
for(int i=0;i<lens.length;i++){
System.out.println("len:"+lens[i]);
}
//上面的那个方法不好,我们发现StringTable中的每个字符串结束都会有一个00(传说中的字符串结束符),那么我们只要知道StringTable的开始位置,然后就可以读取到每个字符串的值了
*/
}
/**
* 解析Elf的头部信息
* @param header
*/
private static void parseHeader(byte[] header, int offset){
if(header == null){
System.out.println("header is null");
return;
}
/**
* public byte[] e_ident = new byte[16];
public short e_type;
public short e_machine;
public int e_version;
public int e_entry;
public int e_phoff;
public int e_shoff;
public int e_flags;
public short e_ehsize;
public short e_phentsize;
public short e_phnum;
public short e_shentsize;
public short e_shnum;
public short e_shstrndx;
*/
type_32.hdr.e_ident = Utils.copyBytes(header, 0, 16);//魔数
type_32.hdr.e_type = Utils.copyBytes(header, 16, 2);
type_32.hdr.e_machine = Utils.copyBytes(header, 18, 2);
type_32.hdr.e_version = Utils.copyBytes(header, 20, 4);
type_32.hdr.e_entry = Utils.copyBytes(header, 24, 4);
type_32.hdr.e_phoff = Utils.copyBytes(header, 28, 4);
type_32.hdr.e_shoff = Utils.copyBytes(header, 32, 4);
type_32.hdr.e_flags = Utils.copyBytes(header, 36, 4);
type_32.hdr.e_ehsize = Utils.copyBytes(header, 40, 2);
type_32.hdr.e_phentsize = Utils.copyBytes(header, 42, 2);
type_32.hdr.e_phnum = Utils.copyBytes(header, 44,2);
type_32.hdr.e_shentsize = Utils.copyBytes(header, 46,2);
type_32.hdr.e_shnum = Utils.copyBytes(header, 48, 2);
type_32.hdr.e_shstrndx = Utils.copyBytes(header, 50, 2);
}
/**
* 解析程序头信息
* @param header
*/
public static void parseProgramHeaderList(byte[] header, int offset){
int header_size = 32;//32个字节
int header_count = Utils.byte2Short(type_32.hdr.e_phnum);//头部的个数
byte[] des = new byte[header_size];
for(int i=0;i<header_count;i++){
System.arraycopy(header, i*header_size + offset, des, 0, header_size);
type_32.phdrList.add(parseProgramHeader(des));
}
}
private static elf32_phdr parseProgramHeader(byte[] header){
/**
* public int p_type;
public int p_offset;
public int p_vaddr;
public int p_paddr;
public int p_filesz;
public int p_memsz;
public int p_flags;
public int p_align;
*/
ElfType32.elf32_phdr phdr = new ElfType32.elf32_phdr();
phdr.p_type = Utils.copyBytes(header, 0, 4);
phdr.p_offset = Utils.copyBytes(header, 4, 4);
phdr.p_vaddr = Utils.copyBytes(header, 8, 4);
phdr.p_paddr = Utils.copyBytes(header, 12, 4);
phdr.p_filesz = Utils.copyBytes(header, 16, 4);
phdr.p_memsz = Utils.copyBytes(header, 20, 4);
phdr.p_flags = Utils.copyBytes(header, 24, 4);
phdr.p_align = Utils.copyBytes(header, 28, 4);
return phdr;
}
/**
* 解析段头信息内容
*/
public static void parseSectionHeaderList(byte[] header, int offset){
int header_size = 40;//40个字节
int header_count = Utils.byte2Short(type_32.hdr.e_shnum);//头部的个数
byte[] des = new byte[header_size];
for(int i=0;i<header_count;i++){
System.arraycopy(header, i*header_size + offset, des, 0, header_size);
type_32.shdrList.add(parseSectionHeader(des));
}
}
private static elf32_shdr parseSectionHeader(byte[] header){
ElfType32.elf32_shdr shdr = new ElfType32.elf32_shdr();
/**
* public byte[] sh_name = new byte[4];
public byte[] sh_type = new byte[4];
public byte[] sh_flags = new byte[4];
public byte[] sh_addr = new byte[4];
public byte[] sh_offset = new byte[4];
public byte[] sh_size = new byte[4];
public byte[] sh_link = new byte[4];
public byte[] sh_info = new byte[4];
public byte[] sh_addralign = new byte[4];
public byte[] sh_entsize = new byte[4];
*/
shdr.sh_name = Utils.copyBytes(header, 0, 4);
shdr.sh_type = Utils.copyBytes(header, 4, 4);
shdr.sh_flags = Utils.copyBytes(header, 8, 4);
shdr.sh_addr = Utils.copyBytes(header, 12, 4);
shdr.sh_offset = Utils.copyBytes(header, 16, 4);
shdr.sh_size = Utils.copyBytes(header, 20, 4);
shdr.sh_link = Utils.copyBytes(header, 24, 4);
shdr.sh_info = Utils.copyBytes(header, 28, 4);
shdr.sh_addralign = Utils.copyBytes(header, 32, 4);
shdr.sh_entsize = Utils.copyBytes(header, 36, 4);
return shdr;
}
/**
* 解析Symbol Table内容
*/
public static void parseSymbolTableList(byte[] header, int header_count, int offset){
int header_size = 16;//16个字节
byte[] des = new byte[header_size];
for(int i=0;i<header_count;i++){
System.arraycopy(header, i*header_size + offset, des, 0, header_size);
type_32.symList.add(parseSymbolTable(des));
}
}
private static ElfType32.Elf32_Sym parseSymbolTable(byte[] header){
/**
* public byte[] st_name = new byte[4];
public byte[] st_value = new byte[4];
public byte[] st_size = new byte[4];
public byte st_info;
public byte st_other;
public byte[] st_shndx = new byte[2];
*/
Elf32_Sym sym = new Elf32_Sym();
sym.st_name = Utils.copyBytes(header, 0, 4);
sym.st_value = Utils.copyBytes(header, 4, 4);
sym.st_size = Utils.copyBytes(header, 8, 4);
sym.st_info = header[12];
//FIXME 这里有一个问题,就是这个字段读出来的值始终是0
sym.st_other = header[13];
sym.st_shndx = Utils.copyBytes(header, 14, 2);
return sym;
}
}
在这里,我需要解析so文件的头部信息,程序头信息,段头信息
//读取头部内容
System.out.println("+++++++++++++++++++Elf Header+++++++++++++++++");
parseHeader(fileByteArys, 0);
System.out.println("header:\n"+type_32.hdr);
//读取程序头信息
//System.out.println();
//System.out.println("+++++++++++++++++++Program Header+++++++++++++++++");
int p_header_offset = Utils.byte2Int(type_32.hdr.e_phoff);
parseProgramHeaderList(fileByteArys, p_header_offset);
//type_32.printPhdrList();
//读取段头信息
//System.out.println();
//System.out.println("+++++++++++++++++++Section Header++++++++++++++++++");
int s_header_offset = Utils.byte2Int(type_32.hdr.e_shoff);
parseSectionHeaderList(fileByteArys, s_header_offset);
//type_32.printShdrList();
获取这些信息之后,下面就来开始寻找我们的段了,只需要遍历Section列表,找到名字是.mytext的section即可,然后获取offset和size,对内容进行加密,回写到文件中。下面来看看核心方法:
private static void encodeSection(byte[] fileByteArys){
//读取String Section段
System.out.println();
int string_section_index = Utils.byte2Short(type_32.hdr.e_shstrndx);
elf32_shdr shdr = type_32.shdrList.get(string_section_index);
int size = Utils.byte2Int(shdr.sh_size);
int offset = Utils.byte2Int(shdr.sh_offset);
int mySectionOffset=0,mySectionSize=0;
for(elf32_shdr temp : type_32.shdrList){
int sectionNameOffset = offset+Utils.byte2Int(temp.sh_name);
if(Utils.isEqualByteAry(fileByteArys, sectionNameOffset, encodeSectionName)){
//这里需要读取section段然后进行数据加密
mySectionOffset = Utils.byte2Int(temp.sh_offset);
mySectionSize = Utils.byte2Int(temp.sh_size);
byte[] sectionAry = Utils.copyBytes(fileByteArys, mySectionOffset, mySectionSize);
for(int i=0;i<sectionAry.length;i++){
sectionAry[i] = (byte)(sectionAry[i] ^ 0xFF);
}
Utils.replaceByteAry(fileByteArys, mySectionOffset, sectionAry);
}
}
//修改Elf Header中的entry和offset值
int nSize = mySectionSize/4096 + (mySectionSize%4096 == 0 ? 0 : 1);
byte[] entry = new byte[4];
entry = Utils.int2Byte((mySectionSize<<16) + nSize);
Utils.replaceByteAry(fileByteArys, 24, entry);
byte[] offsetAry = new byte[4];
offsetAry = Utils.int2Byte(mySectionOffset);
Utils.replaceByteAry(fileByteArys, 32, offsetAry);
}
我们知道Section中的sh_name字段的值是这个section段的name在StringSection中的索引值,这里offset就是StringSection在文件中的偏移值。当然我们需要知道的一个知识点就是:StringSection中的每个name都是以\0结尾的,所以我们只需要判断字符串到结束符就可以了,判断方法是Utils.isEqualByteAry:
public static boolean isEqualByteAry(byte[] src, int start, String destStr){
if(destStr == null){
return false;
}
byte[] dest = destStr.getBytes();
if(src == null || dest == null){
return false;
}
if(dest.length == 0 || src.length == 0){
return false;
}
if(start >= src.length){
return false;
}
int len = 0;
byte temp = src[start];
while(temp != 0){
len++;
temp = src[start+len];
}
byte[] sonAry = copyBytes(src, start, len);
if(sonAry == null || sonAry.length == 0){
return false;
}
if(sonAry.length != dest.length){
return false;
}
String sonStr = new String(sonAry);
if(destStr.equals(sonStr)){
return true;
}
return false;
}
这里我们加密的方法很简单,加密完成之后,我们需要做的是回写到so文件中,当然这里我们还需要做一件事,就是将我们加密的.mytext段的偏移值和pageSize保存到头部信息中:
//修改Elf Header中的entry和offset值
int nSize = mySectionSize/4096 + (mySectionSize%4096 == 0 ? 0 : 1);
byte[] entry = new byte[4];
entry = Utils.int2Byte((mySectionSize<<16) + nSize);
Utils.replaceByteAry(fileByteArys, 24, entry);
这里又有一个知识点需要说明?大家可能会困惑,我们这样修改了so的头部信息的话,在加载运行so文件的时候不会报错吗?这个就要看看Android底层是如何解析so文件,然后将so文件映射到内存中的了,下面我们来看看系统是如何解析so文件的?
源代码的位置:Android linker源码:bionic\linker
在linker.h源码中有一个重要的结构体soinfo,下面列出一些字段:
struct soinfo{
const char name[SOINFO_NAME_LEN]; //so全名
Elf32_Phdr *phdr; //Program header的地址
int phnum; //segment 数量
unsigned *dynamic; //指向.dynamic,在section和segment中相同的
//以下4个成员与.hash表有关
unsigned nbucket;
unsigned nchain;
unsigned *bucket;
unsigned *chain;
//这两个成员只能会出现在可执行文件中
unsigned *preinit_array;
unsigned preinit_array_count;
指向初始化代码,先于main函数之行,即在加载时被linker所调用,在linker.c可以看到:__linker_init -> link_image ->
call_constructors -> call_array
unsigned *init_array;
unsigned init_array_count;
void (*init_func)(void);
//与init_array类似,只是在main结束之后执行
unsigned *fini_array;
unsigned fini_array_count;
void (*fini_func)(void);
}
另外,linker.c中也有许多地方可以佐证。其本质还是linker是基于装载视图解析的so文件的。
基于上面的结论,再来分析下ELF头的字段。
1) e_ident[EI_NIDENT] 字段包含魔数、字节序、字长和版本,后面填充0。对于安卓的linker,通过verify_elf_object函数检验魔数,判定是否为.so文件。那么,我们可以向位置写入数据,至少可以向后面的0填充位置写入数据。遗憾的是,我在fedora 14下测试,是不能向0填充位置写数据,链接器报非0填充错误。
2) 对于安卓的linker,对e_type、e_machine、e_version和e_flags字段并不关心,是可以修改成其他数据的(仅分析,没有实测)
3) 对于动态链接库,e_entry 入口地址是无意义的,因为程序被加载时,设定的跳转地址是动态连接器的地址,这个字段是可以被作为数据填充的。
4) so装载时,与链接视图没有关系,即e_shoff、e_shentsize、e_shnum和e_shstrndx这些字段是可以任意修改的。被修改之后,使用readelf和ida等工具打开,会报各种错误,相信读者已经见识过了。
5) 既然so装载与装载视图紧密相关,自然e_phoff、e_phentsize和e_phnum这些字段是不能动的。
从上面我们可以知道,so中的有些信息在运行的时候是没有用途的,有些东西是不能改的。
2、加密程序(C版)
上面说的是Java版本的,下面再来一个C版本的:
#include <stdio.h>
#include <fcntl.h>
#include "elf.h"
#include <stdlib.h>
#include <string.h>
int main(int argc, char** argv){
char *encodeSoName = "libdemo.so";
char target_section[] = ".mytext";
char *shstr = NULL;
char *content = NULL;
Elf32_Ehdr ehdr;
Elf32_Shdr shdr;
int i;
unsigned int base, length;
unsigned short nblock;
unsigned short nsize;
unsigned char block_size = 16;
int fd;
fd = open(encodeSoName, O_RDWR);
if(fd < 0){
printf("open %s failed\n", argv[1]);
goto _error;
}
if(read(fd, &ehdr, sizeof(Elf32_Ehdr)) != sizeof(Elf32_Ehdr)){
puts("Read ELF header error");
goto _error;
}
lseek(fd, ehdr.e_shoff + sizeof(Elf32_Shdr) * ehdr.e_shstrndx, SEEK_SET);
if(read(fd, &shdr, sizeof(Elf32_Shdr)) != sizeof(Elf32_Shdr)){
puts("Read ELF section string table error");
goto _error;
}
if((shstr = (char *) malloc(shdr.sh_size)) == NULL){
puts("Malloc space for section string table failed");
goto _error;
}
lseek(fd, shdr.sh_offset, SEEK_SET);
if(read(fd, shstr, shdr.sh_size) != shdr.sh_size){
puts("Read string table failed");
goto _error;
}
lseek(fd, ehdr.e_shoff, SEEK_SET);
for(i = 0; i < ehdr.e_shnum; i++){
if(read(fd, &shdr, sizeof(Elf32_Shdr)) != sizeof(Elf32_Shdr)){
puts("Find section .text procedure failed");
goto _error;
}
if(strcmp(shstr + shdr.sh_name, target_section) == 0){
base = shdr.sh_offset;
length = shdr.sh_size;
printf("Find section %s\n", target_section);
break;
}
}
lseek(fd, base, SEEK_SET);
content = (char*) malloc(length);
if(content == NULL){
puts("Malloc space for content failed");
goto _error;
}
if(read(fd, content, length) != length){
puts("Read section .text failed");
goto _error;
}
nblock = length / block_size;
nsize = length / 4096 + (length % 4096 == 0 ? 0 : 1);
printf("base = %x, length = %x\n", base, length);
printf("nblock = %d, nsize = %d\n", nblock, nsize);
printf("entry:%x\n",((length << 16) + nsize));
ehdr.e_entry = (length << 16) + nsize;
ehdr.e_shoff = base;
for(i=0;i<length;i++){
content[i] = ~content[i];
}
lseek(fd, 0, SEEK_SET);
if(write(fd, &ehdr, sizeof(Elf32_Ehdr)) != sizeof(Elf32_Ehdr)){
puts("Write ELFhead to .so failed");
goto _error;
}
lseek(fd, base, SEEK_SET);
if(write(fd, content, length) != length){
puts("Write modified content to .so failed");
goto _error;
}
puts("Completed");
_error:
free(content);
free(shstr);
close(fd);
return 0;
}
这里就不做详细解释了
我们在上面加密完成之后,我们可以验证一下,使用readelf命令查看一下:
哈哈,加密成功,我们在用IDA查看一下:
会有错误提示,但是我们点击OK,还是成功打开了so文件,但是我们ctrl+s查看段信息的时候:
也是没有看到我们的段信息,我们可以看一下我们没有加密前的效果:
既然加密成功了,那么下面我们得验证一下能否运行成功
第三、Android测试demo
我们在获取加密之后的so文件之后,我们用Android工程测试一下:
package com.example.shelldemo;
import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.TextView;
public class MainActivity extends Activity {
private TextView tv;
private native String getString();
static{
System.loadLibrary("demo");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tv = (TextView) findViewById(R.id.tv);
tv.setText(getString());
}
}
运行结果:
看到了,运行成功了。
技术总结
1、Elf文件格式的深入了解
2、两个属性的了解:__attribute__((constructor)); __attribute__((section (".mytext")));
3、程序的maps内存映像了解
4、修改内存属性方法
5、Android系统如何解析so文件linker源码
六、梳理流程步骤
加密流程:
1) 从so文件头读取section偏移shoff、shnum和shstrtab
2) 读取shstrtab中的字符串,存放在str空间中
3) 从shoff位置开始读取section header, 存放在shdr
4) 通过shdr -> sh_name 在str字符串中索引,与.mytext进行字符串比较,如果不匹配,继续读取
5) 通过shdr -> sh_offset 和 shdr -> sh_size字段,将.mytext内容读取并保存在content中。
6) 为了便于理解,不使用复杂的加密算法。这里,只将content的所有内容取反,即 *content = ~(*content);
7) 将content内容写回so文件中
8) 为了验证第二节中关于section 字段可以任意修改的结论,这里,将shdr -> addr 写入ELF头e_shoff,将shdr -> sh_size 和 addr 所在内存块写入e_entry中,即ehdr.e_entry = (length << 16) + nsize。当然,这样同时也简化了解密流程,还有一个好处是:如果将so文件头修正放回去,程序是不能运行的。
解密时,需要保证解密函数在so加载时被调用,那函数声明为:init_getString __attribute__((constructor))。(也可以使用c++构造器实现, 其本质也是用attribute实现)
解密流程:
1) 动态链接器通过call_array调用init_getString
2) Init_getString首先调用getLibAddr方法,得到so文件在内存中的起始地址
3) 读取前52字节,即ELF头。通过e_shoff获得.mytext内存加载地址,ehdr.e_entry获取.mytext大小和所在内存块
4) 修改.mytext所在内存块的读写权限
5) 将[e_shoff, e_shoff + size]内存区域数据解密,即取反操作:*content = ~(*content);
6) 修改回内存区域的读写权限
(这里是对代码段的数据进行解密,需要写权限。如果对数据段的数据解密,是不需要更改权限直接操作的)
2、直接对目标函数进行加密
技术原理
这篇和之前的那篇文章唯一的不同点就是如何找到指定的函数的偏移地址和大小
那么我们先来了解一下so中函数的表现形式:
在so文件中,每个函数的结构描述是存放在.dynsym段中的。每个函数的名称保存在.dynstr段中的,类似于之前说过的每个section的名称都保存在.shstrtab段中,所以在前面的文章中我们找到指定段的时候,就是通过每个段的sh_name字段到.shstrtab中寻找名字即可,而且我们知道.shstrtab这个段在头文件中是有一个index的,就是在所有段列表中的索引值,所以很好定位.shstrtab.
但是在这篇文章我们可能遇到一个问题,就是不能按照这种方式去查找指定函数名了:
可能有的人意识到一个方法,就是我们可以通过section的type来获取.dynsym和.dynstr。我们看到上图中.dynsym类型是:DYNSYM,
.dynstr类型是STRTAB,但是这种方法是不行的,因为这个type不是唯一的,也就说不同的section,type可能相同,我们没办法区分,比如.shstrtab和.dynstr的type都是STRTAB.其实从这里我们就知道这两个段的区别了:
.shstrtab值存储段的名称,.dynstr是存储so中的所有符号名称。
那么我们该怎么办呢?这时候我们再去看一下elf的说明文档:
http://download.csdn.net/detail/jiangwei0910410003/9204051
我们看到有一个.hash段,在上图中我们也可以看到的:
由 Elf32_Word 对象组成的哈希表支持符号表访问。下面的例子有助于解释哈希表
组织,不过不是规范的一部分。bucket 数组包含 nbucket 个项目,chain 数组包含 nchain 个项目,下标都是从 0 开始。bucket 和 chain 中都保存符号表索引。Chain 表项和符号表存在对应。符号 表项的数目应该和 nchain 相等,所以符号表的索引也可用来选取 chain 表项。哈希 函数能够接受符号名并且返回一个可以用来计算 bucket 的索引。
因此,如果哈希函数针对某个名字返回了数值 X,则 bucket[X%nbucket] 给出了 一个索引 y,该索引可用于符号表,也可用于 chain 表。如果符号表项不是所需要的, 那么 chain[y] 则给出了具有相同哈希值的下一个符号表项。我们可以沿着 chain 链 一直搜索,直到所选中的符号表项包含了所需要的符号,或者 chain 项中包含值 STN_UNDEF。
上面的描述感觉有点复杂,其实说的简单点就是:
用目标函数名在用hash函数得到一个hash值,然后再做一些计算就可以得到这个函数在.dynsym段中这个函数对应的条目了。关于这个hash函数,是公用的,我们在Android中的bonic/linker.c源码中也是可以找到的:
unsigned long elf_hash (const unsigned char *name) {
unsigned long h = 0, g; while (*name)
{
h=(h<<4)+*name++; if (g = h & 0xf0000000)
h^=g>>24; h&=-g;
}
return h;
}
那么我们只要得到.hash段即可,但是我们怎么获取到这个section中呢?elf中并没有对这个段进行数据结构的描述,有人可能想到了我们在上图看到.hash段的type是HASH,那么我们再通过这个type来获取?但是之前说了,这个type不是唯一的,通过他来获取section是不靠谱的?那么我们该怎么办呢?这时候我们就要看一下程序头信息了:
我们知道程序头信息是最后so被加载到内存中的映像描述,这里我们看到有一个.dynamic段。我们再看看so文件的装载视图和链接视图:
这个我们在之前也说过,在so被加载到内存之后,就没有section了,对应的是segment了,也就是程序头中描述的结构,而且一个segment可以包含多个section,相同的section可以被包含到不同的segment中。.dynamic段一般用于动态链接的,所以.dynsym和.dynstr,.hash肯定包含在这里。我们可以解析了程序头信息之后,通过type获取到.dynamic程序头信息,然后获取到这个segment的偏移地址和大小,在进行解析成elf32_dyn结构。下面两种图就是程序头的type类型和dyn结构描述,可以在elf.h中找到:
/**
* typedef struct dynamic{
Elf32_Sword d_tag;
union{
Elf32_Sword d_val;
Elf32_Addr d_ptr;
} d_un;
} Elf32_Dyn;
*/
public static class elf32_dyn{
public byte[] d_tag = new byte[4];
public byte[] d_val = new byte[4];
public byte[] d_ptr = new byte[4];
/*public static class d_un{
public static byte[] d_val = new byte[4];
public static byte[] d_ptr = new byte[4];
}*/
@Override
public String toString(){
return "d_tag:"+Utils.bytes2HexString(d_tag)+";d_un_d_val:"+Utils.bytes2HexString(d_val)+";d_un_d_ptr:"+Utils.bytes2HexString(d_ptr);
}
}
这里,需要注意的是,C语言中的union联合体结构,所以我们在Java解析的时候需要注意,后面会详细介绍。这里的三个字段很好理解:d_tag:标示,标示这个dyn是什么类型的,是.dynsym还是.dynstr等d_val:这个section的大小d_ptr:这个section的偏移地址细心的同学可能会发现一个问题,就是在这里寻找.dynamic也是通过类型的,然后再找到对应的section.这种方式和之前说的通过type来寻找section,有两个不同:第一、在程序头信息中,type标示.dynamic段是唯一的,所以可以通过type来进行寻找第二、我们看到上面的链接视图和装载视图发现,我们这种通过程序头中的信息来查找.dysym等section靠谱点,因为当so被加载到内存中,就不存在了section了,只有segment了。
实现方案
编写native程序,只是native直接返回字符串给UI。需要做的是对Java_com_example_shelldemo2_MainActivity_getString函数进行加密。加密和解密都是基于装载视图实现。需要注意的是,被加密函数如果用static声明,那么函数是不会出现在.dynsym中,是无法在装载视图中通过函数名找到进行解密的。当然,也可以采用取巧方式,类似上节,把地址和长度信息写入so头中实现。Java_com_example_shelldemo2_MainActivity_getString需要被调用,那么一定是能在.dynsym找到的。
加密流程:
1) 读取文件头,获取e_phoff、e_phentsize和e_phnum信息
2) 通过Elf32_Phdr中的p_type字段,找到DYNAMIC。从下图可以看出,其实DYNAMIC就是.dynamic section。从p_offset和p_filesz字段得到文件中的起始位置和长度
3) 遍历.dynamic,找到.dynsym、.dynstr、.hash section文件中的偏移和.dynstr的大小。在我的测试环境下,fedora 14和windows7 Cygwin x64中elf.h定义.hash的d_tag标示是:DT_GNU_HASH;而安卓源码中的是:DT_HASH。
4) 根据函数名称,计算hash值
5) 根据hash值,找到下标hash % nbuckets的bucket;根据bucket中的值,读取.dynsym中的对应索引的Elf32_Sym符号;从符号的st_name所以找到在.dynstr中对应的字符串与函数名进行比较。若不等,则根据chain[hash % nbuckets]找下一个Elf32_Sym符号,直到找到或者chain终止为止。这里叙述得有些复杂,直接上代码。
for(i = bucket[funHash % nbucket]; i != 0; i = chain[i]){
if(strcmp(dynstr + (funSym + i)->st_name, funcName) == 0){
flag = 0;
break;
}
}
6) 找到函数对应的Elf32_Sym符号后,即可根据st_value和st_size字段找到函数的位置和大小
7) 后面的步骤就和上节相同了,这里就不赘述
解密流程为加密逆过程,大体相同,只有一些细微的区别,具体如下:
1) 找到so文件在内存中的起始地址
2) 也是通过so文件头找到Phdr;从Phdr找到PT_DYNAMIC后,需取p_vaddr和p_filesz字段,并非p_offset,这里需要注意。
3) 后续操作就加密类似,就不赘述。对内存区域数据的解密,也需要注意读写权限问题。
上面就介绍了完了,下面我们就可以来开始coding了。
代码实现
第一、native程序
#include <jni.h>
#include <android/log.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <elf.h>
#include <sys/mman.h>
#define DEBUG
typedef struct _funcInfo{
Elf32_Addr st_value;
Elf32_Word st_size;
}funcInfo;
void init_getString() __attribute__((constructor));
static void print_debug(const char *msg){
#ifdef DEBUG
__android_log_print(ANDROID_LOG_INFO, "JNITag", "%s", msg);
#endif
}
static unsigned elfhash(const char *_name)
{
const unsigned char *name = (const unsigned char *) _name;
unsigned h = 0, g;
while(*name) {
h = (h << 4) + *name++;
g = h & 0xf0000000;
h ^= g;
h ^= g >> 24;
}
return h;
}
static unsigned int getLibAddr(){
unsigned int ret = 0;
char name[] = "libdemo.so";
char buf[4096], *temp;
int pid;
FILE *fp;
pid = getpid();
sprintf(buf, "/proc/%d/maps", pid);
fp = fopen(buf, "r");
if(fp == NULL)
{
puts("open failed");
goto _error;
}
while(fgets(buf, sizeof(buf), fp)){
if(strstr(buf, name)){
temp = strtok(buf, "-");
ret = strtoul(temp, NULL, 16);
break;
}
}
_error:
fclose(fp);
return ret;
}
static char getTargetFuncInfo(unsigned long base, const char *funcName, funcInfo *info){
char flag = -1, *dynstr;
int i;
Elf32_Ehdr *ehdr;
Elf32_Phdr *phdr;
Elf32_Off dyn_vaddr;
Elf32_Word dyn_size, dyn_strsz;
Elf32_Dyn *dyn;
Elf32_Addr dyn_symtab, dyn_strtab, dyn_hash;
Elf32_Sym *funSym;
unsigned funHash, nbucket;
unsigned *bucket, *chain;
ehdr = (Elf32_Ehdr *)base;
phdr = (Elf32_Phdr *)(base + ehdr->e_phoff);
// __android_log_print(ANDROID_LOG_INFO, "JNITag", "phdr = 0x%p, size = 0x%x\n", phdr, ehdr->e_phnum);
for (i = 0; i < ehdr->e_phnum; ++i) {
// __android_log_print(ANDROID_LOG_INFO, "JNITag", "phdr = 0x%p\n", phdr);
if(phdr->p_type == PT_DYNAMIC){
flag = 0;
print_debug("Find .dynamic segment");
break;
}
phdr ++;
}
if(flag)
goto _error;
dyn_vaddr = phdr->p_vaddr + base;
dyn_size = phdr->p_filesz;
__android_log_print(ANDROID_LOG_INFO, "JNITag", "dyn_vadd = 0x%x, dyn_size = 0x%x", dyn_vaddr, dyn_size);
flag = 0;
for (i = 0; i < dyn_size / sizeof(Elf32_Dyn); ++i) {
dyn = (Elf32_Dyn *)(dyn_vaddr + i * sizeof(Elf32_Dyn));
if(dyn->d_tag == DT_SYMTAB){
dyn_symtab = (dyn->d_un).d_ptr;
flag += 1;
__android_log_print(ANDROID_LOG_INFO, "JNITag", "Find .dynsym section, addr = 0x%x\n", dyn_symtab);
}
if(dyn->d_tag == DT_HASH){
dyn_hash = (dyn->d_un).d_ptr;
flag += 2;
__android_log_print(ANDROID_LOG_INFO, "JNITag", "Find .hash section, addr = 0x%x\n", dyn_hash);
}
if(dyn->d_tag == DT_STRTAB){
dyn_strtab = (dyn->d_un).d_ptr;
flag += 4;
__android_log_print(ANDROID_LOG_INFO, "JNITag", "Find .dynstr section, addr = 0x%x\n", dyn_strtab);
}
if(dyn->d_tag == DT_STRSZ){
dyn_strsz = (dyn->d_un).d_val;
flag += 8;
__android_log_print(ANDROID_LOG_INFO, "JNITag", "Find strsz size = 0x%x\n", dyn_strsz);
}
}
if((flag & 0x0f) != 0x0f){
print_debug("Find needed .section failed\n");
goto _error;
}
dyn_symtab += base;
dyn_hash += base;
dyn_strtab += base;
dyn_strsz += base;
funHash = elfhash(funcName);
funSym = (Elf32_Sym *) dyn_symtab;
dynstr = (char*) dyn_strtab;
nbucket = *((int *) dyn_hash);
bucket = (int *)(dyn_hash + 8);
chain = (unsigned int *)(dyn_hash + 4 * (2 + nbucket));
flag = -1;
__android_log_print(ANDROID_LOG_INFO, "JNITag", "hash = 0x%x, nbucket = 0x%x\n", funHash, nbucket);
int mod = (funHash % nbucket);
__android_log_print(ANDROID_LOG_INFO, "JNITag", "mod = %d\n", mod);
__android_log_print(ANDROID_LOG_INFO, "JNITag", "i = 0x%d\n", bucket[mod]);
for(i = bucket[mod]; i != 0; i = chain[i]){
__android_log_print(ANDROID_LOG_INFO, "JNITag", "Find index = %d\n", i);
if(strcmp(dynstr + (funSym + i)->st_name, funcName) == 0){
flag = 0;
__android_log_print(ANDROID_LOG_INFO, "JNITag", "Find %s\n", funcName);
break;
}
}
if(flag) goto _error;
info->st_value = (funSym + i)->st_value;
info->st_size = (funSym + i)->st_size;
__android_log_print(ANDROID_LOG_INFO, "JNITag", "st_value = %d, st_size = %d", info->st_value, info->st_size);
return 0;
_error:
return -1;
}
void init_getString(){
const char target_fun[] = "Java_com_example_shelldemo2_MainActivity_getString";
funcInfo info;
int i;
unsigned int npage, base = getLibAddr();
__android_log_print(ANDROID_LOG_INFO, "JNITag", "base addr = 0x%x", base);
if(getTargetFuncInfo(base, target_fun, &info) == -1){
print_debug("Find Java_com_example_shelldemo2_MainActivity_getString failed");
return ;
}
npage = info.st_size / PAGE_SIZE + ((info.st_size % PAGE_SIZE == 0) ? 0 : 1);
__android_log_print(ANDROID_LOG_INFO, "JNITag", "npage = 0x%d", npage);
__android_log_print(ANDROID_LOG_INFO, "JNITag", "npage = 0x%d", PAGE_SIZE);
if(mprotect((void *) ((base + info.st_value) / PAGE_SIZE * PAGE_SIZE), 4096*npage, PROT_READ | PROT_EXEC | PROT_WRITE) != 0){
print_debug("mem privilege change failed");
}
for(i=0;i< info.st_size - 1; i++){
char *addr = (char*)(base + info.st_value -1 + i);
*addr = ~(*addr);
}
if(mprotect((void *) ((base + info.st_value) / PAGE_SIZE * PAGE_SIZE), 4096*npage, PROT_READ | PROT_EXEC) != 0){
print_debug("mem privilege change failed");
}
}
JNIEXPORT jstring JNICALL
Java_com_example_shelldemo2_MainActivity_getString( JNIEnv* env,
jobject thiz )
{
#if defined(__arm__)
#if defined(__ARM_ARCH_7A__)
#if defined(__ARM_NEON__)
#define ABI "armeabi-v7a/NEON"
#else
#define ABI "armeabi-v7a"
#endif
#else
#define ABI "armeabi"
#endif
#elif defined(__i386__)
#define ABI "x86"
#elif defined(__mips__)
#define ABI "mips"
#else
#define ABI "unknown"
#endif
return (*env)->NewStringUTF(env, "Native method return!");
}
这里就不想做太多解释了,代码逻辑和之前文章中的加密section中的代码类似,只有在寻找函数的地方有点不同,这个也不再这里说明了,在加密的代码中我在说明一下。
第二、加密程序1、Java版本加密程序
private static void encodeFunc(byte[] fileByteArys){
//寻找Dynamic段的偏移值和大小
int dy_offset = 0,dy_size = 0;
for(elf32_phdr phdr : type_32.phdrList){
if(Utils.byte2Int(phdr.p_type) == ElfType32.PT_DYNAMIC){
dy_offset = Utils.byte2Int(phdr.p_offset);
dy_size = Utils.byte2Int(phdr.p_filesz);
}
}
System.out.println("dy_size:"+dy_size);
int dynSize = 8;
int size = dy_size / dynSize;
System.out.println("size:"+size);
byte[] dest = new byte[dynSize];
for(int i=0;i<size;i++){
System.arraycopy(fileByteArys, i*dynSize + dy_offset, dest, 0, dynSize);
type_32.dynList.add(parseDynamic(dest));
}
//type_32.printDynList();
byte[] symbolStr = null;
int strSize=0,strOffset=0;
int symbolOffset = 0;
int dynHashOffset = 0;
int funcIndex = 0;
int symbolSize = 16;
for(elf32_dyn dyn : type_32.dynList){
if(Utils.byte2Int(dyn.d_tag) == ElfType32.DT_HASH){
dynHashOffset = Utils.byte2Int(dyn.d_ptr);
}else if(Utils.byte2Int(dyn.d_tag) == ElfType32.DT_STRTAB){
System.out.println("strtab:"+dyn);
strOffset = Utils.byte2Int(dyn.d_ptr);
}else if(Utils.byte2Int(dyn.d_tag) == ElfType32.DT_SYMTAB){
System.out.println("systab:"+dyn);
symbolOffset = Utils.byte2Int(dyn.d_ptr);
}else if(Utils.byte2Int(dyn.d_tag) == ElfType32.DT_STRSZ){
System.out.println("strsz:"+dyn);
strSize = Utils.byte2Int(dyn.d_val);
}
}
symbolStr = Utils.copyBytes(fileByteArys, strOffset, strSize);
//打印所有的Symbol Name,注意用0来进行分割,C中的字符串都是用0做结尾的
/*String[] strAry = new String(symbolStr).split(new String(new byte[]{0}));
for(String str : strAry){
System.out.println(str);
}*/
for(elf32_dyn dyn : type_32.dynList){
if(Utils.byte2Int(dyn.d_tag) == ElfType32.DT_HASH){
//这里的逻辑有点绕
/**
* 根据hash值,找到下标hash % nbuckets的bucket;根据bucket中的值,读取.dynsym中的对应索引的Elf32_Sym符号;
* 从符号的st_name所以找到在.dynstr中对应的字符串与函数名进行比较。若不等,则根据chain[hash % nbuckets]找下一个Elf32_Sym符号,
* 直到找到或者chain终止为止。这里叙述得有些复杂,直接上代码。
for(i = bucket[funHash % nbucket]; i != 0; i = chain[i]){
if(strcmp(dynstr + (funSym + i)->st_name, funcName) == 0){
flag = 0;
break;
}
}
*/
int nbucket = Utils.byte2Int(Utils.copyBytes(fileByteArys, dynHashOffset, 4));
int nchian = Utils.byte2Int(Utils.copyBytes(fileByteArys, dynHashOffset+4, 4));
int hash = (int)elfhash(funcName.getBytes());
hash = (hash % nbucket);
//这里的8是读取nbucket和nchian的两个值
funcIndex = Utils.byte2Int(Utils.copyBytes(fileByteArys, dynHashOffset+hash*4 + 8, 4));
System.out.println("nbucket:"+nbucket+",hash:"+hash+",funcIndex:"+funcIndex+",chian:"+nchian);
System.out.println("sym:"+Utils.bytes2HexString(Utils.int2Byte(symbolOffset)));
System.out.println("hash:"+Utils.bytes2HexString(Utils.int2Byte(dynHashOffset)));
byte[] des = new byte[symbolSize];
System.arraycopy(fileByteArys, symbolOffset+funcIndex*symbolSize, des, 0, symbolSize);
Elf32_Sym sym = parseSymbolTable(des);
System.out.println("sym:"+sym);
boolean isFindFunc = Utils.isEqualByteAry(symbolStr, Utils.byte2Int(sym.st_name), funcName);
if(isFindFunc){
System.out.println("find func....");
return;
}
while(true){
/**
* lseek(fd, dyn_hash + 4 * (2 + nbucket + funIndex), SEEK_SET);
if(read(fd, &funIndex, 4) != 4){
puts("Read funIndex failed\n");
goto _error;
}
*/
//System.out.println("dyHash:"+Utils.bytes2HexString(Utils.int2Byte(dynHashOffset))+",nbucket:"+nbucket+",funIndex:"+funcIndex);
funcIndex = Utils.byte2Int(Utils.copyBytes(fileByteArys, dynHashOffset+4*(2+nbucket+funcIndex), 4));
System.out.println("funcIndex:"+funcIndex);
System.arraycopy(fileByteArys, symbolOffset+funcIndex*symbolSize, des, 0, symbolSize);
sym = parseSymbolTable(des);
isFindFunc = Utils.isEqualByteAry(symbolStr, Utils.byte2Int(sym.st_name), funcName);
if(isFindFunc){
System.out.println("find func...");
int funcSize = Utils.byte2Int(sym.st_size);
int funcOffset = Utils.byte2Int(sym.st_value);
System.out.println("size:"+funcSize+",funcOffset:"+funcOffset);
//进行目标函数代码部分进行加密
//这里需要注意的是从funcOffset-1的位置开始
byte[] funcAry = Utils.copyBytes(fileByteArys, funcOffset-1, funcSize);
for(int i=0;i<funcAry.length-1;i++){
funcAry[i] = (byte)(funcAry[i] ^ 0xFF);
}
Utils.replaceByteAry(fileByteArys, funcOffset-1, funcAry);
break;
}
}
break;
}
}
}
这里的解密程序,需要说明一下。1)、定位到.dynamic的segment,解析成elf32_dyn结构信息
//寻找Dynamic段的偏移值和大小
int dy_offset = 0,dy_size = 0;
for(elf32_phdr phdr : type_32.phdrList){
if(Utils.byte2Int(phdr.p_type) == ElfType32.PT_DYNAMIC){
dy_offset = Utils.byte2Int(phdr.p_offset);
dy_size = Utils.byte2Int(phdr.p_filesz);
}
}
System.out.println("dy_size:"+dy_size);
int dynSize = 8;
int size = dy_size / dynSize;
System.out.println("size:"+size);
byte[] dest = new byte[dynSize];
for(int i=0;i<size;i++){
System.arraycopy(fileByteArys, i*dynSize + dy_offset, dest, 0, dynSize);
type_32.dynList.add(parseDynamic(dest));
}
这里有一个解析elf32_dyn结构:
[cpp] view plaincopy
private static elf32_dyn parseDynamic(byte[] src){
elf32_dyn dyn = new elf32_dyn();
dyn.d_tag = Utils.copyBytes(src, 0, 4);
dyn.d_ptr = Utils.copyBytes(src, 4, 4);
dyn.d_val = Utils.copyBytes(src, 4, 4);
return dyn;
}
这里需要注意的是,elf32_dyn中用到了联合体union结构,Java中是不存在这个类型的,所以我们需要了解这个联合体的含义,这里虽然是三个字段,但是大小是8个字节,而不是12字节,这个需要注意的。dyn.d_val和dyn.d_val是在一个联合体中的。2)、计算目标函数的hash值,得到函数的偏移值和大小
[java] view plaincopy
for(elf32_dyn dyn : type_32.dynList){
if(Utils.byte2Int(dyn.d_tag) == ElfType32.DT_HASH){
//这里的逻辑有点绕
/**
* 根据hash值,找到下标hash % nbuckets的bucket;根据bucket中的值,读取.dynsym中的对应索引的Elf32_Sym符号;
* 从符号的st_name所以找到在.dynstr中对应的字符串与函数名进行比较。若不等,则根据chain[hash % nbuckets]找下一个Elf32_Sym符号,
* 直到找到或者chain终止为止。这里叙述得有些复杂,直接上代码。
for(i = bucket[funHash % nbucket]; i != 0; i = chain[i]){
if(strcmp(dynstr + (funSym + i)->st_name, funcName) == 0){
flag = 0;
break;
}
}
*/
int nbucket = Utils.byte2Int(Utils.copyBytes(fileByteArys, dynHashOffset, 4));
int nchian = Utils.byte2Int(Utils.copyBytes(fileByteArys, dynHashOffset+4, 4));
int hash = (int)elfhash(funcName.getBytes());
hash = (hash % nbucket);
//这里的8是读取nbucket和nchian的两个值
funcIndex = Utils.byte2Int(Utils.copyBytes(fileByteArys, dynHashOffset+hash*4 + 8, 4));
System.out.println("nbucket:"+nbucket+",hash:"+hash+",funcIndex:"+funcIndex+",chian:"+nchian);
System.out.println("sym:"+Utils.bytes2HexString(Utils.int2Byte(symbolOffset)));
System.out.println("hash:"+Utils.bytes2HexString(Utils.int2Byte(dynHashOffset)));
byte[] des = new byte[symbolSize];
System.arraycopy(fileByteArys, symbolOffset+funcIndex*symbolSize, des, 0, symbolSize);
Elf32_Sym sym = parseSymbolTable(des);
System.out.println("sym:"+sym);
boolean isFindFunc = Utils.isEqualByteAry(symbolStr, Utils.byte2Int(sym.st_name), funcName);
if(isFindFunc){
System.out.println("find func....");
return;
}
while(true){
/**
* lseek(fd, dyn_hash + 4 * (2 + nbucket + funIndex), SEEK_SET);
if(read(fd, &funIndex, 4) != 4){
puts("Read funIndex failed\n");
goto _error;
}
*/
//System.out.println("dyHash:"+Utils.bytes2HexString(Utils.int2Byte(dynHashOffset))+",nbucket:"+nbucket+",funIndex:"+funcIndex);
funcIndex = Utils.byte2Int(Utils.copyBytes(fileByteArys, dynHashOffset+4*(2+nbucket+funcIndex), 4));
System.out.println("funcIndex:"+funcIndex);
System.arraycopy(fileByteArys, symbolOffset+funcIndex*symbolSize, des, 0, symbolSize);
sym = parseSymbolTable(des);
isFindFunc = Utils.isEqualByteAry(symbolStr, Utils.byte2Int(sym.st_name), funcName);
if(isFindFunc){
System.out.println("find func...");
int funcSize = Utils.byte2Int(sym.st_size);
int funcOffset = Utils.byte2Int(sym.st_value);
System.out.println("size:"+funcSize+",funcOffset:"+funcOffset);
//进行目标函数代码部分进行加密
//这里需要注意的是从funcOffset-1的位置开始
byte[] funcAry = Utils.copyBytes(fileByteArys, funcOffset-1, funcSize);
for(int i=0;i<funcAry.length-1;i++){
funcAry[i] = (byte)(funcAry[i] ^ 0xFF);
}
Utils.replaceByteAry(fileByteArys, funcOffset-1, funcAry);
break;
}
}
break;
}
}
这里的寻找逻辑有点饶人,但是我们知道了解原理即可:
结合上面的这张图就可以理解了。其中nbucket和nchain,bucket[i]和chain[i]都是4个字节。他们的值就是目标函数在.dynsym中的位置。
2、C版加密程序
#include <stdio.h>
#include <fcntl.h>
#include "elf.h"
#include <stdlib.h>
#include <string.h>
typedef struct _funcInfo{
Elf32_Addr st_value;
Elf32_Word st_size;
}funcInfo;
Elf32_Ehdr ehdr;
//For Test
static void print_all(char *str, int len){
int i;
for(i=0;i<len;i++)
{
if(str[i] == 0)
puts("");
else
printf("%c", str[i]);
}
}
static unsigned elfhash(const char *_name)
{
const unsigned char *name = (const unsigned char *) _name;
unsigned h = 0, g;
while(*name) {
h = (h << 4) + *name++;
g = h & 0xf0000000;
h ^= g;
h ^= g >> 24;
}
return h;
}
static Elf32_Off findTargetSectionAddr(const int fd, const char *secName){
Elf32_Shdr shdr;
char *shstr = NULL;
int i;
lseek(fd, 0, SEEK_SET);
if(read(fd, &ehdr, sizeof(Elf32_Ehdr)) != sizeof(Elf32_Ehdr)){
puts("Read ELF header error");
goto _error;
}
lseek(fd, ehdr.e_shoff + sizeof(Elf32_Shdr) * ehdr.e_shstrndx, SEEK_SET);
if(read(fd, &shdr, sizeof(Elf32_Shdr)) != sizeof(Elf32_Shdr)){
puts("Read ELF section string table error");
goto _error;
}
if((shstr = (char *) malloc(shdr.sh_size)) == NULL){
puts("Malloc space for section string table failed");
goto _error;
}
lseek(fd, shdr.sh_offset, SEEK_SET);
if(read(fd, shstr, shdr.sh_size) != shdr.sh_size){
puts(shstr);
puts("Read string table failed");
goto _error;
}
lseek(fd, ehdr.e_shoff, SEEK_SET);
for(i = 0; i < ehdr.e_shnum; i++){
if(read(fd, &shdr, sizeof(Elf32_Shdr)) != sizeof(Elf32_Shdr)){
puts("Find section .text procedure failed");
goto _error;
}
if(strcmp(shstr + shdr.sh_name, secName) == 0){
printf("Find section %s, addr = 0x%x\n", secName, shdr.sh_offset);
break;
}
}
free(shstr);
return shdr.sh_offset;
_error:
return -1;
}
static char getTargetFuncInfo(int fd, const char *funcName, funcInfo *info){
char flag = -1, *dynstr;
int i;
Elf32_Sym funSym;
Elf32_Phdr phdr;
Elf32_Off dyn_off;
Elf32_Word dyn_size, dyn_strsz;
Elf32_Dyn dyn;
Elf32_Addr dyn_symtab, dyn_strtab, dyn_hash;
unsigned funHash, nbucket, nchain, funIndex;
lseek(fd, ehdr.e_phoff, SEEK_SET);
for(i=0;i < ehdr.e_phnum; i++){
if(read(fd, &phdr, sizeof(Elf32_Phdr)) != sizeof(Elf32_Phdr)){
puts("Read segment failed");
goto _error;
}
if(phdr.p_type == PT_DYNAMIC){
dyn_size = phdr.p_filesz;
dyn_off = phdr.p_offset;
flag = 0;
printf("Find section %s, size = 0x%x, addr = 0x%x\n", ".dynamic", dyn_size, dyn_off);
break;
}
}
if(flag){
puts("Find .dynamic failed");
goto _error;
}
flag = 0;
printf("dyn_size:%d\n",dyn_size);
printf("count:%d\n",(dyn_size/sizeof(Elf32_Dyn)));
printf("off:%x\n",dyn_off);
lseek(fd, dyn_off, SEEK_SET);
for(i=0;i < dyn_size / sizeof(Elf32_Dyn); i++){
int sizes = read(fd, &dyn, sizeof(Elf32_Dyn));
if(sizes != sizeof(Elf32_Dyn)){
puts("Read .dynamic information failed");
//goto _error;
break;
}
if(dyn.d_tag == DT_SYMTAB){
dyn_symtab = dyn.d_un.d_ptr;
flag += 1;
printf("Find .dynsym, addr = 0x%x, val = 0x%x\n", dyn_symtab, dyn.d_un.d_val);
}
if(dyn.d_tag == DT_HASH){
dyn_hash = dyn.d_un.d_ptr;
flag += 2;
printf("Find .hash, addr = 0x%x\n", dyn_hash);
}
if(dyn.d_tag == DT_STRTAB){
dyn_strtab = dyn.d_un.d_ptr;
flag += 4;
printf("Find .dynstr, addr = 0x%x\n", dyn_strtab);
}
if(dyn.d_tag == DT_STRSZ){
dyn_strsz = dyn.d_un.d_val;
flag += 8;
printf("Find .dynstr size, size = 0x%x\n", dyn_strsz);
}
}
if((flag & 0x0f) != 0x0f){
puts("Find needed .section failed\n");
goto _error;
}
dynstr = (char*) malloc(dyn_strsz);
if(dynstr == NULL){
puts("Malloc .dynstr space failed");
goto _error;
}
lseek(fd, dyn_strtab, SEEK_SET);
if(read(fd, dynstr, dyn_strsz) != dyn_strsz){
puts("Read .dynstr failed");
goto _error;
}
funHash = elfhash(funcName);
printf("Function %s hashVal = 0x%x\n", funcName, funHash);
lseek(fd, dyn_hash, SEEK_SET);
if(read(fd, &nbucket, 4) != 4){
puts("Read hash nbucket failed\n");
goto _error;
}
printf("nbucket = %d\n", nbucket);
if(read(fd, &nchain, 4) != 4){
puts("Read hash nchain failed\n");
goto _error;
}
printf("nchain = %d\n", nchain);
funHash = funHash % nbucket;
printf("funHash mod nbucket = %d \n", funHash);
lseek(fd, funHash * 4, SEEK_CUR);
if(read(fd, &funIndex, 4) != 4){
puts("Read funIndex failed\n");
goto _error;
}
printf("funcIndex:%d\n", funIndex);
lseek(fd, dyn_symtab + funIndex * sizeof(Elf32_Sym), SEEK_SET);
if(read(fd, &funSym, sizeof(Elf32_Sym)) != sizeof(Elf32_Sym)){
puts("Read funSym failed");
goto _error;
}
if(strcmp(dynstr + funSym.st_name, funcName) != 0){
while(1){
printf("hash:%x,nbucket:%d,funIndex:%d\n",dyn_hash,nbucket,funIndex);
lseek(fd, dyn_hash + 4 * (2 + nbucket + funIndex), SEEK_SET);
if(read(fd, &funIndex, 4) != 4){
puts("Read funIndex failed\n");
goto _error;
}
printf("funcIndex:%d\n", funIndex);
if(funIndex == 0){
puts("Cannot find funtion!\n");
goto _error;
}
lseek(fd, dyn_symtab + funIndex * sizeof(Elf32_Sym), SEEK_SET);
if(read(fd, &funSym, sizeof(Elf32_Sym)) != sizeof(Elf32_Sym)){
puts("In FOR loop, Read funSym failed");
goto _error;
}
if(strcmp(dynstr + funSym.st_name, funcName) == 0){
break;
}
}
}
printf("Find: %s, offset = 0x%x, size = 0x%x\n", funcName, funSym.st_value, funSym.st_size);
info->st_value = funSym.st_value;
info->st_size = funSym.st_size;
free(dynstr);
return 0;
_error:
free(dynstr);
return -1;
}
int main(int argc, char **argv){
char secName[] = ".text";
char funcName[] = "Java_com_example_shelldemo2_MainActivity_getString";
char *soName = "libdemo.so";
char *content = NULL;
int fd, i;
Elf32_Off secOff;
funcInfo info;
unsigned a = elfhash(funcName);
printf("a:%d\n", a);
fd = open(soName, O_RDWR);
if(fd < 0){
printf("open %s failed\n", argv[1]);
goto _error;
}
secOff = findTargetSectionAddr(fd, secName);
if(secOff == -1){
printf("Find section %s failed\n", secName);
goto _error;
}
if(getTargetFuncInfo(fd, funcName, &info) == -1){
printf("Find function %s failed\n", funcName);
goto _error;
}
content = (char*) malloc(info.st_size);
if(content == NULL){
puts("Malloc space failed");
goto _error;
}
lseek(fd, info.st_value - 1, SEEK_SET);
if(read(fd, content, info.st_size) != info.st_size){
puts("Malloc space failed");
goto _error;
}
for(i=0;i<info.st_size -1;i++){
content[i] = ~content[i];
}
lseek(fd, info.st_value-1, SEEK_SET);
if(write(fd, content, info.st_size) != info.st_size){
puts("Write modified content to .so failed");
goto _error;
}
puts("Complete!");
_error:
free(content);
close(fd);
return 0;
}
这里就不做介绍了。
上面对so中的函数加密成功了,那么下面我们来验证加密,我们使用IDA进行查看:
看到我们加密的函数内容已经面目全非了,看不到信息了。比较加密前的:
哈哈,加密成功了~~
第三、测试Android项目我们用加密之后的so文件来测试一下:
package com.example.shelldemo;
import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.TextView;
public class MainActivity extends Activity {
private TextView tv;
private native String getString();
static{
System.loadLibrary("demo");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tv = (TextView) findViewById(R.id.tv);
tv.setText(getString());
}
}
运行结果:
运行成功啦。
这里详细介绍了具体的实现方案和原理,这里就不做太多的介绍了,后续我将开始介绍破解的相关知识,那时候我们会发现,我们做的这些加固其实并没有什么卵用,所以在研究逆向的时候,弄不好会变疯的,因为在你研究完了加固之后,还要去破解它,没有绝对的安全,只有相对的攻防。
弹出键盘布局闪动原理和解决
在开发中,遇到一个问题:做一个微信一样,表情输入和软键盘在切换的时候,聊天界面不闪动的问题。为了解决这个问题,需要知道一下Android的软键盘弹出的时候发生的几个变化。
当AndroidMainfest.xml 中配置android:windowSoftInputMode="adjustResize|stateHidden" 属性后,如果弹出软键盘,那么会重绘界面。基本流程如下(API 10):
1. Android 收到打开软键盘命令
2. Android 打开软键盘后,调用App 注册在AWM 中的接口,告知它,界面需要进行变化.
2.1 调用ViewRoot.java#W#resized(w,h)
2.2 调用viewRoot.dispatchResized()
2.3 构造Message msg = obtainMessage(reportDraw ? RESIZED_REPORT :RESIZED),然后post过去
3. 在RootView的handleMessage的case RESIZED_REPORT: 收到具体的大小,配置App Window的大小,特别是 bottom 的大小, 最后调用requestLayout进行重绘
在上述的过程中,我们可以发现,其实弹出键盘之后的界面闪动的核心是在于app 的window botton 属性进行变化了,从而导致整个ViewTree的高度变化。那么我们只要表情PANEL在父布局onMeasure之前,进行VISIBLE或者GONE处理,使得最终的所有子View的高度满足window height,既可以实现不闪动聊天页面。
其核心思想为:在父布局onMeasure之前,隐藏/显示 合适高度的VIEW,既可以使得其他子VIEW高度不变化,从而避免界面闪动。引用自http://blog.dreamtobe.cn/2015/09/01/keyboard-panel-switch/。
代码如下:
Layout:
<com.test.MyActivity.MyLineLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:id="@+id/mll_main"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:background="#f3f3f3"
android:id="@+id/fl_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<TextView
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="DARKGEM"/>
<LinearLayout
android:id="@+id/ll_edit"
android:layout_width="match_parent"
android:orientation="horizontal"
android:layout_height="50dp">
<EditText
android:id="@+id/et_input"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"/>
<Button
android:id="@+id/btn_trigger"
android:text="trigger"
android:layout_width="100dp"
android:layout_height="match_parent"/>
<FrameLayout
android:id="@+id/fl_panel"
android:background="#CCCCCC"
android:layout_width="match_parent"
android:layout_height="0dp"/>
Activity:
package com.test;
import android.app.Activity;
import android.content.Context;
import android.graphics.Rect;
import android.os.Bundle;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
/**
*
* 聊天界面布局闪动处理, 基本原理如下: * 1. 弹出键盘的时候,会导致 RootView 的bottom 变小,直到容纳 键盘+虚拟按键 * 2. 收回键盘的时候,会导致 RootView的bottom 变大,直到容纳 虚拟键盘 * 3. 因为RootView bottom的变化,会导致整个布局高度(bottom - top)的变化,所以就会发生布局闪动的情况. 而为了 * 避免这种情况,只需要在发生变动的父布局调用 onMeasure() 之前,将子View的高度和配置为最终高度,既可以实现弹 * 出/收回键盘 不闪动的效果(如微信聊天界面)。 *
*/
public class MyActivity extends Activity {
MyLineLayout mll_main;
FrameLayout fl_list;
LinearLayout ll_edit;
EditText et_input;
Button btn_trigger;
FrameLayout fl_panel;
Rect rect = new Rect();
enum State {
//空状态
NONE,
//打开输入法状态
KEYBOARD,
//打开面板状态
PANEL,
}
State state = State.NONE;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mll_main = (MyLineLayout) findViewById(R.id.mll_main);
fl_list = (FrameLayout) findViewById(R.id.fl_list);
ll_edit = (LinearLayout) findViewById(R.id.ll_edit);
et_input = (EditText) findViewById(R.id.et_input);
btn_trigger = (Button) findViewById(R.id.btn_trigger);
fl_panel = (FrameLayout) findViewById(R.id.fl_panel);
mll_main.onMeasureListener = new MyLineLayout.OnMeasureListener() {
/**
* 可能会发生多次 调用的情况,因为存在 layout_weight 属性,需要2次测试,给定最终大小
* */
@Override
public void onMeasure(int maxHeight, int oldHeight, int nowHeight) {
switch (state) {
case NONE: {
}
break;
case PANEL: {
//state 处于 panel 状态只有一种可能,就是主动点击切换到panel,
//1.如果之前是keyboard状态,则在本次onMeasure的时候,一定要把panel显示出来
//避免 mll 刷动
//2. 如果之前处于 none状态,那么本次触发来自于 postDelay,可以忽略
fl_panel.setVisibility(View.VISIBLE);
}
break;
case KEYBOARD: {
//state = KEYBOARD 状态,只有一种可能,就是主动点击了 EditText
//1. 如果之前是panel状态,则一般已经有了固有高度,这个高度刚刚好满足键盘的高度,那么只用隐藏掉
//panel 既可以实现页面不进行刷新
//2. 如果之前为none状态,则可以忽略
fl_panel.setVisibility(View.GONE);
//处于键盘状态,需要更新键盘高度为面板的高度
if (oldHeight >= nowHeight) {
//记录当前的缩放大小为键盘大小
int h = maxHeight - nowHeight;
//避免 输入法 悬浮状态, 保留一个最低高度
if (h < 500) {
h = 500;
}
fl_panel.getLayoutParams().height = h;
}
}
break;
}
Log.d("SC_SIZE", String.format("onMeasure %d %d %d", maxHeight, nowHeight, oldHeight));
}
};
fl_list.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
hideSoftInputView();
fl_panel.setVisibility(View.GONE);
state = State.NONE;
return false;
}
});
et_input.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
state = State.KEYBOARD;
}
});
btn_trigger.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
switch (state) {
case NONE:
case KEYBOARD: {
hideSoftInputView();
state = State.PANEL;
//无论App 处于什么状态,都追加一个 显示 panel 的方法,避免处于非正常状态无法打开panel
getWindow().getDecorView().postDelayed(new Runnable() {
@Override
public void run() {
fl_panel.setVisibility(View.VISIBLE);
}
}, 100);
}
break;
case PANEL: {
state = State.NONE;
fl_panel.setVisibility(View.GONE);
}
break;
}
}
});
//设置基本panel 高度,以使得第一次能正常打开panel
getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);
fl_panel.getLayoutParams().height = rect.height() / 2;
fl_panel.setVisibility(View.GONE);
}
/**
* 隐藏软键盘输入
*/
public void hideSoftInputView() {
InputMethodManager manager = ((InputMethodManager) this.getSystemService(Activity.INPUT_METHOD_SERVICE));
if (getWindow().getAttributes().softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN) {
if (getCurrentFocus() != null && manager != null)
manager.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
}
}
/**
* Created by Administrator on 2015/11/20.
*/
public static class MyLineLayout extends LinearLayout {
OnMeasureListener onMeasureListener;
int maxHeight = 0;
int oldHeight;
public MyLineLayout(Context context) {
super(context);
}
public MyLineLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int height = MeasureSpec.getSize(heightMeasureSpec);
if (onMeasureListener != null) {
onMeasureListener.onMeasure(maxHeight, oldHeight, height);
}
oldHeight = height;
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
//之所以,在这里记录 maxHeight的大小,是因为 onMeasure 中可能多次调用,中间可能会逐步出现 ActionBar,BottomVirtualKeyboard,
//所以 onMeasure中获取的maxHeight存在误差
if (h > maxHeight) {
maxHeight = h;
}
Log.d("SC_SIZE", String.format("Size Change %d %d", h, oldh));
}
interface OnMeasureListener {
void onMeasure(int maxHeight, int oldHeight, int nowHeight);
}
}
}
测试效果图:
解决Andriod软键盘出现把原来的布局给顶上去的方法
决方法,在mainfest.xml中,对那个Activity加:
就不会把原来Activity的布局给顶上去了。
今天要做一个搜索功能,搜索界面采用AutoCompleteTextView做搜索条,然后下面用listview来显示搜索结果,而我的主界面是在底部用tab做了一个主界面导航,其中有一个搜索按钮,因为在搜索条中输入文字的时候会弹出软件盘,但是如果不做什么设置的话,软键盘弹出来的同时,会把我下面的tab导航给相应拉到屏幕的上面,界面显示的扭曲啊,后来找到一种解决方法,在相应的activity中(比如我这是tab的activity,用的是adjustpan)添加
android:windowSoftInputMode这个属性,下面详细说下这个属性:
& X! Q6c9 }% i. ]6 @0 Y" N6^ d {"X
windowSoftInputMode属性设置值说明。
attributes:
android:windowSoftInputMode
活动的主窗口如何与包含屏幕上的软键盘窗口交互。这个属性的设置将会影响两件事情7S7 U+ S! p7 s( U) n: t: m& N
:
1>
软键盘的状态——是否它是隐藏或显示——当活动5w$ r- U9 i" h. O' M" M
(Activity)成为用户关注的焦点。
2>
活动的主窗口调整——是否减少活动主窗口大小以便腾出空间放软键盘或是否当活动窗口的部分被软键盘覆盖时它的内容的当前焦点是可见的。
它的设置必须是下面列表中的一个值,或一个
”state…”值加一个+ s. Z" m5 u: {; k; B7v4 Q
”adjust…”值的组合。在任一组设置多个值——多个
”state…”values,例如&
mdash有未定义的结果。各个值之间用+H8 v$ Q# ~5 f3 B& `- G8 c$ y
|分开。例如
:
在这设置的值8A: N! L' x0 `: C
(除'H0 N" g, w2 W) K F# y2 l!c
"stateUnspecified"和
"adjustUnspecified"以外3 ^, p2E G: I2 y/ V
)将覆盖在主题中设置的值
将覆盖在主题中设置的值
值 | 描述 |
"stateUnspecified" | 软键盘的状态 |
"stateUnchanged" | 软键盘被保持无论它上次是什么状态,是否可见或隐藏,当主窗口出现在前面时。 |
"stateHidden" | 当用户选择该 |
"stateAlwaysHidden" | 软键盘总是被隐藏的,当该 |
"stateVisible" | 软键盘是可见的,当那个是正常合适的时& d% G.y8 [; G; _: v |
"stateAlwaysVisible" | 当用户选择这个6 Z%C |
"adjustUnspecified" | 它不被指定是否该" H9 b! V3 h5_& O$ d$ M |
"adjustResize" | 该/ M" R: m- W( Z. Q6 d*A |
"adjustPan" | 该 |
先看看效果图
Activity:
package com.example.editortoast; import android.app.Activity; import android.os.Bundle; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.widget.TextView; import android.widget.Toast; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); findViewById(R.id.bt).setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { toastShow(); } }); } private void toastShow() { LayoutInflater inflater = LayoutInflater.from(getApplicationContext()); View view = inflater.inflate(R.layout.item_toast, null); TextView textView1 = (TextView) view.findViewById(R.id.TextView_1); textView1.setText("Toast1"); Toast toast = new Toast(getApplicationContext()); toast.setGravity(Gravity.CENTER_VERTICAL, 0, 0); toast.setDuration(0); toast.setView(view); toast.show(); } }
activity_main.xml:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.example.editortoast.MainActivity" > <Button android:id="@+id/bt" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="点击" />
</RelativeLayout>
item_toast.xml:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <ImageView android:id="@+id/image" android:layout_width="80dp" android:layout_height="80dp" android:layout_centerVertical="true" android:src="@drawable/ic_launcher" /> <TextView android:id="@+id/TextView_1" android:textSize="30sp" android:textColor="@android:color/holo_red_light" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_toRightOf="@id/image" /> </RelativeLayout>
android 自定义Toast,可设定显示时间
开发android的同学可能会抱怨Toast设定显示的时长无效,只能是Toast.LENGTH_LONG 或者Toast.LENGTH_SHORT 之一,为了解决这些办法,有多种实现方式:
1.使用定时器,定时调用show()方法.
2.使用CountDownTimer类,也是调用show()方法.
3.使用WindownManager类实现.
本文使用方法三进行实现,难度不大,直接看代码吧.
package com.open.toast; import android.content.Context; import android.graphics.Color; import android.graphics.PixelFormat; import android.os.Handler; import android.view.Gravity; import android.view.View; import android.view.WindowManager; import android.widget.LinearLayout; import android.widget.TextView; /** * 自定义时长的Toast * @author DexYang * */ public class CToast { public static CToast makeText(Context context, CharSequence text, int duration) { CToast result = new CToast(context); LinearLayout mLayout=new LinearLayout(context); TextView tv = new TextView(context); tv.setText(text); tv.setTextColor(Color.WHITE); tv.setGravity(Gravity.CENTER); mLayout.setBackgroundResource(R.drawable.widget_toast_bg); int w=context.getResources().getDisplayMetrics().widthPixels / 2; int h=context.getResources().getDisplayMetrics().widthPixels / 10; mLayout.addView(tv, w, h); result.mNextView = mLayout; result.mDuration = duration; return result; } public static final int LENGTH_SHORT = 2000; public static final int LENGTH_LONG = 3500; private final Handler mHandler = new Handler(); private int mDuration=LENGTH_SHORT; private int mGravity = Gravity.CENTER; private int mX, mY; private float mHorizontalMargin; private float mVerticalMargin; private View mView; private View mNextView; private WindowManager mWM; private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams(); public CToast(Context context) { init(context); } /** * Set the view to show. * @see #getView */ public void setView(View view) { mNextView = view; } /** * Return the view. * @see #setView */ public View getView() { return mNextView; } /** * Set how long to show the view for. * @see #LENGTH_SHORT * @see #LENGTH_LONG */ public void setDuration(int duration) { mDuration = duration; } /** * Return the duration. * @see #setDuration */ public int getDuration() { return mDuration; } /** * Set the margins of the view. * * @param horizontalMargin The horizontal margin, in percentage of the * container width, between the container's edges and the * notification * @param verticalMargin The vertical margin, in percentage of the * container height, between the container's edges and the * notification */ public void setMargin(float horizontalMargin, float verticalMargin) { mHorizontalMargin = horizontalMargin; mVerticalMargin = verticalMargin; } /** * Return the horizontal margin. */ public float getHorizontalMargin() { return mHorizontalMargin; } /** * Return the vertical margin. */ public float getVerticalMargin() { return mVerticalMargin; } /** * Set the location at which the notification should appear on the screen. * @see android.view.Gravity * @see #getGravity */ public void setGravity(int gravity, int xOffset, int yOffset) { mGravity = gravity; mX = xOffset; mY = yOffset; } /** * Get the location at which the notification should appear on the screen. * @see android.view.Gravity * @see #getGravity */ public int getGravity() { return mGravity; } /** * Return the X offset in pixels to apply to the gravity's location. */ public int getXOffset() { return mX; } /** * Return the Y offset in pixels to apply to the gravity's location. */ public int getYOffset() { return mY; } /** * schedule handleShow into the right thread */ public void show() { mHandler.post(mShow); if(mDuration>0) { mHandler.postDelayed(mHide, mDuration); } } /** * schedule handleHide into the right thread */ public void hide() { mHandler.post(mHide); } private final Runnable mShow = new Runnable() { public void run() { handleShow(); } }; private final Runnable mHide = new Runnable() { public void run() { handleHide(); } }; private void init(Context context) { final WindowManager.LayoutParams params = mParams; params.height = WindowManager.LayoutParams.WRAP_CONTENT; params.width = WindowManager.LayoutParams.WRAP_CONTENT; params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; params.format = PixelFormat.TRANSLUCENT; params.windowAnimations = android.R.style.Animation_Toast; params.type = WindowManager.LayoutParams.TYPE_TOAST; params.setTitle("Toast"); mWM = (WindowManager) context.getApplicationContext() .getSystemService(Context.WINDOW_SERVICE); } private void handleShow() { if (mView != mNextView) { // remove the old view if necessary handleHide(); mView = mNextView; // mWM = WindowManagerImpl.getDefault(); final int gravity = mGravity; mParams.gravity = gravity; if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) { mParams.horizontalWeight = 1.0f; } if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) { mParams.verticalWeight = 1.0f; } mParams.x = mX; mParams.y = mY; mParams.verticalMargin = mVerticalMargin; mParams.horizontalMargin = mHorizontalMargin; if (mView.getParent() != null) { mWM.removeView(mView); } mWM.addView(mView, mParams); } } private void handleHide() { if (mView != null) { if (mView.getParent() != null) { mWM.removeView(mView); } mView = null; } } }
测试类的代码如下:
package com.open.toast; import android.app.Activity; import android.os.Bundle; import android.text.TextUtils; import android.view.View; import android.widget.EditText; public class MainActivity extends Activity { private EditText mEditText; private CToast mCToast; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); init(); } private void init() { mEditText=(EditText)findViewById(R.id.timeEditText); findViewById(R.id.showToastBtn).setOnClickListener(listener); findViewById(R.id.hideToastBtn).setOnClickListener(listener); } private View.OnClickListener listener=new View.OnClickListener() { @Override public void onClick(View v) { switch(v.getId()) { case R.id.showToastBtn: if(null!=mCToast) { mCToast.hide(); } int time=TextUtils.isEmpty(mEditText.getText().toString())?CToast.LENGTH_SHORT:Integer.valueOf(mEditText.getText().toString()); mCToast=CToast.makeText(getApplicationContext(), "我来自CToast!",time); mCToast.show(); break; case R.id.hideToastBtn: if(null!=mCToast) { mCToast.hide(); } break; } } }; }
效果如下:
Android ScrollView滚动机制
我们都知道通过View#scrollTo(x,y)既可以实现将View滚动的效果,如果再添加Scroller类,就可以实现滚到效果。但是,这背后是如何实现的呢?这个问题涉及到View的绘图机制。我们先看看View的绘图的基本流程
(图片来自于网上比较常见的view绘图流程图)
关于三个阶段的简单描述:
1. measure:预估计ViewTree的各个View的占用空间。
2. layout : 确定ViewTree中各个View所处的空间位置,包括width,height,left,top,right,bottom
3. draw: 使用RootViewImpl中的一个surface.lockCanvas(dirty)对象作为画布,然ViewTree上所有的View都在这个Canvas上进行画图,
值得注意的是,Canvas通过getHeight() 和 getWidth()就是整个屏幕的真实大小。包括了通知栏(虽然在打印出来的ViewTree看不到,但是通过top属性,留下了一点空间给通知栏),标题栏,Content,底部虚拟按键等。
我们先看看mScrollX/mScrollY在代码中的注释:
mScrollX/mScrollY相对这个View的内容(文字,图片,子View)垂直/水平的像素偏移。如下图:
在设置mScrollX / mScrollY后,就可以滚动到指定的“内容",而mScrollX/mScrollY 就是相对于“内容”的偏移量,内容原点为(0,0)。
而这种内容大小以及偏移是如何发生的?在ViewGroup中,存在一个API drawChild(),这个函数主要完成对子View的空间大小的限制以及偏移,见如下的描述
protected boolean drawChild(Canvas canvas, View child, long drawingTime) { boolean more = false; //获取子View的空间大小 final int cl = child.mLeft; final int ct = child.mTop; final int cr = child.mRight; final int cb = child.mBottom; //通知子View进行判断是否完成滚动,这里就是通过Scroller代码实现滚动的关键点 child.computeScroll(); //获取最新的偏移量 final int sx = child.mScrollX; final int sy = child.mScrollY; //创建一个还原点 final int restoreTo = canvas.save(); //偏移,通过这个API,实现了scroll对内容偏移, 先把内容的原点进行偏移到负数区域 canvas.translate(cl - sx, ct - sy); //剪切,因为之前有一个translate操作,所有剪切出来的空间就是父View给定的可见区域 //所以如果子View填充Canvas的内容超出给定的空间,也不会显示出来 canvas.clipRect(sx, sy, sx + (cr - cl), sy + (cb - ct)); //让子View进行绘图,注意子View不用处理Scroll属性,既可以实现内容偏移 child.draw(canvas); //还原 canvas.restoreToCount(restoreTo); return more; }
值得注意的是,ListView不是采用这种机制实现的,而是采用替换ChildView来实现滑动效果的。
Android 嵌套滑动机制(NestedScrolling)
Android 在发布 Lollipop版本之后,为了更好的用户体验,Google为Android的滑动机制提供了NestedScrolling特性
NestedScrolling的特性可以体现在哪里呢?
比如你使用了Toolbar,下面一个ScrollView,向上滚动隐藏Toolbar,向下滚动显示Toolbar,这里在逻辑上就是一个NestedScrolling —— 因为你在滚动整个Toolbar在内的View的过程中,又嵌套滚动了里面的ScrollView。
效果如上图
在这之前,我们知道Android对Touch事件的分发是有自己一套机制的。主要是有是三个函数:
dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent。
这种分发机制有一个漏洞:
如果子view获得处理touch事件机会的时候,父view就再也没有机会去处理这个touch事件了,直到下一次手指再按下。
也就是说,我们在滑动子View的时候,如果子View对这个滑动事件不想要处理的时候,只能抛弃这个touch事件,而不会把这些传给父view去处理。
但是Google新的NestedScrolling机制就很好的解决了这个问题。
我们看看如何实现这个NestedScrolling,首先有几个类(接口)我们需要关注一下
NestedScrollingChild NestedScrollingParent NestedScrollingChildHelper NestedScrollingParentHelper
以上四个类都在support-v4包中提供,Lollipop的View默认实现了几种方法。
实现接口很简单,这边我暂时用到了NestedScrollingChild系列的方法(因为Parent是support-design提供的CoordinatorLayout)
@Override public void setNestedScrollingEnabled(boolean enabled) { super.setNestedScrollingEnabled(enabled); mChildHelper.setNestedScrollingEnabled(enabled); } @Override public boolean isNestedScrollingEnabled() { return mChildHelper.isNestedScrollingEnabled(); } @Override public boolean startNestedScroll(int axes) { return mChildHelper.startNestedScroll(axes); } @Override public void stopNestedScroll() { mChildHelper.stopNestedScroll(); } @Override public boolean hasNestedScrollingParent() { return mChildHelper.hasNestedScrollingParent(); } @Override public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) { return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow); } @Override public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); } @Override public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed); } @Override public boolean dispatchNestedPreFling(float velocityX, float velocityY) { return mChildHelper.dispatchNestedPreFling(velocityX, velocityY); }
对,简单的话你就这么实现就好了。
这些接口都是我们在需要的时候自己调用的。childHelper干了些什么事呢?,看一下startNestedScroll方法
/** * Start a new nested scroll for this view. * * <p>This is a delegate method. Call it from your {@link android.view.View View} subclass * method/{@link NestedScrollingChild} interface method with the same signature to implement * the standard policy.</p> * * @param axes Supported nested scroll axes. * See {@link NestedScrollingChild#startNestedScroll(int)}. * @return true if a cooperating parent view was found and nested scrolling started successfully */ public boolean startNestedScroll(int axes) { if (hasNestedScrollingParent()) { // Already in progress return true; } if (isNestedScrollingEnabled()) { ViewParent p = mView.getParent(); View child = mView; while (p != null) { if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) { mNestedScrollingParent = p; ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes); return true; } if (p instanceof View) { child = (View) p; } p = p.getParent(); } } return false; }
可以看到这里是帮你实现一些跟NestedScrollingParent交互的一些方法。
ViewParentCompat是一个和父view交互的兼容类,它会判断api version,如果在Lollipop以上,就是用view自带的方法,否则判断是否实现了NestedScrollingParent接口,去调用接口的方法。
那么具体我们怎么使用这一套机制呢?比如子View这时候我需要通知父view告诉它我有一个嵌套的touch事件需要我们共同处理。那么针对一个只包含scroll交互,它整个工作流是这样的:
一、startNestedScroll
首先子view需要开启整个流程(内部主要是找到合适的能接受nestedScroll的parent),通知父View,我要和你配合处理TouchEvent
二、dispatchNestedPreScroll
在子View的onInterceptTouchEvent或者onTouch中(一般在MontionEvent.ACTION_MOVE事件里),调用该方法通知父View滑动的距离。该方法的第三第四个参数返回父view消费掉的scroll长度和子View的窗体偏移量。如果这个scroll没有被消费完,则子view进行处理剩下的一些距离,由于窗体进行了移动,如果你记录了手指最后的位置,需要根据第四个参数offsetInWindow计算偏移量,才能保证下一次的touch事件的计算是正确的。
如果父view接受了它的滚动参数,进行了部分消费,则这个函数返回true,否则为false。
这个函数一般在子view处理scroll前调用。
三、dispatchNestedScroll
向父view汇报滚动情况,包括子view消费的部分和子view没有消费的部分。
如果父view接受了它的滚动参数,进行了部分消费,则这个函数返回true,否则为false。
这个函数一般在子view处理scroll后调用。
四、stopNestedScroll
结束整个流程。
整个对应流程是这样
子view | 父view |
---|---|
startNestedScroll | onStartNestedScroll、onNestedScrollAccepted |
dispatchNestedPreScroll | onNestedPreScroll |
dispatchNestedScroll | onNestedScroll |
stopNestedScroll | onStopNestedScroll |
一般是子view发起调用,父view接受回调。
我们最需要关注的是dispatchNestedPreScroll中的consumed参数。
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) ;
它是一个int型的数组,长度为2,第一个元素是父view消费的x方向的滚动距离;第二个元素是父view消费的y方向的滚动距离,如果这两个值不为0,则子view需要对滚动的量进行一些修正。正因为有了这个参数,使得我们处理滚动事件的时候,思路更加清晰,不会像以前一样被一堆的滚动参数搞混。
对NestedScroll的介绍暂时到这里,下一次将讲一下CoordinatorLayout的使用(其中让人较难理解的Behavior对象),以及在SegmentFault Android客户端中的实践。谢谢支持。
相关文章
- 有时为了网站安全和版权问题,会对自己写的php源码进行加密,在php加密技术上最常用的是zend公司的zend guard 加密软件,现在我们来图文讲解一下。 下面就简单说说如何...2016-11-25
- 下面我们来看一篇关于Android子控件超出父控件的范围显示出来方法,希望这篇文章能够帮助到各位朋友,有碰到此问题的朋友可以进来看看哦。 <RelativeLayout xmlns:an...2016-10-02
- ps软件是现在很多人都会使用到的,HSL面板在ps软件中又有着非常独特的作用。这次文章就给大家介绍下ps怎么使用HSL面板,还不知道使用方法的下面一起来看看。  ...2017-07-06
- 这篇文章主要介绍了C#实现HTTP下载文件的方法,包括了HTTP通信的创建、本地文件的写入等,非常具有实用价值,需要的朋友可以参考下...2020-06-25
- 许多的朋友对于Plesk控制面板应用不是非常的了解特别是英文版的Plesk控制面板,在这里小编整理了一些关于Plesk控制面板常用的使用方案整理,具体如下。 本文基于Linu...2016-10-10
使用insertAfter()方法在现有元素后添加一个新元素
复制代码 代码如下: //在现有元素后添加一个新元素 function insertAfter(newElement, targetElement){ var parent = targetElement.parentNode; if (parent.lastChild == targetElement){ parent.appendChild(newEl...2014-05-31Android开发中findViewById()函数用法与简化
findViewById方法在android开发中是获取页面控件的值了,有没有发现我们一个页面控件多了会反复研究写findViewById呢,下面我们一起来看它的简化方法。 Android中Fin...2016-09-20- 如果我们的项目需要做来电及短信的功能,那么我们就得在Android模拟器开发这些功能,本来就来告诉我们如何在Android模拟器上模拟来电及来短信的功能。 在Android模拟...2016-09-20
jQuery 1.9使用$.support替代$.browser的使用方法
jQuery 从 1.9 版开始,移除了 $.browser 和 $.browser.version , 取而代之的是 $.support 。 在更新的 2.0 版本中,将不再支持 IE 6/7/8。 以后,如果用户需要支持 IE 6/7/8,只能使用 jQuery 1.9。 如果要全面支持 IE,并混合...2014-05-31使用percona-toolkit操作MySQL的实用命令小结
1.pt-archiver 功能介绍: 将mysql数据库中表的记录归档到另外一个表或者文件 用法介绍: pt-archiver [OPTION...] --source DSN --where WHERE 这个工具只是归档旧的数据,不会对线上数据的OLTP查询造成太大影响,你可以将...2015-11-24- 大概有如下步骤 新建项目Bejs 新建文件package.json 新建文件Gruntfile.js 命令行执行grunt任务 一、新建项目Bejs源码放在src下,该目录有两个js文件,selector.js和ajax.js。编译后代码放在dest,这个grunt会...2014-06-07
如何使用php脚本给html中引用的js和css路径打上版本号
在搜索引擎中搜索关键字.htaccess 缓存,你可以搜索到很多关于设置网站文件缓存的教程,通过设置可以将css、js等不太经常更新的文件缓存在浏览器端,这样访客每次访问你的网站的时候,浏览器就可以从浏览器的缓存中获取css、...2015-11-24- C#注释的一些使用方法浅谈,需要的朋友可以参考一下...2020-06-25
- 夜神android模拟器如何设置代理呢?对于这个问题其实操作起来是非常的简单,下面小编来为各位详细介绍夜神android模拟器设置代理的方法,希望例子能够帮助到各位。 app...2016-09-20
- 为了增强android应用的用户体验,我们可以在一些Button按钮上自定义动态的设置一些样式,比如交互时改变字体、颜色、背景图等。 今天来看一个通过重写Button来动态实...2016-09-20
- 一、下载 mysqlsla [root@localhost tmp]# wget http://hackmysql.com/scripts/mysqlsla-2.03.tar.gz--19:45:45-- http://hackmysql.com/scripts/mysqlsla-2.03.tar.gzResolving hackmysql.com... 64.13.232.157Conn...2015-11-24
- 如果我们要在Android应用APP中加载html5页面,我们可以使用WebView,本文我们分享两个WebView加载html5页面实例应用。 实例一:WebView加载html5实现炫酷引导页面大多...2016-09-20
- 举一个案例:复制代码 代码如下:<?phpclass Downfile { function downserver($file_name){$file_path = "./img/".$file_name;//转码,文件名转为gb2312解决中文乱码$file_name = iconv("utf-8","gb2312",$file_name...2014-06-07
安装和使用percona-toolkit来辅助操作MySQL的基本教程
一、percona-toolkit简介 percona-toolkit是一组高级命令行工具的集合,用来执行各种通过手工执行非常复杂和麻烦的mysql和系统任务,这些任务包括: 检查master和slave数据的一致性 有效地对记录进行归档 查找重复的索...2015-11-24- 深入理解Android中View和ViewGroup从组成架构上看,似乎ViewGroup在View之上,View需要继承ViewGroup,但实际上不是这样的。View是基类,ViewGroup是它的子类。本教程我们深...2016-09-20