Ubuntu下用opengrok部署Android源码
前言
浏览代码的工具很多,像AndroidStudio, source insight等,但是像studio设置过于麻烦,并且还对内存有要求,source insight付费的且对新手不太友好.所以我们就用免费开源轻巧的神器. opengrok,下面我们来一步一步来实现,保证是你看到的最详细的教程
Android 官方 https://source.android.com/setup/build/downloading 需要自带梯子
国内清华大学镜像 https://mirrors.tuna.tsinghua.edu.cn/help/AOSP/
下载完成之后随便起一个名字比如我的是sourcecode19.02
sudo apt-get install openjdk-8-jdk
https://tomcat.apache.org/download-90.cgi 我是用的9.0的
https://github.com/oracle/opengrok/releases 我是用的1.3.1
不要再使用 Exuberant ctags,因为已经不再维护更新,对于新版本的Opengrok支持度不好
1. git clone https://github.com/universal-ctags/ctags.git
2. 下载完成后,进入ctags文件夹,依次执行以下命令,完成编译和安装:
./autogen.sh
./configure
make
sudo make install
3. 使用which ctags查看安装路径,一会部署的时候要用到,我的路径是
/usr/local/bin/ctags
这里需要注意下,需要一个磁盘空间比较大的地方,因为一会生成索引的时候会在opengrok的目录里面生成.
mkdir src data etc
# src:源代码目录
# data:存放opengrok索引文件目录
# etc:放置configuration.xml的目录,xml文件由opengrok生成,我们只需要配置路径。
原本需要把源码放到src下,因为我的android源代码放在了~/sourcecode19.02下,没必要把它整个移动到src下,所以在src下建了个sourcecode19.02的软连接指向它:
cd src
ln -s ~/sourcecode19.02 sourcecode19.02
像这样
cp ~/opengrok-1.2.2/lib/source.war /opt/tomcat8.5/webapps/
在web.xml中修改configuration.xml的路径,
vi ~/tomcat9.0/webapps/source/WEB-INF/web.xml
<param-value>~/opengrok-1.3.1/etc/configuration.xml</param-value>
应该会看到如下的错误
opengrok configuration parameter has not been configured in web.xml
这是因为我们没有生成索引,接下来我们来生成索引
我是把下面的命令封装成了一个.sh文件,执行的时候尽量用sudo sh xxxx.sh执行,
#!/bin/bash
java -Xmx8g \
-jar /home/pxwen/WorkStation/opengrok/opengrok-1.3.1/lib/opengrok.jar \
-c /usr/local/bin/ctags \
-s /home/pxwen/WorkStation/opengrok/opengrok-1.3.1/src -d /home/pxwen/WorkStation/opengrok/opengrok-1.3.1/data -H -P -S -G -v \
-W /home/pxwen/WorkStation/opengrok/opengrok-1.3.1/etc/configuration.xml -U http://localhost:8080/source \
--depth 100000 \
--progress \
-m 2048
同样的办法在src下建立软链接,然后执行上面生成索引的脚本. 等生成完了之后就可以看到了.
删除项目的话,把刚才建立的软链接删掉,然后在重新生成索引.在浏览器看不到项目了.
如何写一个本地的C程序在Android下执行
本地的一个c程序在系统下一直编译不过, 就想着自己写一个简单的测试程序,测试下是否是系统编译的问题
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#define MAX_BUF_COUNT (4)
int main() {
printf("hello\n");
return 0;
}
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS) //必须把上次的环境变量清空掉
#LOCAL_MODULE_TAGS := optional //指定在那种环境下可以编译,user userdebug
app_src_files := \
test.c \
LOCAL_SRC_FILES:= $(app_src_files)
LOCAL_MODULE:= pxwendemo
include $(BUILD_EXECUTABLE)
在系统目录下建立文件夹,把上面的两个文件copy进来 执行mm命令, 可见生成文件在out/…/system/bin下面
push生成的bin文件在手机里面system/bin目录下面, 然后执行 adb shell pxwendemo(以你自己编译出来的模块为准) 就可以看到此程序执行了
C语言的真正编译过程
我们分部来看下每一步都做了什么
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("hello world!\n");
return 0;
}
通过查看文件内容和文件大小可以得知a.c讲stdio.h和stdlib.h包含了进来
预处理过程实质上是处理“#”,将#include包含的头文件直接拷贝到hello.c当中;将#define定义的宏进行替换,同时将代码中没用的注释部分删除等
具体做的事儿如下:
(1)将所有的#define删除,并且展开所有的宏定义。说白了就是字符替换
(2)处理所有的条件编译指令,#ifdef #ifndef #endif等,就是带#的那些
(3)处理#include,将#include指向的文件插入到该行处
(4)删除所有注释
(5)添加行号和文件标示,这样的在调试和编译出错的时候才知道是是哪个文件的哪一行
(6)保留#pragma编译器指令,因为编译器需要使用它们.(#pragma once 防止重复引用头文件)
可以查看生成的a.c文件,把所有的include的文件插入进指定的位置. 所有宏都已经展开
(1)词法分析,
(2)语法分析
(3)语义分析
(4)优化后生成相应的汇编代码
1 .file "hello.c"
2 .section .rodata
3 .LC0:
4 .string "hello world!"
5 .text
6 .globl main
7 .type main, @function
8 main:
9 .LFB0:
10 .cfi_startproc
11 pushl %ebp
12 .cfi_def_cfa_offset 8
13 .cfi_offset 5, -8
14 movl %esp, %ebp
15 .cfi_def_cfa_register 5
16 andl $-16, %esp
17 subl $16, %esp
18 movl $.LC0, (%esp)
19 call puts
20 movl $0, %eax
21 leave
22 .cfi_restore 5
23 .cfi_def_cfa 4, 4
24 ret
25 .cfi_endproc
26 .LFE0:
27 .size main, .-main
28 .ident "GCC: (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3"
29 .section .note.GNU-stack,"",@progbits
计算机只识别二进制文件
就像刚才的hello.c它使用到了C标准库的东西“printf”,但是编译过程只是把源文件翻译成二进制而已,这个二进制还不能直接执行,这个时候就需要做一个动作,将翻译成的二进制与需要用到库绑定在一块
举个例子,像printf函数是系统定义的函数,有系统库,我们只是做了一个动作,把库文件和我们的文件做了一个链接: 可以通过ldd的指令查看
pxwen@pxwen-Think:~/Workstation/cmake/studycmake$ ldd main
linux-vdso.so.1 => (0x00007ffcacb9d000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fd1e3828000)
/lib64/ld-linux-x86-64.so.2 (0x000055931be8f000)
可以看到是和系统下的库做了一个链接
其实我们直接可以gcc hello.c 就可以输出一个在Linux下的可执行文件, 我只是把这个过程做了一个分步,希望大家看的更清楚一点. gcc hello.c 其实是把这四个步骤合并了,希望对大家有点帮助。
如何正确的存放自己编写的二进制文件的路径
自己写了一个简单的c程序,然后放在系统的根目录下,执行mm编译,结果
error: unused parameter 'argc' [-Werror,-Wunused-parameter]
仔细检查了程序,发现并没有错,只是有一个参数我并没有用到, 上网Google之 关键字 Werror
Werror :: all warnings being treated as errors
所有的警告都被视为是错误信息
网上各种解决方案:只需要找到相应的Makefile,去掉编译选项中的 -Werror 即可。
可是我的makefile里面并没有加任何Werror的选项,这是怎么回事,那就在源码里面搜索 关键字吧,找到在一个.mk文件里面做了判断,类似这样::
if(什么目录下) 然后添加-Werror else 不添加
恍然大悟,是我放的目录不对啊。不应该放在根目录下的, 该地方,放在vendor下,编译成功.
由此得出结论,网上的未必靠谱,(只是给你提供了思路要去那个方向)要根据自己的代码实际情况去 看自己的code.
Android系统中的log都有那些,如何在JNI或者自己在Android系统下开发的程序中打印Log
Android 系统中的Log分为两类,一种是java层的, 一种是Native层的,
Log.d
Log.e
Log.v
Log.i
在此不做过多赘述, 最终还是通过jni调到系统的
1.源码位置,系统编译出liblog库
/system/core/liblog/include/android/log.h
2.在mk文件里面引用这个库文件
LOCAL_SHARED_LIBRARIES := \
liblog
3.在c或者c++代码中引用 #include <android/log.h>
ANDROID_LOG_DEBUG log.h文件里面定义的枚举,可以直接用,
__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
__VA_ARGS__ 这个是可变参数接收的类型
__android_log_print(ANDROID_LOG_DEBUG, "skyxiao", "test");
一般定义一个宏来表示可变参数,这个可以自己上网搜下,很简单的
1.源码位置,系统编译出libcutils库文件
/system/core/include/log/log_system.h
2.引入头文件
#include <cutils/log.h>
3.SLOGD("xxxxx");
定义宏
#define LOG_TAG "test"
为什么要这样定义?
参考源码:第二个参数是LOG_TAG 也可以不用定义
#ifndef ALOGE
242#define ALOGE(...) ((void)ALOG(LOG_ERROR, LOG_TAG, __VA_ARGS__))
#### 第一种方式:
上面的两种方式引入的库和头文件之后,可以直接用
ALOGE("xxxxx");
#### 第二种方式:
#include <utils/Log.h>
mk中添加
LOCAL_SHARED_LIBRARIES:= libcutils libutils liblog
ALOGE("xxxxx");
ALOGE(“%s(), %d”,__FUNCTION__,__LINE__);
可参考这个链接
<https://blog.csdn.net/u010164190/article/details/78659503>
详解Android 中的ScrollTo, ScrollBy方法
源码分析
/**
* Set the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the x position to scroll to
* @param y the y position to scroll to
*/
public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}
/**
* Move the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the amount of pixels to scroll by horizontally
* @param y the amount of pixels to scroll by vertically
*/
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
原来scrollBy最终还是调用scrollTo,那我们接着来看scrollTo,scrollTo改变的是View的mScrollX和mScrollY这两个属性,我们来看下文档对这两个属性的解释:
/**
* The offset, in pixels, by which the content of this view is scrolled
* horizontally.
* {@hide}
*/
@ViewDebug.ExportedProperty(category = "scrolling")
protected int mScrollX;
/**
* The offset, in pixels, by which the content of this view is scrolled
* vertically.
* {@hide}
*/
@ViewDebug.ExportedProperty(category = "scrolling")
protected int mScrollY;
指的是View内容的偏移量,如果是ViewGroup的话作用的就是它的所有子view,如果是TextView的话则作用的就是TextView的内容。这两个api作用的对象是view的内容而不是view本身。
从上面源码,注意scrollTo(int x,int y)与scrollBy里的参数都是指偏移量,scrollTo是一步到位直接修改偏移量为x或y,而scrollBy是在当前偏移量加减x或y。这样说好像不是很准确,我们先来看下Android坐标系以及一些参数
一般情况下View的坐标都是相对父ViewGroup,像以下的api:
是不是发现明明通过scrollTo设置的偏移量是-300,按照正常的逻辑以及android坐标系,应该向上移动,怎么还向下移动了呢,这时候我们就要深入源码来查看究竟了
/**
* Set the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the x position to scroll to
* @param y the y position to scroll to
*/
public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}
先来看看onScrollChanged
/**
* This is called in response to an internal scroll in this view (i.e., the
* view scrolled its own contents). This is typically as a result of
* {@link #scrollBy(int, int)} or {@link #scrollTo(int, int)} having been
* called.
*
* @param l Current horizontal scroll origin.
* @param t Current vertical scroll origin.
* @param oldl Previous horizontal scroll origin.
* @param oldt Previous vertical scroll origin.
*/
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
notifySubtreeAccessibilityStateChangedIfNeeded();
if (AccessibilityManager.getInstance(mContext).isEnabled()) {
postSendViewScrolledAccessibilityEventCallback();
}
mBackgroundSizeChanged = true;
mDefaultFocusHighlightSizeChanged = true;
if (mForegroundInfo != null) {
mForegroundInfo.mBoundsChanged = true;
}
final AttachInfo ai = mAttachInfo;
if (ai != null) {
ai.mViewScrollChanged = true;
}
if (mListenerInfo != null && mListenerInfo.mOnScrollChangeListener != null) {
mListenerInfo.mOnScrollChangeListener.onScrollChange(this, l, t, oldl, oldt);
}
}
当View注册了OnScrollChangeListener,onScrollChange才会被调用。 接着就调用了postInvalidateOnAnimation
/**
* <p>Cause an invalidate to happen on the next animation time step, typically the
* next display frame.</p>
*
* <p>This method can be invoked from outside of the UI thread
* only when this View is attached to a window.</p>
*
* @see #invalidate()
*/
public void postInvalidateOnAnimation() {
// We try only with the AttachInfo because there's no point in invalidating
// if we are not attached to our window
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
attachInfo.mViewRootImpl.dispatchInvalidateOnAnimation(this);
}
}
在来看看dispatchInvalidateOnAnimation的实现
public void dispatchInvalidateOnAnimation(View view) {
mInvalidateOnAnimationRunnable.addView(view);
}
final class InvalidateOnAnimationRunnable implements Runnable {
private boolean mPosted;
private final ArrayList<View> mViews = new ArrayList<View>();
private final ArrayList<AttachInfo.InvalidateInfo> mViewRects =
new ArrayList<AttachInfo.InvalidateInfo>();
private View[] mTempViews;
private AttachInfo.InvalidateInfo[] mTempViewRects;
public void addView(View view) {
synchronized (this) {
mViews.add(view);
postIfNeededLocked();
}
}
public void addViewRect(AttachInfo.InvalidateInfo info) {
synchronized (this) {
mViewRects.add(info);
postIfNeededLocked();
}
}
public void removeView(View view) {
synchronized (this) {
mViews.remove(view);
for (int i = mViewRects.size(); i-- > 0; ) {
AttachInfo.InvalidateInfo info = mViewRects.get(i);
if (info.target == view) {
mViewRects.remove(i);
info.recycle();
}
}
if (mPosted && mViews.isEmpty() && mViewRects.isEmpty()) {
mChoreographer.removeCallbacks(Choreographer.CALLBACK_ANIMATION, this, null);
mPosted = false;
}
}
}
@Override
public void run() {
final int viewCount;
final int viewRectCount;
synchronized (this) {
mPosted = false;
viewCount = mViews.size();
if (viewCount != 0) {
mTempViews = mViews.toArray(mTempViews != null
? mTempViews : new View[viewCount]);
mViews.clear();
}
viewRectCount = mViewRects.size();
if (viewRectCount != 0) {
mTempViewRects = mViewRects.toArray(mTempViewRects != null
? mTempViewRects : new AttachInfo.InvalidateInfo[viewRectCount]);
mViewRects.clear();
}
}
for (int i = 0; i < viewCount; i++) {
mTempViews[i].invalidate();
mTempViews[i] = null;
}
for (int i = 0; i < viewRectCount; i++) {
final View.AttachInfo.InvalidateInfo info = mTempViewRects[i];
info.target.invalidate(info.left, info.top, info.right, info.bottom);
info.recycle();
}
}
private void postIfNeededLocked() {
if (!mPosted) {
mChoreographer.postCallback(Choreographer.CALLBACK_ANIMATION, this, null);
mPosted = true;
}
}
}
接着会调用run方法,其中有
for (int i = 0; i < viewCount; i++) {
mTempViews[i].invalidate();
mTempViews[i] = null;
}
遍历所有添加进去的View,然后对每个View调用invalidate(),
/**
* Invalidate the whole view. If the view is visible,
* {@link #onDraw(android.graphics.Canvas)} will be called at some point in
* the future.
* <p>
* This must be called from a UI thread. To call from a non-UI thread, call
* {@link #postInvalidate()}.
*/
public void invalidate() {
invalidate(true);
}
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
boolean fullInvalidate) {
if (mGhostView != null) {
mGhostView.invalidate(true);
return;
}
if (skipInvalidate()) {
return;
}
if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)
|| (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)
|| (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED
|| (fullInvalidate && isOpaque() != mLastIsOpaque)) {
if (fullInvalidate) {
mLastIsOpaque = isOpaque();
mPrivateFlags &= ~PFLAG_DRAWN;
}
mPrivateFlags |= PFLAG_DIRTY;
if (invalidateCache) {
mPrivateFlags |= PFLAG_INVALIDATED;
mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
}
// Propagate the damage rectangle to the parent view.
final AttachInfo ai = mAttachInfo;
final ViewParent p = mParent;
if (p != null && ai != null && l < r && t < b) {
final Rect damage = ai.mTmpInvalRect;
damage.set(l, t, r, b);
p.invalidateChild(this, damage);
}
// Damage the entire projection receiver, if necessary.
if (mBackground != null && mBackground.isProjected()) {
final View receiver = getProjectionReceiver();
if (receiver != null) {
receiver.damageInParent();
}
}
}
}
注意这个地方, 这个地方是绘制view面的内容,这也解释了为什么说是用scrollto 或者 scrollby来 移动里面的内容, 而不是这个view本身
if (p != null && ai != null && l < r && t < b) {
final Rect damage = ai.mTmpInvalRect;
damage.set(l, t, r, b);
p.invalidateChild(this, damage);
}
接着
/**
* Don't call or override this method. It is used for the implementation of
* the view hierarchy.
*
* @deprecated Use {@link #onDescendantInvalidated(View, View)} instead to observe updates to
* draw state in descendants.
*/
@Deprecated
@Override
public final void invalidateChild(View child, final Rect dirty) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null && attachInfo.mHardwareAccelerated) {
// HW accelerated fast path
onDescendantInvalidated(child, child);
return;
}
ViewParent parent = this;
if (attachInfo != null) {
// If the child is drawing an animation, we want to copy this flag onto
// ourselves and the parent to make sure the invalidate request goes
// through
final boolean drawAnimation = (child.mPrivateFlags & PFLAG_DRAW_ANIMATION) != 0;
// Check whether the child that requests the invalidate is fully opaque
// Views being animated or transformed are not considered opaque because we may
// be invalidating their old position and need the parent to paint behind them.
Matrix childMatrix = child.getMatrix();
final boolean isOpaque = child.isOpaque() && !drawAnimation &&
child.getAnimation() == null && childMatrix.isIdentity();
// Mark the child as dirty, using the appropriate flag
// Make sure we do not set both flags at the same time
int opaqueFlag = isOpaque ? PFLAG_DIRTY_OPAQUE : PFLAG_DIRTY;
if (child.mLayerType != LAYER_TYPE_NONE) {
mPrivateFlags |= PFLAG_INVALIDATED;
mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
}
final int[] location = attachInfo.mInvalidateChildLocation;
location[CHILD_LEFT_INDEX] = child.mLeft;
location[CHILD_TOP_INDEX] = child.mTop;
if (!childMatrix.isIdentity() ||
(mGroupFlags & ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS) != 0) {
RectF boundingRect = attachInfo.mTmpTransformRect;
boundingRect.set(dirty);
Matrix transformMatrix;
if ((mGroupFlags & ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS) != 0) {
Transformation t = attachInfo.mTmpTransformation;
boolean transformed = getChildStaticTransformation(child, t);
if (transformed) {
transformMatrix = attachInfo.mTmpMatrix;
transformMatrix.set(t.getMatrix());
if (!childMatrix.isIdentity()) {
transformMatrix.preConcat(childMatrix);
}
} else {
transformMatrix = childMatrix;
}
} else {
transformMatrix = childMatrix;
}
transformMatrix.mapRect(boundingRect);
dirty.set((int) Math.floor(boundingRect.left),
(int) Math.floor(boundingRect.top),
(int) Math.ceil(boundingRect.right),
(int) Math.ceil(boundingRect.bottom));
}
do {
View view = null;
if (parent instanceof View) {
view = (View) parent;
}
if (drawAnimation) {
if (view != null) {
view.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
} else if (parent instanceof ViewRootImpl) {
((ViewRootImpl) parent).mIsAnimating = true;
}
}
// If the parent is dirty opaque or not dirty, mark it dirty with the opaque
// flag coming from the child that initiated the invalidate
if (view != null) {
if ((view.mViewFlags & FADING_EDGE_MASK) != 0 &&
view.getSolidColor() == 0) {
opaqueFlag = PFLAG_DIRTY;
}
if ((view.mPrivateFlags & PFLAG_DIRTY_MASK) != PFLAG_DIRTY) {
view.mPrivateFlags = (view.mPrivateFlags & ~PFLAG_DIRTY_MASK) | opaqueFlag;
}
}
parent = parent.invalidateChildInParent(location, dirty);
if (view != null) {
// Account for transform on current parent
Matrix m = view.getMatrix();
if (!m.isIdentity()) {
RectF boundingRect = attachInfo.mTmpTransformRect;
boundingRect.set(dirty);
m.mapRect(boundingRect);
dirty.set((int) Math.floor(boundingRect.left),
(int) Math.floor(boundingRect.top),
(int) Math.ceil(boundingRect.right),
(int) Math.ceil(boundingRect.bottom));
}
}
} while (parent != null);
}
}
会有这样的一行代码:parent = parent.invalidateChildInParent(location, dirty);
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
if(this.mHostView != null) {
dirty.offset(location[0], location[1]);
if(this.mHostView instanceof ViewGroup) {
location[0] = 0;
location[1] = 0;
int[] offset = new int[2];
this.getOffset(offset);
dirty.offset(offset[0], offset[1]);
return super.invalidateChildInParent(location, dirty);
}
this.invalidate(dirty);
}
return null;
}
/**
* Mark the area defined by dirty as needing to be drawn. If the view is
* visible, {@link #onDraw(android.graphics.Canvas)} will be called at some
* point in the future.
* <p>
* This must be called from a UI thread. To call from a non-UI thread, call
* {@link #postInvalidate()}.
* <p>
* <b>WARNING:</b> In API 19 and below, this method may be destructive to
* {@code dirty}.
*
* @param dirty the rectangle representing the bounds of the dirty region
*/
public void invalidate(Rect dirty) {
final int scrollX = mScrollX;
final int scrollY = mScrollY;
invalidateInternal(dirty.left - scrollX, dirty.top - scrollY,
dirty.right - scrollX, dirty.bottom - scrollY, true, false);
}
这个地方都是减去scrollX 和减去scrollY, 当你传入负数的时候就是正值了,所以, 我们就和开篇对应起来了, 传入负数向正方向移动, 传入正数向负方向移动